# Builds and pushes a site's container image, then opens a petersweb-infra # PR that pins it by digest (README.md). Runs on runner-x86-linux (host # mode: podman, git, curl, jq on the host). name: site-image description: >- Build and push a site's image, then open a petersweb-infra PR that bumps the image's pinned digest. inputs: image: description: The image, without a tag, as petersweb-infra pins it (forge.in.quineglobal.com//) required: true infra-file: description: The petersweb-infra file that pins it as @sha256: required: true name: description: Short name for the branch and PR title (default the image's last path part) default: '' context: description: Build context default: . containerfile: description: Containerfile or Dockerfile (default podman's choice in the context) default: '' platform: description: Platform to build for default: linux/amd64 registry-username: description: Account that pushes the image required: true registry-password: description: Its token (package write) required: true infra-token: description: Token that can push a branch to petersweb-infra and open a PR there required: true infra-repo: description: The infra repo default: Cold-Air-Networks/petersweb-infra server: description: >- The forge's address, for cloning, the API and links. Not github.server_url: a runner next to Forgejo (the forge VM's) is told http://localhost:3000, which a job container can't reach. default: https://forge.in.quineglobal.com outputs: digest: description: The pushed image's digest (sha256:...) value: ${{ steps.push.outputs.digest }} pr: description: The PR's URL, or empty if petersweb-infra already pins this digest value: ${{ steps.pr.outputs.url }} runs: using: composite steps: - name: Build and push the image id: push shell: bash env: IMAGE: ${{ inputs.image }} CONTEXT: ${{ inputs.context }} CONTAINERFILE: ${{ inputs.containerfile }} PLATFORM: ${{ inputs.platform }} REGISTRY_USERNAME: ${{ inputs.registry-username }} REGISTRY_PASSWORD: ${{ inputs.registry-password }} SHA: ${{ github.sha }} run: | set -euo pipefail printf %s "$REGISTRY_PASSWORD" | podman login "${IMAGE%%/*}" -u "$REGISTRY_USERNAME" --password-stdin args=(--platform "$PLATFORM" -t "$IMAGE:$SHA") [ -z "$CONTAINERFILE" ] || args+=(-f "$CONTAINERFILE") podman build "${args[@]}" "$CONTEXT" digestfile=$(mktemp) podman push --retry 5 --digestfile "$digestfile" "$IMAGE:$SHA" digest=$(cat "$digestfile") rm -f "$digestfile" echo "pushed $IMAGE:$SHA as $digest" echo "digest=$digest" >>"$GITHUB_OUTPUT" - name: Open a petersweb-infra PR id: pr shell: bash env: IMAGE: ${{ inputs.image }} INFRA_FILE: ${{ inputs.infra-file }} INFRA_REPO: ${{ inputs.infra-repo }} INFRA_TOKEN: ${{ inputs.infra-token }} NAME: ${{ inputs.name }} SERVER: ${{ inputs.server }} DIGEST: ${{ steps.push.outputs.digest }} SHA: ${{ github.sha }} run: | set -euo pipefail server=$SERVER name=${NAME:-${IMAGE##*/}} short=${SHA:0:7} branch="bump-$name-$short" commit_url="$server/$GITHUB_REPOSITORY/commit/$SHA" run_url="$server/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" api="$server/api/v1/repos/$INFRA_REPO" auth=(-H "Authorization: token $INFRA_TOKEN") # Close this image's other open bump PRs (bump--<7 hex>, opened # by earlier builds), with a comment saying why, and delete their # branches: the latest build is the one to merge, and merging an # older one after it would pin an older image. close_stale() { local why=$1 keep=${2:-} prs n ref prs=$(curl -fsS "${auth[@]}" "$api/pulls?state=open&limit=50" | jq -r --arg p "bump-$name-" --arg keep "$keep" '.[] | select(.head.ref | startswith($p) and (.[($p|length):] | test("^[0-9a-f]{7}$"))) | select(.head.ref != $keep) | "\(.number) \(.head.ref)"') while read -r n ref; do [ -n "$n" ] || continue curl -fsS -o /dev/null -X POST "${auth[@]}" -H "Content-Type: application/json" \ "$api/issues/$n/comments" -d "$(jq -n --arg b "$why" '{body: $b}')" curl -fsS -o /dev/null -X PATCH "${auth[@]}" -H "Content-Type: application/json" \ "$api/pulls/$n" -d '{"state": "closed"}' curl -fsS -o /dev/null -X DELETE "${auth[@]}" "$api/branches/$ref" || echo "::warning::couldn't delete $ref" echo "closed #$n ($ref)" done <<<"$prs" } work=$(mktemp -d) git clone -q --depth 1 "https://ci:$INFRA_TOKEN@${server#https://}/$INFRA_REPO.git" "$work" cd "$work" if ! grep -qF "$IMAGE@sha256:" "$INFRA_FILE"; then echo "::error::$INFRA_REPO's $INFRA_FILE doesn't pin $IMAGE@sha256:" exit 1 fi sed -i -E "s|(${IMAGE//./\\.}@)sha256:[0-9a-f]{64}|\1$DIGEST|" "$INFRA_FILE" if git diff --quiet; then echo "$INFRA_REPO already pins $IMAGE@$DIGEST" close_stale "Superseded: main already pins \`$IMAGE@$DIGEST\`, built from $commit_url ($run_url)." echo "url=" >>"$GITHUB_OUTPUT" exit 0 fi git switch -q -c "$branch" git -c user.name="Site image CI" -c user.email="ci@quinefoundation.com" \ commit -q -a -m "$name: bump to $short" \ -m "$IMAGE@$DIGEST, built from $commit_url ($run_url)." git push -q origin "$branch" body=$(jq -n --arg t "$name: bump to $short" --arg h "$branch" \ --arg b "Pins \`$IMAGE@$DIGEST\` in \`$INFRA_FILE\`, built from $commit_url by $run_url." \ '{title: $t, head: $h, base: "main", body: $b}') url=$(curl -fsS -X POST "${auth[@]}" -H "Content-Type: application/json" \ "$api/pulls" -d "$body" | jq -r .html_url) echo "opened $url" close_stale "Superseded by $url ($name: bump to $short)." "$branch" echo "url=$url" >>"$GITHUB_OUTPUT"