Skip to content

ci(devcontainer): add :sha tag and prune stale build intermediates (#93) #6

ci(devcontainer): add :sha tag and prune stale build intermediates (#93)

ci(devcontainer): add :sha tag and prune stale build intermediates (#93) #6

name: DevContainer Prebuild
on:
push:
branches:
- main
paths:
- '.devcontainer/**'
- '.github/workflows/devcontainer-cache.yml'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: write
id-token: write
env:
DEVCONTAINER_IMAGE: ghcr.io/crunchloop/devcontainer/devcontainer
jobs:
prebuild:
name: Build devcontainer prebuild (${{ matrix.platform }})
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# devcontainers/ci doesn't cleanly expose digest-only push, so we push
# per-arch tags and merge them into a multi-arch manifest below.
- name: Build devcontainer prebuild image
uses: devcontainers/ci@v0.3
with:
configFile: .devcontainer/devcontainer-build.json
imageName: ${{ env.DEVCONTAINER_IMAGE }}
imageTag: build-${{ github.run_id }}-${{ matrix.suffix }}
platform: ${{ matrix.platform }}
cacheFrom: ${{ env.DEVCONTAINER_IMAGE }}:buildcache-${{ matrix.suffix }}
push: always
merge:
name: Merge multi-arch manifest
needs: prebuild
runs-on: ubuntu-latest
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
run: |
# Tag the multi-arch manifest as :latest (the rolling tag developers
# pull) and as an immutable :sha-<commit> tag for pinning/rollback.
short_sha="${GITHUB_SHA::12}"
docker buildx imagetools create \
-t ${{ env.DEVCONTAINER_IMAGE }}:latest \
-t ${{ env.DEVCONTAINER_IMAGE }}:sha-${short_sha} \
${{ env.DEVCONTAINER_IMAGE }}:build-${{ github.run_id }}-amd64 \
${{ env.DEVCONTAINER_IMAGE }}:build-${{ github.run_id }}-arm64
- name: Inspect manifest
run: docker buildx imagetools inspect ${{ env.DEVCONTAINER_IMAGE }}:latest
# Best-effort cleanup of the per-arch build-<run_id>-* intermediates that
# devcontainers/ci has to push (it can't push digest-only). This is
# non-fatal: a failure here must never block a published :latest.
#
# Safety: the current run's intermediates share their image digests with
# the children of the :latest manifest list we just pushed — deleting
# those versions would break :latest. So we only delete build-* versions
# whose digest is NOT referenced by the current :latest, and never touch
# versions tagged latest or sha-*.
- name: Prune stale per-arch build intermediates
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -uo pipefail
owner="${GITHUB_REPOSITORY_OWNER}"
pkg="devcontainer%2Fdevcontainer" # url-encoded "devcontainer/devcontainer"
# Digests referenced by the just-pushed :latest manifest list. These
# must be preserved.
mapfile -t keep_digests < <(
docker buildx imagetools inspect --raw "${DEVCONTAINER_IMAGE}:latest" \
| jq -r '.manifests[].digest'
)
echo "Protected digests (referenced by :latest):"
printf ' %s\n' "${keep_digests[@]}"
gh api --paginate "/orgs/${owner}/packages/container/${pkg}/versions" \
| jq -c '.[]' | while read -r v; do
id=$(jq -r '.id' <<<"$v")
digest=$(jq -r '.name' <<<"$v")
tags=$(jq -r '(.metadata.container.tags // []) | join(",")' <<<"$v")
# Only consider intermediates; never delete latest/sha-* versions.
case ",${tags}," in
*",latest,"*) continue ;;
esac
[[ "${tags}" == *sha-* ]] && continue
[[ "${tags}" == *build-* ]] || continue
# Preserve anything still referenced by the current :latest.
for d in "${keep_digests[@]}"; do
[ "${d}" = "${digest}" ] && { echo "keep ${id} (${tags}) — in :latest"; continue 2; }
done
echo "prune ${id} (tags: ${tags})"
gh api -X DELETE "/orgs/${owner}/packages/container/${pkg}/versions/${id}" \
&& echo " deleted" || echo " delete failed (non-fatal)"
done