mirror of
https://github.com/wirenboard/agent-vm.git
synced 2026-07-09 16:00:54 +00:00
image: make :latest manifest deterministic so unchanged pulls are no-ops
PR #15 stopped the agent layers from rebuilding when no agent released, but consumers still re-downloaded the whole image (~700 MiB) on every hourly :latest. Root cause: the image config carried a fresh build timestamp each hour (the `org.opencontainers.image.created=<hour>` label + buildkit's own config/history timestamps), so the linux/amd64 manifest digest changed every cron build despite byte-identical layers. microsandbox keys its fsmeta/VMDK materialization on the manifest digest (registry/client.rs: `layer_force = force || !fsmeta_valid`, fsmeta keyed by manifest_digest), and `agent-vm pull` uses PullPolicy::Always — so a new manifest digest forces re-download + re-materialize of every layer. Measured: pulling a fresh hourly tag with byte-identical layers re-fetched 723 MiB; pulling the SAME manifest again fetched 0. Make the manifest deterministic so it only moves on real content change: - SOURCE_DATE_EPOCH = HEAD commit time, fed to buildkit, clamps the config `created` field and history timestamps to a per-commit constant. Same commit + same agent versions + same layers => identical config => identical manifest digest. - Drop the per-hour `created` label (it lived in the config and was the main churn source). `revision` (commit sha) is stable across a commit's hourly builds. - provenance: false — the attestation manifest embeds build time and would churn the multi-arch index digest (and the update banner) even with a stable image manifest; we don't consume provenance. Result: an unchanged hourly :latest keeps the same digest, so `agent-vm pull`/`setup` is a true no-op (0 bytes) instead of re-downloading ~700 MiB. A new commit or a changed agent layer moves the digest as it should. Follow-up to #15. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016gtxsKVWPrR8K1M1Te57MG
This commit is contained in:
parent
ac20c8e746
commit
aaf88cfa46
1 changed files with 39 additions and 2 deletions
41
.github/workflows/build-image.yml
vendored
41
.github/workflows/build-image.yml
vendored
|
|
@ -44,10 +44,26 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Compute timestamp tag (UTC)
|
||||
- name: Compute timestamp tag + source epoch
|
||||
id: ts
|
||||
run: |
|
||||
# Immutable hourly pin (:YYYY-MM-DDTHH).
|
||||
echo "tag=$(date -u +%Y-%m-%dT%H)" >> "$GITHUB_OUTPUT"
|
||||
# SOURCE_DATE_EPOCH = HEAD commit time. Fed to buildkit (see
|
||||
# the build step), it clamps the image config's `created`
|
||||
# field and every history timestamp to a value that is
|
||||
# CONSTANT across the hourly cron builds of a given commit.
|
||||
# So two builds with the same commit + same agent versions +
|
||||
# same layers produce a byte-identical config → an identical
|
||||
# linux/amd64 manifest digest. microsandbox keys its
|
||||
# fsmeta/VMDK materialization on THAT digest, so an unchanged
|
||||
# `:latest` becomes a true no-op pull (0 bytes) instead of
|
||||
# re-downloading the whole image. A new commit, or any changed
|
||||
# agent layer, moves the digest as it should. Without this the
|
||||
# config carried a fresh build timestamp every hour, so every
|
||||
# cron `:latest` had a new manifest digest and consumers
|
||||
# re-pulled ~700 MiB of byte-identical layers hourly.
|
||||
echo "epoch=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
|
@ -108,6 +124,13 @@ jobs:
|
|||
|
||||
- name: Build and push
|
||||
id: build
|
||||
env:
|
||||
# Clamps buildkit's image-config `created` + history
|
||||
# timestamps to the commit time (see "Compute timestamp tag +
|
||||
# source epoch") so an unchanged build yields an identical
|
||||
# manifest digest. docker/build-push-action forwards
|
||||
# SOURCE_DATE_EPOCH from the environment to buildkit.
|
||||
SOURCE_DATE_EPOCH: ${{ steps.ts.outputs.epoch }}
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: images
|
||||
|
|
@ -145,6 +168,13 @@ jobs:
|
|||
# change (the common hourly case).
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
# No provenance/SBOM attestation. The attestation manifest
|
||||
# embeds the build time, so it changes the multi-arch index
|
||||
# digest on every build even when the image content is
|
||||
# identical — which would keep `:latest`'s digest churning
|
||||
# hourly (and nag the update banner) despite a stable
|
||||
# linux/amd64 manifest. We don't consume provenance anywhere.
|
||||
provenance: false
|
||||
# Moving tags (`:latest`, `:YYYY-MM-DDTHH`) are gated to
|
||||
# the integration branches (`main`, `rewrite-microsandbox`)
|
||||
# so workflow_dispatch on a feature branch can verify the
|
||||
|
|
@ -161,10 +191,17 @@ jobs:
|
|||
${{ env.IMAGE }}:sha-${{ github.sha }}
|
||||
# Tag with the short SHA too so we can trace any image
|
||||
# back to the exact Dockerfile commit.
|
||||
# NOTE: no `org.opencontainers.image.created` label. It was
|
||||
# set to the per-hour timestamp and lived in the image config,
|
||||
# so it changed the manifest digest every cron build even with
|
||||
# identical content (the root cause of the hourly re-pull). The
|
||||
# config still carries a `created` field, but clamped to the
|
||||
# commit time via SOURCE_DATE_EPOCH, so it's stable per commit.
|
||||
# `revision` (the commit sha) is stable across a commit's
|
||||
# hourly builds, so it doesn't reintroduce churn.
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.created=${{ steps.ts.outputs.tag }}
|
||||
|
||||
- name: Show pushed digest
|
||||
run: |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue