ci: shadow the label-based reuse key before trusting it (318 step 3)
CI / lint (push) Successful in 4s
Build images / sign-extension (push) Successful in 5s
CI / extension-version (push) Successful in 5s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 18s
extension / lint (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 29s
CI / integration (push) Successful in 3m50s

Additive and inert. Every build now stamps `fc.revision` as an image label,
and a diagnostic step tries to read it back off the channel tag. Nothing
downstream reads either yet — the reuse check still keys on the r-<rev> tag.

The question step 3 has to answer before committing: can the reuse check be
keyed on a LABEL read off the channel tag instead of on a separate tag? If
it can, nothing is minted per build, the -main/-dev identity split
disappears with CHANNELLED, and #3154's base-refresh clobber stops existing
rather than needing a workaround. It also closes a defect found in what
already shipped: r-<rev> tags do not match the registry's keep_pattern
(#3157), so they are prunable, and a pruned identity tag silently costs a
rebuild. A label cannot be pruned — it rides inside a tag that must exist.

Three probes rather than one, because `--format` templating over .Image
varies by buildx version and one round trip per guess is a poor trade.

Two questions, answered at different times, and conflating them would
abandon a working design:

1. Do the template mechanics work here? Answered on the first run — if the
   probes print a labels map, even an empty one, `--format` resolves on this
   buildx and this registry. That is the real gate.
2. Does a value round-trip? Only once an artifact genuinely rebuilds. This
   push touches build.yml, which is in no path set, so all three artifacts
   reuse and stamp nothing. Expect no values this run.

That second point is the migration path rather than a defect: under the real
mechanism "no label" reads as a MISS, so each artifact rebuilds once and is
stamped from then on — one extra build per artifact, ever, in the safe
direction. `imagetools create` cannot backfill it, since it copies a
manifest and config labels are not manifest annotations.
This commit is contained in:
2026-08-28 14:00:09 -04:00
parent d9aa5aa832
commit dee93faa37
+210
View File
@@ -487,6 +487,11 @@ jobs:
set -eu
ID=$(sh scripts/artifacts.sh identity web "$CHANNEL")
echo "identity=$ID" >> "$GITHUB_OUTPUT"
# The value step 3 will key on once the label mechanism is proven.
# Emitted now so the build below can stamp it — stamping is harmless
# and is what gives the shadow read something to find next push.
REV=$(sh scripts/artifacts.sh revision web)
echo "revision=$REV" >> "$GITHUB_OUTPUT"
# Every build that runs now claims the identity. The carve-out here
# existed only for tag pushes, which rebuilt an already-published
@@ -502,6 +507,66 @@ jobs:
echo "reuse: $IMAGE:$ID is not published — building"
fi
# --- shadow: can we read a label off the channel tag? (milestone 318 step 3)
# Informational ONLY — no `set -e`, every probe falls back to
# UNAVAILABLE, and nothing downstream reads this. It must never fail a
# build; that is the whole reason it runs before the mechanism does.
#
# The question: can the reuse check be keyed on an image LABEL read off
# the channel tag, instead of on a separate r-<rev> tag? If yes, nothing
# is minted per build, the -main/-dev identity split disappears, and the
# base-refresh clobber (#3154) stops existing.
#
# Two questions, answered at different times — worth separating, because
# reading "no value" as "the mechanism does not work" would abandon a
# working design:
#
# 1. Do the TEMPLATE MECHANICS work here? Answered on the very first
# run, by probes B and C: if they print a labels map (even an empty
# one) or a config JSON, `--format` over .Image resolves on this
# buildx and this registry. That is the actual gate for step 3.
# 2. Does a stamped value ROUND-TRIP? Only answerable once an artifact
# genuinely rebuilds. A push that changes no shipped file reuses,
# the build is skipped, and nothing gets stamped — so an artifact
# keeps reporting no label until its own source next moves.
#
# That second point is not a defect, it is the migration path: under the
# real mechanism "no label" reads as a MISS, so the artifact rebuilds
# once and is stamped from then on. One extra build per artifact, ever,
# in the safe direction. `imagetools create` cannot add the label on the
# reuse path — it copies a manifest and config labels are not manifest
# annotations — so there is no way to backfill, and no need to.
#
# Several probes on purpose: `--format` templating over .Image varies by
# buildx version, and one push should tell us which spelling works
# rather than costing a round trip per guess.
- name: Shadow — read fc.revision off the channel tag (informational)
env:
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator
CHANNEL: ${{ steps.tag.outputs.channel }}
DERIVED: ${{ steps.reuse.outputs.revision }}
run: |
set -u
# The moving tag this channel publishes. `main` is asked about
# :latest, which is what step 4 keeps; :main is on its way out.
if [ "$CHANNEL" = "main" ]; then T=latest; else T=dev; fi
REF="$IMAGE:$T"
echo "label-shadow: ref=$REF derived=$DERIVED"
A=$(docker buildx imagetools inspect "$REF" \
--format '{{ index .Image.Config.Labels "fc.revision" }}' 2>&1 || echo FAILED)
echo "label-shadow: probe-index = $A"
B=$(docker buildx imagetools inspect "$REF" \
--format '{{ .Image.Config.Labels }}' 2>&1 | head -c 300 || echo FAILED)
echo "label-shadow: probe-labels = $B"
C=$(docker buildx imagetools inspect "$REF" \
--format '{{ json .Image }}' 2>&1 | head -c 400 || echo FAILED)
echo "label-shadow: probe-json = $C"
echo "label-shadow: buildx = $(docker buildx version 2>&1 || echo UNAVAILABLE)"
- name: Download signed XPI from Forgejo release asset
# dev and main each bundle the XPI their own sign-extension just
# published — the point of the channel work (milestone 271 step 6): the
@@ -586,6 +651,11 @@ jobs:
file: Dockerfile
push: true
tags: ${{ steps.reuse.outputs.build_tags }}
# Stamped now, read later. Additive and inert this step: no
# decision depends on it until the shadow above proves the
# label can actually be read back (milestone 318 step 3).
labels: |
fc.revision=${{ steps.reuse.outputs.revision }}
# Only the web image carries a channel: it is the one that serves
# /api/extension/manifest. The ml and agent images have nothing to
# report it to.
@@ -745,6 +815,11 @@ jobs:
set -eu
ID=$(sh scripts/artifacts.sh identity ml "$CHANNEL")
echo "identity=$ID" >> "$GITHUB_OUTPUT"
# The value step 3 will key on once the label mechanism is proven.
# Emitted now so the build below can stamp it — stamping is harmless
# and is what gives the shadow read something to find next push.
REV=$(sh scripts/artifacts.sh revision ml)
echo "revision=$REV" >> "$GITHUB_OUTPUT"
# Every build that runs now claims the identity. The carve-out here
# existed only for tag pushes, which rebuilt an already-published
@@ -760,6 +835,66 @@ jobs:
echo "reuse: $IMAGE:$ID is not published — building"
fi
# --- shadow: can we read a label off the channel tag? (milestone 318 step 3)
# Informational ONLY — no `set -e`, every probe falls back to
# UNAVAILABLE, and nothing downstream reads this. It must never fail a
# build; that is the whole reason it runs before the mechanism does.
#
# The question: can the reuse check be keyed on an image LABEL read off
# the channel tag, instead of on a separate r-<rev> tag? If yes, nothing
# is minted per build, the -main/-dev identity split disappears, and the
# base-refresh clobber (#3154) stops existing.
#
# Two questions, answered at different times — worth separating, because
# reading "no value" as "the mechanism does not work" would abandon a
# working design:
#
# 1. Do the TEMPLATE MECHANICS work here? Answered on the very first
# run, by probes B and C: if they print a labels map (even an empty
# one) or a config JSON, `--format` over .Image resolves on this
# buildx and this registry. That is the actual gate for step 3.
# 2. Does a stamped value ROUND-TRIP? Only answerable once an artifact
# genuinely rebuilds. A push that changes no shipped file reuses,
# the build is skipped, and nothing gets stamped — so an artifact
# keeps reporting no label until its own source next moves.
#
# That second point is not a defect, it is the migration path: under the
# real mechanism "no label" reads as a MISS, so the artifact rebuilds
# once and is stamped from then on. One extra build per artifact, ever,
# in the safe direction. `imagetools create` cannot add the label on the
# reuse path — it copies a manifest and config labels are not manifest
# annotations — so there is no way to backfill, and no need to.
#
# Several probes on purpose: `--format` templating over .Image varies by
# buildx version, and one push should tell us which spelling works
# rather than costing a round trip per guess.
- name: Shadow — read fc.revision off the channel tag (informational)
env:
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator-ml
CHANNEL: ${{ steps.tag.outputs.channel }}
DERIVED: ${{ steps.reuse.outputs.revision }}
run: |
set -u
# The moving tag this channel publishes. `main` is asked about
# :latest, which is what step 4 keeps; :main is on its way out.
if [ "$CHANNEL" = "main" ]; then T=latest; else T=dev; fi
REF="$IMAGE:$T"
echo "label-shadow: ref=$REF derived=$DERIVED"
A=$(docker buildx imagetools inspect "$REF" \
--format '{{ index .Image.Config.Labels "fc.revision" }}' 2>&1 || echo FAILED)
echo "label-shadow: probe-index = $A"
B=$(docker buildx imagetools inspect "$REF" \
--format '{{ .Image.Config.Labels }}' 2>&1 | head -c 300 || echo FAILED)
echo "label-shadow: probe-labels = $B"
C=$(docker buildx imagetools inspect "$REF" \
--format '{{ json .Image }}' 2>&1 | head -c 400 || echo FAILED)
echo "label-shadow: probe-json = $C"
echo "label-shadow: buildx = $(docker buildx version 2>&1 || echo UNAVAILABLE)"
- name: Build and push ml image
if: steps.reuse.outputs.hit != 'true'
uses: docker/build-push-action@v5
@@ -768,6 +903,11 @@ jobs:
file: Dockerfile.ml
push: true
tags: ${{ steps.reuse.outputs.build_tags }}
# Stamped now, read later. Additive and inert this step: no
# decision depends on it until the shadow above proves the
# label can actually be read back (milestone 318 step 3).
labels: |
fc.revision=${{ steps.reuse.outputs.revision }}
# Registry-side manifest copy: no layer transfer, no local daemon, no
# rebuild. Each -t becomes another reference to the SAME manifest the
@@ -918,6 +1058,11 @@ jobs:
set -eu
ID=$(sh scripts/artifacts.sh identity agent "$CHANNEL")
echo "identity=$ID" >> "$GITHUB_OUTPUT"
# The value step 3 will key on once the label mechanism is proven.
# Emitted now so the build below can stamp it — stamping is harmless
# and is what gives the shadow read something to find next push.
REV=$(sh scripts/artifacts.sh revision agent)
echo "revision=$REV" >> "$GITHUB_OUTPUT"
# Every build that runs now claims the identity. The carve-out here
# existed only for tag pushes, which rebuilt an already-published
@@ -933,6 +1078,66 @@ jobs:
echo "reuse: $IMAGE:$ID is not published — building"
fi
# --- shadow: can we read a label off the channel tag? (milestone 318 step 3)
# Informational ONLY — no `set -e`, every probe falls back to
# UNAVAILABLE, and nothing downstream reads this. It must never fail a
# build; that is the whole reason it runs before the mechanism does.
#
# The question: can the reuse check be keyed on an image LABEL read off
# the channel tag, instead of on a separate r-<rev> tag? If yes, nothing
# is minted per build, the -main/-dev identity split disappears, and the
# base-refresh clobber (#3154) stops existing.
#
# Two questions, answered at different times — worth separating, because
# reading "no value" as "the mechanism does not work" would abandon a
# working design:
#
# 1. Do the TEMPLATE MECHANICS work here? Answered on the very first
# run, by probes B and C: if they print a labels map (even an empty
# one) or a config JSON, `--format` over .Image resolves on this
# buildx and this registry. That is the actual gate for step 3.
# 2. Does a stamped value ROUND-TRIP? Only answerable once an artifact
# genuinely rebuilds. A push that changes no shipped file reuses,
# the build is skipped, and nothing gets stamped — so an artifact
# keeps reporting no label until its own source next moves.
#
# That second point is not a defect, it is the migration path: under the
# real mechanism "no label" reads as a MISS, so the artifact rebuilds
# once and is stamped from then on. One extra build per artifact, ever,
# in the safe direction. `imagetools create` cannot add the label on the
# reuse path — it copies a manifest and config labels are not manifest
# annotations — so there is no way to backfill, and no need to.
#
# Several probes on purpose: `--format` templating over .Image varies by
# buildx version, and one push should tell us which spelling works
# rather than costing a round trip per guess.
- name: Shadow — read fc.revision off the channel tag (informational)
env:
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator-agent
CHANNEL: ${{ steps.tag.outputs.channel }}
DERIVED: ${{ steps.reuse.outputs.revision }}
run: |
set -u
# The moving tag this channel publishes. `main` is asked about
# :latest, which is what step 4 keeps; :main is on its way out.
if [ "$CHANNEL" = "main" ]; then T=latest; else T=dev; fi
REF="$IMAGE:$T"
echo "label-shadow: ref=$REF derived=$DERIVED"
A=$(docker buildx imagetools inspect "$REF" \
--format '{{ index .Image.Config.Labels "fc.revision" }}' 2>&1 || echo FAILED)
echo "label-shadow: probe-index = $A"
B=$(docker buildx imagetools inspect "$REF" \
--format '{{ .Image.Config.Labels }}' 2>&1 | head -c 300 || echo FAILED)
echo "label-shadow: probe-labels = $B"
C=$(docker buildx imagetools inspect "$REF" \
--format '{{ json .Image }}' 2>&1 | head -c 400 || echo FAILED)
echo "label-shadow: probe-json = $C"
echo "label-shadow: buildx = $(docker buildx version 2>&1 || echo UNAVAILABLE)"
- name: Build and push agent image
if: steps.reuse.outputs.hit != 'true'
uses: docker/build-push-action@v5
@@ -941,6 +1146,11 @@ jobs:
file: agent/Dockerfile
push: true
tags: ${{ steps.reuse.outputs.build_tags }}
# Stamped now, read later. Additive and inert this step: no
# decision depends on it until the shadow above proves the
# label can actually be read back (milestone 318 step 3).
labels: |
fc.revision=${{ steps.reuse.outputs.revision }}
# Registry-side manifest copy: no layer transfer, no local daemon, no
# rebuild. Each -t becomes another reference to the SAME manifest the