🏡 index : quine-global/site-image.git

# 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/<owner>/<name>)
    required: true
  infra-file:
    description: The petersweb-infra file that pins it as <image>@sha256:<digest>
    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-<name>-<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:<digest>"
          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"