From 2daadcdf2713858c9b61c87337a9701b22425a78 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Fri, 3 Jul 2026 12:54:52 -0400 Subject: [PATCH] feature: beta release channel builds + per-channel image retention Build the beta branch into beta-prefixed tags and buildcache, and replace delete-package-versions (which matches version digests, not container tags) with a tag-based prune that only touches the channel being built. Co-Authored-By: Claude Fable 5 --- .github/workflows/build.yaml | 77 ++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7f00b8d..0669c53 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,12 +4,17 @@ on: push: branches: - "main" + - "beta" workflow_dispatch: concurrency: - group: build + group: build-${{ github.ref_name }} cancel-in-progress: true +env: + CHANNEL: ${{ github.ref_name == 'main' && 'latest' || 'beta' }} + TAG_PREFIX: ${{ github.ref_name != 'main' && 'beta-' || '' }} + jobs: build-amd64: runs-on: ubuntu-24.04 @@ -31,11 +36,11 @@ jobs: file: ./Dockerfile push: true platforms: linux/amd64 - cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/demo-parser:buildcache-amd64 - cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/demo-parser:buildcache-amd64,mode=max + cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/demo-parser:buildcache-${{ env.TAG_PREFIX }}amd64 + cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/demo-parser:buildcache-${{ env.TAG_PREFIX }}amd64,mode=max tags: | - ghcr.io/${{ github.repository_owner }}/demo-parser:latest-amd64 - ghcr.io/${{ github.repository_owner }}/demo-parser:${{ github.sha }}-amd64 + ghcr.io/${{ github.repository_owner }}/demo-parser:${{ env.CHANNEL }}-amd64 + ghcr.io/${{ github.repository_owner }}/demo-parser:${{ env.TAG_PREFIX }}${{ github.sha }}-amd64 build-arm64: runs-on: ubuntu-24.04-arm steps: @@ -56,14 +61,16 @@ jobs: file: ./Dockerfile push: true platforms: linux/arm64 - cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/demo-parser:buildcache-arm64 - cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/demo-parser:buildcache-arm64,mode=max + cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/demo-parser:buildcache-${{ env.TAG_PREFIX }}arm64 + cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/demo-parser:buildcache-${{ env.TAG_PREFIX }}arm64,mode=max tags: | - ghcr.io/${{ github.repository_owner }}/demo-parser:latest-arm64 - ghcr.io/${{ github.repository_owner }}/demo-parser:${{ github.sha }}-arm64 + ghcr.io/${{ github.repository_owner }}/demo-parser:${{ env.CHANNEL }}-arm64 + ghcr.io/${{ github.repository_owner }}/demo-parser:${{ env.TAG_PREFIX }}${{ github.sha }}-arm64 merge: needs: [build-amd64, build-arm64] runs-on: ubuntu-latest + permissions: + packages: write steps: - name: Log in to GitHub Docker Registry uses: docker/login-action@v3 @@ -73,16 +80,44 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Create manifest list and push run: | - docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/demo-parser:latest \ - ghcr.io/${{ github.repository_owner }}/demo-parser:latest-amd64 \ - ghcr.io/${{ github.repository_owner }}/demo-parser:latest-arm64 - docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/demo-parser:${{ github.sha }} \ - ghcr.io/${{ github.repository_owner }}/demo-parser:${{ github.sha }}-amd64 \ - ghcr.io/${{ github.repository_owner }}/demo-parser:${{ github.sha }}-arm64 - - name: Delete Package Versions - uses: actions/delete-package-versions@v5 + docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/demo-parser:${{ env.CHANNEL }} \ + ghcr.io/${{ github.repository_owner }}/demo-parser:${{ env.CHANNEL }}-amd64 \ + ghcr.io/${{ github.repository_owner }}/demo-parser:${{ env.CHANNEL }}-arm64 + docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/demo-parser:${{ env.TAG_PREFIX }}${{ github.sha }} \ + ghcr.io/${{ github.repository_owner }}/demo-parser:${{ env.TAG_PREFIX }}${{ github.sha }}-amd64 \ + ghcr.io/${{ github.repository_owner }}/demo-parser:${{ env.TAG_PREFIX }}${{ github.sha }}-arm64 + - name: Prune old images for channel + uses: actions/github-script@v7 with: - package-name: demo-parser - package-type: container - min-versions-to-keep: 9 - ignore-versions: '^buildcache-*' + script: | + const channel = context.ref === 'refs/heads/main' ? 'main' : 'beta'; + const keep = 9; + const owner = context.repo.owner; + const pkg = 'demo-parser'; + const isBuildcache = (t) => t.startsWith('buildcache'); + const isBeta = (t) => t === 'beta' || t.startsWith('beta-'); + const { data: ownerInfo } = await github.rest.users.getByUsername({ username: owner }); + const isOrg = ownerInfo.type === 'Organization'; + const listFn = isOrg + ? github.rest.packages.getAllPackageVersionsForPackageOwnedByOrg + : github.rest.packages.getAllPackageVersionsForPackageOwnedByUser; + const listArgs = isOrg + ? { package_type: 'container', package_name: pkg, org: owner, per_page: 100 } + : { package_type: 'container', package_name: pkg, username: owner, per_page: 100 }; + const versions = await github.paginate(listFn, listArgs); + const inChannel = versions.filter((v) => { + const tags = v.metadata?.container?.tags || []; + if (tags.length === 0) return false; + if (tags.some(isBuildcache)) return false; + const beta = tags.some(isBeta); + return channel === 'beta' ? beta : !beta; + }); + inChannel.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + const toDelete = inChannel.slice(keep); + for (const v of toDelete) { + await (isOrg + ? github.rest.packages.deletePackageVersionForOrg({ package_type: 'container', package_name: pkg, org: owner, package_version_id: v.id }) + : github.rest.packages.deletePackageVersionForUser({ package_type: 'container', package_name: pkg, username: owner, package_version_id: v.id })); + core.info(`deleted ${pkg} ${v.id} [${(v.metadata?.container?.tags || []).join(', ')}]`); + } + core.info(`channel=${channel} candidates=${inChannel.length} deleted=${toDelete.length}`);