Repository navigation
ci(devcontainer): add :sha tag and prune stale build intermediates (#93) #6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |