Milestone 318 step 8 — extension CalVer, and the #3190 mitigation's first main build #241
+153
-69
@@ -476,7 +476,6 @@ jobs:
|
||||
env:
|
||||
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator
|
||||
CHANNEL: ${{ steps.tag.outputs.channel }}
|
||||
TAGS: ${{ steps.tag.outputs.tags }}
|
||||
run: |
|
||||
set -eu
|
||||
DERIVED=$(sh scripts/artifacts.sh revision web)
|
||||
@@ -485,7 +484,6 @@ jobs:
|
||||
# A pure function of the revision — same commit, same string — so it
|
||||
# adds no variability the reuse check would have to account for.
|
||||
echo "version=$(sh scripts/artifacts.sh version web)" >> "$GITHUB_OUTPUT"
|
||||
echo "build_tags=$TAGS" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# The moving tag for this channel. Which tag we ask IS the channel —
|
||||
# that is why the revision needs no -main/-dev qualifier any more.
|
||||
@@ -608,7 +606,13 @@ jobs:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.reuse.outputs.build_tags }}
|
||||
# ONE tag, the channel's. Every other tag is written by the step
|
||||
# below, registry-side. buildx here pushes the first tag to the
|
||||
# registry and then re-pushes the rest through the DOCKER driver,
|
||||
# out of a local image store a registry-direct build never filled —
|
||||
# #3190, which cost `main` its :c-<sha> on 2026-08-29 while :latest
|
||||
# published perfectly well.
|
||||
tags: ${{ steps.reuse.outputs.channel_ref }}
|
||||
# The reuse key. Read back off the channel tag on the next push to
|
||||
# decide whether that push needs to build at all, so this is not
|
||||
# decoration — an unstamped image is one that will always rebuild.
|
||||
@@ -621,17 +625,40 @@ jobs:
|
||||
FC_CHANNEL=${{ steps.tag.outputs.channel }}
|
||||
FC_VERSION=${{ steps.reuse.outputs.version }}
|
||||
|
||||
# Registry-side manifest copy: no layer transfer, no local daemon, no
|
||||
# rebuild. Each -t becomes another reference to the SAME manifest the
|
||||
# channel tag already holds, so :c-<sha> is byte-identical to what is
|
||||
# published rather than a lookalike rebuild.
|
||||
# Every tag but the channel's own is written HERE, registry-side,
|
||||
# whether or not a build ran. Each -t becomes another reference to the
|
||||
# SAME manifest the channel tag holds, so :c-<sha> is byte-identical to
|
||||
# what is published rather than a lookalike rebuild.
|
||||
#
|
||||
# Runs on EVERY reuse, which is what keeps family rule 146 true: a
|
||||
# rolling channel refreshes itself, so skipping a build must never mean
|
||||
# leaving :dev or :latest pointing at something older than the commit
|
||||
# that was just pushed.
|
||||
- name: Repoint the tags at the published image (reuse)
|
||||
if: steps.reuse.outputs.hit == 'true'
|
||||
# Owning the build path too is #3190's fix, not a tidy-up:
|
||||
#
|
||||
# #27 pushing …/fabledcurator:latest DONE 15.8s
|
||||
# #28 pushing …/fabledcurator:c-0e15c44 with docker
|
||||
# #28 ERROR: tag does not exist: …:c-0e15c44
|
||||
#
|
||||
# Intermittent — build-ml made the identical two-tag push seconds later
|
||||
# and succeeded — and worse than it looks. `:latest` had already
|
||||
# published, so production was correct while the immutable rollback tag
|
||||
# rule 145 requires of every main push simply did not exist. Nothing but
|
||||
# the red job would ever have noticed: a missing :c-<sha> has no
|
||||
# consumer that fails, so it surfaces when somebody needs to roll back.
|
||||
#
|
||||
# `imagetools create` is a registry-side manifest copy — no layer
|
||||
# transfer, no local daemon, nothing that can be absent. The reuse case
|
||||
# has always gone this way, so this puts the build case on the code that
|
||||
# was already proven rather than on a second path.
|
||||
#
|
||||
# Running on every path also keeps family rule 146 true: a rolling
|
||||
# channel refreshes itself, so skipping a build must never leave :dev or
|
||||
# :latest pointing at something older than the commit just pushed.
|
||||
#
|
||||
# The cost, accepted knowingly: `imagetools create` wraps its source in
|
||||
# an index, so :c-<sha> becomes an index and fc.revision does not
|
||||
# resolve through it. Nothing reads that label off :c-<sha> — the reuse
|
||||
# check only ever inspects the CHANNEL tag — and the index names the
|
||||
# same manifest, so a pull is byte-identical. The reuse path already
|
||||
# produced :c-<sha> this way; this only makes it uniform.
|
||||
- name: Write the remaining tags from the published image
|
||||
env:
|
||||
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator
|
||||
SOURCE: ${{ steps.reuse.outputs.channel_ref }}
|
||||
@@ -650,15 +677,16 @@ jobs:
|
||||
# ml:dev reported fc.revision=<none> one push after run 4749 had read
|
||||
# a7e626a67a79 off it. Nothing failed; the savings just evaporated.
|
||||
#
|
||||
# Excluding the source means the channel tag is only ever written by
|
||||
# a real build, so it stays a plain image and stays readable. On dev
|
||||
# that leaves nothing to do — :dev already points at the right
|
||||
# content, which is what the hit established. On main it leaves
|
||||
# :c-<sha>, which rule 145 requires of every main push whether or not
|
||||
# a build ran.
|
||||
# Excluding the source means the channel tag is only ever written
|
||||
# by a real build, so it stays a plain image and stays readable.
|
||||
# On dev that leaves nothing to do either way: the build pushed :dev
|
||||
# itself, or the hit established it was already right. On main it
|
||||
# leaves :c-<sha>, which rule 145 requires of every main push whether
|
||||
# or not a build ran.
|
||||
#
|
||||
# steps.tag emits ONE comma-separated list, because that is the shape
|
||||
# docker/build-push-action takes; imagetools wants a -t per ref.
|
||||
# steps.tag emits ONE comma-separated list; imagetools wants a -t per
|
||||
# ref. (That list used to feed docker/build-push-action directly —
|
||||
# which is exactly what #3190 made unsafe.)
|
||||
ARGS=""
|
||||
IFS=,
|
||||
for t in $TAGS; do
|
||||
@@ -667,8 +695,8 @@ jobs:
|
||||
done
|
||||
unset IFS
|
||||
if [ -z "$ARGS" ]; then
|
||||
echo "repoint: $SOURCE already carries this revision and is the"
|
||||
echo "repoint: only tag for this channel — nothing to write."
|
||||
echo "repoint: $SOURCE is the only tag for this channel and"
|
||||
echo "repoint: already holds this revision — nothing to write."
|
||||
exit 0
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
@@ -781,12 +809,10 @@ jobs:
|
||||
env:
|
||||
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator-ml
|
||||
CHANNEL: ${{ steps.tag.outputs.channel }}
|
||||
TAGS: ${{ steps.tag.outputs.tags }}
|
||||
run: |
|
||||
set -eu
|
||||
DERIVED=$(sh scripts/artifacts.sh revision ml)
|
||||
echo "revision=$DERIVED" >> "$GITHUB_OUTPUT"
|
||||
echo "build_tags=$TAGS" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# The moving tag for this channel. Which tag we ask IS the channel —
|
||||
# that is why the revision needs no -main/-dev qualifier any more.
|
||||
@@ -833,24 +859,53 @@ jobs:
|
||||
context: .
|
||||
file: Dockerfile.ml
|
||||
push: true
|
||||
tags: ${{ steps.reuse.outputs.build_tags }}
|
||||
# ONE tag, the channel's. Every other tag is written by the step
|
||||
# below, registry-side. buildx here pushes the first tag to the
|
||||
# registry and then re-pushes the rest through the DOCKER driver,
|
||||
# out of a local image store a registry-direct build never filled —
|
||||
# #3190, which cost `main` its :c-<sha> on 2026-08-29 while :latest
|
||||
# published perfectly well.
|
||||
tags: ${{ steps.reuse.outputs.channel_ref }}
|
||||
# The reuse key. Read back off the channel tag on the next push to
|
||||
# decide whether that push needs to build at all, so this is not
|
||||
# decoration — an unstamped image is one that will always rebuild.
|
||||
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
|
||||
# channel tag already holds, so :c-<sha> is byte-identical to what is
|
||||
# published rather than a lookalike rebuild.
|
||||
# Every tag but the channel's own is written HERE, registry-side,
|
||||
# whether or not a build ran. Each -t becomes another reference to the
|
||||
# SAME manifest the channel tag holds, so :c-<sha> is byte-identical to
|
||||
# what is published rather than a lookalike rebuild.
|
||||
#
|
||||
# Runs on EVERY reuse, which is what keeps family rule 146 true: a
|
||||
# rolling channel refreshes itself, so skipping a build must never mean
|
||||
# leaving :dev or :latest pointing at something older than the commit
|
||||
# that was just pushed.
|
||||
- name: Repoint the tags at the published image (reuse)
|
||||
if: steps.reuse.outputs.hit == 'true'
|
||||
# Owning the build path too is #3190's fix, not a tidy-up:
|
||||
#
|
||||
# #27 pushing …/fabledcurator:latest DONE 15.8s
|
||||
# #28 pushing …/fabledcurator:c-0e15c44 with docker
|
||||
# #28 ERROR: tag does not exist: …:c-0e15c44
|
||||
#
|
||||
# Intermittent — build-ml made the identical two-tag push seconds later
|
||||
# and succeeded — and worse than it looks. `:latest` had already
|
||||
# published, so production was correct while the immutable rollback tag
|
||||
# rule 145 requires of every main push simply did not exist. Nothing but
|
||||
# the red job would ever have noticed: a missing :c-<sha> has no
|
||||
# consumer that fails, so it surfaces when somebody needs to roll back.
|
||||
#
|
||||
# `imagetools create` is a registry-side manifest copy — no layer
|
||||
# transfer, no local daemon, nothing that can be absent. The reuse case
|
||||
# has always gone this way, so this puts the build case on the code that
|
||||
# was already proven rather than on a second path.
|
||||
#
|
||||
# Running on every path also keeps family rule 146 true: a rolling
|
||||
# channel refreshes itself, so skipping a build must never leave :dev or
|
||||
# :latest pointing at something older than the commit just pushed.
|
||||
#
|
||||
# The cost, accepted knowingly: `imagetools create` wraps its source in
|
||||
# an index, so :c-<sha> becomes an index and fc.revision does not
|
||||
# resolve through it. Nothing reads that label off :c-<sha> — the reuse
|
||||
# check only ever inspects the CHANNEL tag — and the index names the
|
||||
# same manifest, so a pull is byte-identical. The reuse path already
|
||||
# produced :c-<sha> this way; this only makes it uniform.
|
||||
- name: Write the remaining tags from the published image
|
||||
env:
|
||||
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator-ml
|
||||
SOURCE: ${{ steps.reuse.outputs.channel_ref }}
|
||||
@@ -869,15 +924,16 @@ jobs:
|
||||
# ml:dev reported fc.revision=<none> one push after run 4749 had read
|
||||
# a7e626a67a79 off it. Nothing failed; the savings just evaporated.
|
||||
#
|
||||
# Excluding the source means the channel tag is only ever written by
|
||||
# a real build, so it stays a plain image and stays readable. On dev
|
||||
# that leaves nothing to do — :dev already points at the right
|
||||
# content, which is what the hit established. On main it leaves
|
||||
# :c-<sha>, which rule 145 requires of every main push whether or not
|
||||
# a build ran.
|
||||
# Excluding the source means the channel tag is only ever written
|
||||
# by a real build, so it stays a plain image and stays readable.
|
||||
# On dev that leaves nothing to do either way: the build pushed :dev
|
||||
# itself, or the hit established it was already right. On main it
|
||||
# leaves :c-<sha>, which rule 145 requires of every main push whether
|
||||
# or not a build ran.
|
||||
#
|
||||
# steps.tag emits ONE comma-separated list, because that is the shape
|
||||
# docker/build-push-action takes; imagetools wants a -t per ref.
|
||||
# steps.tag emits ONE comma-separated list; imagetools wants a -t per
|
||||
# ref. (That list used to feed docker/build-push-action directly —
|
||||
# which is exactly what #3190 made unsafe.)
|
||||
ARGS=""
|
||||
IFS=,
|
||||
for t in $TAGS; do
|
||||
@@ -886,8 +942,8 @@ jobs:
|
||||
done
|
||||
unset IFS
|
||||
if [ -z "$ARGS" ]; then
|
||||
echo "repoint: $SOURCE already carries this revision and is the"
|
||||
echo "repoint: only tag for this channel — nothing to write."
|
||||
echo "repoint: $SOURCE is the only tag for this channel and"
|
||||
echo "repoint: already holds this revision — nothing to write."
|
||||
exit 0
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
@@ -998,12 +1054,10 @@ jobs:
|
||||
env:
|
||||
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator-agent
|
||||
CHANNEL: ${{ steps.tag.outputs.channel }}
|
||||
TAGS: ${{ steps.tag.outputs.tags }}
|
||||
run: |
|
||||
set -eu
|
||||
DERIVED=$(sh scripts/artifacts.sh revision agent)
|
||||
echo "revision=$DERIVED" >> "$GITHUB_OUTPUT"
|
||||
echo "build_tags=$TAGS" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# The moving tag for this channel. Which tag we ask IS the channel —
|
||||
# that is why the revision needs no -main/-dev qualifier any more.
|
||||
@@ -1050,24 +1104,53 @@ jobs:
|
||||
context: agent
|
||||
file: agent/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.reuse.outputs.build_tags }}
|
||||
# ONE tag, the channel's. Every other tag is written by the step
|
||||
# below, registry-side. buildx here pushes the first tag to the
|
||||
# registry and then re-pushes the rest through the DOCKER driver,
|
||||
# out of a local image store a registry-direct build never filled —
|
||||
# #3190, which cost `main` its :c-<sha> on 2026-08-29 while :latest
|
||||
# published perfectly well.
|
||||
tags: ${{ steps.reuse.outputs.channel_ref }}
|
||||
# The reuse key. Read back off the channel tag on the next push to
|
||||
# decide whether that push needs to build at all, so this is not
|
||||
# decoration — an unstamped image is one that will always rebuild.
|
||||
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
|
||||
# channel tag already holds, so :c-<sha> is byte-identical to what is
|
||||
# published rather than a lookalike rebuild.
|
||||
# Every tag but the channel's own is written HERE, registry-side,
|
||||
# whether or not a build ran. Each -t becomes another reference to the
|
||||
# SAME manifest the channel tag holds, so :c-<sha> is byte-identical to
|
||||
# what is published rather than a lookalike rebuild.
|
||||
#
|
||||
# Runs on EVERY reuse, which is what keeps family rule 146 true: a
|
||||
# rolling channel refreshes itself, so skipping a build must never mean
|
||||
# leaving :dev or :latest pointing at something older than the commit
|
||||
# that was just pushed.
|
||||
- name: Repoint the tags at the published image (reuse)
|
||||
if: steps.reuse.outputs.hit == 'true'
|
||||
# Owning the build path too is #3190's fix, not a tidy-up:
|
||||
#
|
||||
# #27 pushing …/fabledcurator:latest DONE 15.8s
|
||||
# #28 pushing …/fabledcurator:c-0e15c44 with docker
|
||||
# #28 ERROR: tag does not exist: …:c-0e15c44
|
||||
#
|
||||
# Intermittent — build-ml made the identical two-tag push seconds later
|
||||
# and succeeded — and worse than it looks. `:latest` had already
|
||||
# published, so production was correct while the immutable rollback tag
|
||||
# rule 145 requires of every main push simply did not exist. Nothing but
|
||||
# the red job would ever have noticed: a missing :c-<sha> has no
|
||||
# consumer that fails, so it surfaces when somebody needs to roll back.
|
||||
#
|
||||
# `imagetools create` is a registry-side manifest copy — no layer
|
||||
# transfer, no local daemon, nothing that can be absent. The reuse case
|
||||
# has always gone this way, so this puts the build case on the code that
|
||||
# was already proven rather than on a second path.
|
||||
#
|
||||
# Running on every path also keeps family rule 146 true: a rolling
|
||||
# channel refreshes itself, so skipping a build must never leave :dev or
|
||||
# :latest pointing at something older than the commit just pushed.
|
||||
#
|
||||
# The cost, accepted knowingly: `imagetools create` wraps its source in
|
||||
# an index, so :c-<sha> becomes an index and fc.revision does not
|
||||
# resolve through it. Nothing reads that label off :c-<sha> — the reuse
|
||||
# check only ever inspects the CHANNEL tag — and the index names the
|
||||
# same manifest, so a pull is byte-identical. The reuse path already
|
||||
# produced :c-<sha> this way; this only makes it uniform.
|
||||
- name: Write the remaining tags from the published image
|
||||
env:
|
||||
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator-agent
|
||||
SOURCE: ${{ steps.reuse.outputs.channel_ref }}
|
||||
@@ -1086,15 +1169,16 @@ jobs:
|
||||
# ml:dev reported fc.revision=<none> one push after run 4749 had read
|
||||
# a7e626a67a79 off it. Nothing failed; the savings just evaporated.
|
||||
#
|
||||
# Excluding the source means the channel tag is only ever written by
|
||||
# a real build, so it stays a plain image and stays readable. On dev
|
||||
# that leaves nothing to do — :dev already points at the right
|
||||
# content, which is what the hit established. On main it leaves
|
||||
# :c-<sha>, which rule 145 requires of every main push whether or not
|
||||
# a build ran.
|
||||
# Excluding the source means the channel tag is only ever written
|
||||
# by a real build, so it stays a plain image and stays readable.
|
||||
# On dev that leaves nothing to do either way: the build pushed :dev
|
||||
# itself, or the hit established it was already right. On main it
|
||||
# leaves :c-<sha>, which rule 145 requires of every main push whether
|
||||
# or not a build ran.
|
||||
#
|
||||
# steps.tag emits ONE comma-separated list, because that is the shape
|
||||
# docker/build-push-action takes; imagetools wants a -t per ref.
|
||||
# steps.tag emits ONE comma-separated list; imagetools wants a -t per
|
||||
# ref. (That list used to feed docker/build-push-action directly —
|
||||
# which is exactly what #3190 made unsafe.)
|
||||
ARGS=""
|
||||
IFS=,
|
||||
for t in $TAGS; do
|
||||
@@ -1103,8 +1187,8 @@ jobs:
|
||||
done
|
||||
unset IFS
|
||||
if [ -z "$ARGS" ]; then
|
||||
echo "repoint: $SOURCE already carries this revision and is the"
|
||||
echo "repoint: only tag for this channel — nothing to write."
|
||||
echo "repoint: $SOURCE is the only tag for this channel and"
|
||||
echo "repoint: already holds this revision — nothing to write."
|
||||
exit 0
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
|
||||
@@ -108,6 +108,18 @@ per `docs/process.md`'s "add deps to the image when used by >1 project".
|
||||
source tag — `imagetools create` wraps its source in a manifest index, and
|
||||
config labels do not resolve through an index, so writing the channel tag
|
||||
from itself destroys the label the next run reads (#3183).
|
||||
- **The build pushes exactly ONE tag — the channel's — and every other tag is
|
||||
written registry-side afterwards** (#3190). buildx on this runner pushes the
|
||||
first tag to the registry and then re-pushes the rest through the docker
|
||||
driver, out of a local image store that a registry-direct build never fills;
|
||||
it fails intermittently with `tag does not exist`. On `dev` that only reddens
|
||||
a job, but on `main` it silently skips `:c-<sha>` while `:latest` publishes
|
||||
fine — a missing rollback tag has no consumer that fails, so nothing but the
|
||||
red job would notice until somebody needs to roll back. `imagetools create`
|
||||
has no local store to be absent from, and it is the code the reuse path
|
||||
already ran, so both paths now share one proven route. The cost: `:c-<sha>`
|
||||
is an index rather than a plain image, so `fc.revision` does not resolve
|
||||
through it — nothing reads it there, and the index names the same manifest.
|
||||
- **`FC_CHANNEL` and `FC_VERSION` are build args, not runtime settings.**
|
||||
`build.yml` passes them to the web image only — the ml and agent images have
|
||||
nothing to report them to. `/api/health` returns both, the foot of Settings
|
||||
|
||||
Reference in New Issue
Block a user