Stop minting identifiers nothing reads: moving channel tags, :c-<sha> on main, and a label-keyed build cache #239

Merged
bvandeusen merged 6 commits from dev into main 2026-08-28 15:14:35 -04:00
Showing only changes of commit dee93faa37 - Show all commits
+210
View File
@@ -487,6 +487,11 @@ jobs:
set -eu set -eu
ID=$(sh scripts/artifacts.sh identity web "$CHANNEL") ID=$(sh scripts/artifacts.sh identity web "$CHANNEL")
echo "identity=$ID" >> "$GITHUB_OUTPUT" 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 # Every build that runs now claims the identity. The carve-out here
# existed only for tag pushes, which rebuilt an already-published # existed only for tag pushes, which rebuilt an already-published
@@ -502,6 +507,66 @@ jobs:
echo "reuse: $IMAGE:$ID is not published — building" echo "reuse: $IMAGE:$ID is not published — building"
fi 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 - name: Download signed XPI from Forgejo release asset
# dev and main each bundle the XPI their own sign-extension just # dev and main each bundle the XPI their own sign-extension just
# published — the point of the channel work (milestone 271 step 6): the # published — the point of the channel work (milestone 271 step 6): the
@@ -586,6 +651,11 @@ jobs:
file: Dockerfile file: Dockerfile
push: true push: true
tags: ${{ steps.reuse.outputs.build_tags }} 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 # Only the web image carries a channel: it is the one that serves
# /api/extension/manifest. The ml and agent images have nothing to # /api/extension/manifest. The ml and agent images have nothing to
# report it to. # report it to.
@@ -745,6 +815,11 @@ jobs:
set -eu set -eu
ID=$(sh scripts/artifacts.sh identity ml "$CHANNEL") ID=$(sh scripts/artifacts.sh identity ml "$CHANNEL")
echo "identity=$ID" >> "$GITHUB_OUTPUT" 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 # Every build that runs now claims the identity. The carve-out here
# existed only for tag pushes, which rebuilt an already-published # existed only for tag pushes, which rebuilt an already-published
@@ -760,6 +835,66 @@ jobs:
echo "reuse: $IMAGE:$ID is not published — building" echo "reuse: $IMAGE:$ID is not published — building"
fi 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 - name: Build and push ml image
if: steps.reuse.outputs.hit != 'true' if: steps.reuse.outputs.hit != 'true'
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@@ -768,6 +903,11 @@ jobs:
file: Dockerfile.ml file: Dockerfile.ml
push: true push: true
tags: ${{ steps.reuse.outputs.build_tags }} 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 # Registry-side manifest copy: no layer transfer, no local daemon, no
# rebuild. Each -t becomes another reference to the SAME manifest the # rebuild. Each -t becomes another reference to the SAME manifest the
@@ -918,6 +1058,11 @@ jobs:
set -eu set -eu
ID=$(sh scripts/artifacts.sh identity agent "$CHANNEL") ID=$(sh scripts/artifacts.sh identity agent "$CHANNEL")
echo "identity=$ID" >> "$GITHUB_OUTPUT" 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 # Every build that runs now claims the identity. The carve-out here
# existed only for tag pushes, which rebuilt an already-published # existed only for tag pushes, which rebuilt an already-published
@@ -933,6 +1078,66 @@ jobs:
echo "reuse: $IMAGE:$ID is not published — building" echo "reuse: $IMAGE:$ID is not published — building"
fi 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 - name: Build and push agent image
if: steps.reuse.outputs.hit != 'true' if: steps.reuse.outputs.hit != 'true'
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@@ -941,6 +1146,11 @@ jobs:
file: agent/Dockerfile file: agent/Dockerfile
push: true push: true
tags: ${{ steps.reuse.outputs.build_tags }} 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 # Registry-side manifest copy: no layer transfer, no local daemon, no
# rebuild. Each -t becomes another reference to the SAME manifest the # rebuild. Each -t becomes another reference to the SAME manifest the