Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions .github/workflows/act-image-repin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Keeps the e2e act runner image pin fresh so it never ages into an upstream
# garbage-collection window.
#
# e2e/harness/act.go pins the catthehacker/ubuntu image by digest for a
# deterministic runtime. That digest is a bare upstream reference: if
# catthehacker retags act-latest and later garbage-collects the old untagged
# digest, every e2e run fails to pull it. This job re-resolves the current
# act-latest digest on a schedule, confirms it still carries the required Node
# major, and opens a repin pull request when the digest has moved. The pin
# therefore keeps tracking a live, tag-referenced digest instead of drifting
# toward a collectable one.
#
# The pull request is reviewer-gated like any other change: it swaps the e2e
# runtime, so a human confirms the suite stays green before it merges. The job
# refuses to repin to a digest below the Node floor, mirroring the harness
# startup preflight, so a bad upstream image cannot land unnoticed.
#
# The pull request is authored with the default token, so it does not itself
# fire the pull_request checks; add it to the merge queue to run the e2e suite
# before merging.
name: Act Runner Image Repin

on:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch: {}

permissions:
contents: read

jobs:
repin:
name: Re-resolve act runner digest
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
env:
IMAGE_REPO: ghcr.io/catthehacker/ubuntu
IMAGE_TAG: act-latest
NODE_MAJOR_MIN: '24'
ACT_GO: e2e/harness/act.go
GH_TOKEN: ${{ github.token }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Resolve current upstream digest
id: resolve
run: |
set -euo pipefail
ref="${IMAGE_REPO}:${IMAGE_TAG}"
docker pull "$ref"

node_version="$(docker run --rm "$ref" node --version)"
node_major="${node_version#v}"
node_major="${node_major%%.*}"
if [ "$node_major" -lt "$NODE_MAJOR_MIN" ]; then
echo "::error::${ref} provides Node ${node_version}; the e2e suite requires Node >= ${NODE_MAJOR_MIN}. Refusing to repin."
exit 1
fi

new_ref="$(docker inspect --format '{{index .RepoDigests 0}}' "$ref")"
new_digest="${new_ref#*@}"
case "$new_digest" in
sha256:*) ;;
*) echo "::error::could not resolve a sha256 digest from ${new_ref}"; exit 1 ;;
esac

current_ref="$(grep -oE 'ghcr\.io/catthehacker/ubuntu@sha256:[0-9a-f]+' "$ACT_GO" | head -n1)"
current_digest="${current_ref#*@}"
if [ -z "$current_digest" ]; then
echo "::error::could not read the current digest from ${ACT_GO}"
exit 1
fi

{
echo "current=${current_digest}"
echo "resolved=${new_digest}"
} >> "$GITHUB_OUTPUT"
if [ "$current_digest" = "$new_digest" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "Digest unchanged (${current_digest}); nothing to repin."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "Digest moved: ${current_digest} -> ${new_digest}"
fi

- name: Apply repin
if: steps.resolve.outputs.changed == 'true'
env:
CURRENT: ${{ steps.resolve.outputs.current }}
RESOLVED: ${{ steps.resolve.outputs.resolved }}
run: |
set -euo pipefail
sed -i "s#@${CURRENT}#@${RESOLVED}#g" "$ACT_GO"
if ! grep -q "@${RESOLVED}" "$ACT_GO"; then
echo "::error::failed to apply the digest repin to ${ACT_GO}"
exit 1
fi

- name: Set up Go
if: steps.resolve.outputs.changed == 'true'
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.25"

- name: Verify e2e module builds
if: steps.resolve.outputs.changed == 'true'
working-directory: e2e
run: go build ./...

- name: Open repin pull request
if: steps.resolve.outputs.changed == 'true'
env:
CURRENT: ${{ steps.resolve.outputs.current }}
RESOLVED: ${{ steps.resolve.outputs.resolved }}
run: |
set -euo pipefail
short="${RESOLVED#sha256:}"
short="${short:0:12}"
branch="ci/act-repin-${short}"

if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
echo "Branch ${branch} already exists; a repin pull request is already open."
exit 0
fi

git config user.name "cascade-bot"
git config user.email "cascade-bot@users.noreply.github.com"
git checkout -b "$branch"
git add "$ACT_GO"
git commit -s -m "ci(e2e): repin act runner image to current upstream digest"
git push origin "$branch"

body="$(printf 'The e2e act runner image pin was re-resolved to the current %s:%s digest so it stays ahead of any upstream garbage-collection window.\n\nWas: %s\nNow: %s\n\nThe resolver confirmed the new image still carries Node >= %s and that the e2e module builds against it. Add this to the merge queue so the e2e suite runs before merge.\n' "$IMAGE_REPO" "$IMAGE_TAG" "$CURRENT" "$RESOLVED" "$NODE_MAJOR_MIN")"

gh pr create \
--base main \
--head "$branch" \
--title "ci(e2e): repin act runner image to current upstream digest" \
--body "$body"
8 changes: 7 additions & 1 deletion e2e/harness/act.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ const actStartupPollInterval = 2 * time.Second
// publishing dated act-latest-YYYYMMDD tags in 2023 (all predate Node 24), so a
// digest is the only deterministic reference that reaches a Node 24 runtime.
//
// To repin: pull ghcr.io/catthehacker/ubuntu:act-latest, confirm
// The scheduled Act Runner Image Repin workflow
// (.github/workflows/act-image-repin.yml) re-resolves act-latest to its current
// digest and opens a repin pull request whenever it moves, so this pin keeps
// tracking a live, tag-referenced digest ahead of any upstream
// garbage-collection window. The manual recipe below stays the fallback.
//
// To repin by hand: pull ghcr.io/catthehacker/ubuntu:act-latest, confirm
// `docker run --rm <image> node --version` reports >= actRunnerNodeMajorMin,
// then record the resolved digest here
// (`docker inspect --format '{{index .RepoDigests 0}}' <image>`).
Expand Down
Loading