universal-rebuild #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: universal-rebuild | |
| # Weekly rebuild of the universal image on top of the current :latest | |
| # vanilla image, so apt security updates flow into universal even when | |
| # kandev itself hasn't been re-released. | |
| # | |
| # - Pushes a moving `:universal-weekly-YYYYMMDD` tag plus refreshes | |
| # `:universal` (the "latest universal" alias). | |
| # - Does NOT push `:vX.Y.Z-universal`. Those are immutable per release; | |
| # only the floating `:universal` and the dated weekly tags move here. | |
| # | |
| # Build flow mirrors release.yml's docker-universal: push to a per-run | |
| # staging tag, gate on size against the staging tag, then promote the | |
| # manifest to the user-facing tags via `buildx imagetools create`. The | |
| # promote step is metadata-only - no rebuild, no extra layer upload - so | |
| # a failed size gate aborts before `:universal` ever points at an | |
| # oversized image (the previous week's tag remains live). | |
| # | |
| # Tradeoff: staging tags (one per weekly run) accumulate in GHCR. See the | |
| # comment in release.yml's docker-universal job for why per-run cleanup | |
| # isn't safe (shared manifest digest with promoted tags). A separate | |
| # cleanup workflow filtering for `universal-staging-*` versions whose | |
| # only tag matches is the right escape hatch if accumulation becomes a | |
| # problem; ~52 stale tags/year is acceptable in the meantime. | |
| # | |
| # Manual trigger is also wired up so a maintainer can force a rebuild | |
| # after a known CVE without waiting for the cron. | |
| on: | |
| schedule: | |
| # Mondays 06:00 UTC. Picks up the previous week's Debian security | |
| # patches without colliding with most weekday release work. | |
| - cron: "0 6 * * 1" | |
| workflow_dispatch: {} | |
| permissions: | |
| contents: read | |
| jobs: | |
| rebuild: | |
| name: Rebuild universal on top of :latest | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| STAGING_IMAGE: ghcr.io/kdlbs/kandev:universal-staging-weekly-${{ github.run_id }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Compute date tag | |
| id: date | |
| run: echo "tag=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT" | |
| - uses: docker/setup-buildx-action@v3 | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Rebuild against :latest deliberately - the whole point of the | |
| # weekly is to pick up any base-image moves. Use --pull so the | |
| # current :latest is freshly fetched, not whatever's in BuildKit's | |
| # cache from a previous run. | |
| - name: Build and push (staging tag) | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: Dockerfile.universal | |
| push: true | |
| pull: true | |
| platforms: linux/amd64,linux/arm64 | |
| build-args: | | |
| BASE_IMAGE=ghcr.io/kdlbs/kandev:latest | |
| tags: ${{ env.STAGING_IMAGE }} | |
| cache-from: type=gha,scope=universal | |
| cache-to: type=gha,mode=max,scope=universal | |
| # Same size gate as release.yml's docker-universal job. A base-image | |
| # update that adds significant weight (e.g. a new Debian dep pulled by | |
| # apt security updates) is caught here before :universal is promoted. | |
| # Keep MAX in sync with release.yml. | |
| - name: Enforce size budget | |
| env: | |
| MAX_COMPRESSED_BYTES: "2684354560" # 2.5 GiB | |
| run: | | |
| set -euo pipefail | |
| docker buildx imagetools inspect "$STAGING_IMAGE" --raw \ | |
| > /tmp/index.json | |
| AMD64_DIGEST="$(jq -r ' | |
| .manifests[] | |
| | select(.platform.architecture == "amd64" and .platform.os == "linux") | |
| | .digest | |
| ' /tmp/index.json)" | |
| docker buildx imagetools inspect "ghcr.io/kdlbs/kandev@${AMD64_DIGEST}" --raw \ | |
| > /tmp/manifest.json | |
| TOTAL="$(jq '[.layers[].size] | add' /tmp/manifest.json)" | |
| echo "Universal image (amd64) compressed size: $TOTAL bytes (max: $MAX_COMPRESSED_BYTES)" | |
| if [ "$TOTAL" -gt "$MAX_COMPRESSED_BYTES" ]; then | |
| echo "::error::Universal weekly rebuild exceeds compressed-size budget. Staging tag $STAGING_IMAGE will not be promoted; previous :universal remains live. See docs/images.md inclusion policy." >&2 | |
| exit 1 | |
| fi | |
| # Promote: metadata-only retag from staging to the user-facing tags. | |
| # If any earlier step fails, the previous :universal stays in place. | |
| - name: Promote staging to final tags | |
| run: | | |
| set -euo pipefail | |
| docker buildx imagetools create \ | |
| --tag "ghcr.io/kdlbs/kandev:universal" \ | |
| --tag "ghcr.io/kdlbs/kandev:universal-weekly-${{ steps.date.outputs.tag }}" \ | |
| "$STAGING_IMAGE" |