From cd0b0ff04a49736b721515b0956d4a698cac8b71 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 28 Aug 2026 14:45:11 -0400 Subject: [PATCH] =?UTF-8?q?ci:=20publish=20:latest=20+=20:c-=20on=20m?= =?UTF-8?q?ain,=20:dev=20on=20dev=20=E2=80=94=20nothing=20else=20(318=20st?= =?UTF-8?q?ep=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The narrowing itself. Rule 145, narrowed 2026-08-28 once it was verified that nothing pins: "a third name for the same thing is upkeep for a model we do not run." Gone: :2026.8.28 the per-artifact date tag from milestone 313 step 3. It shipped, it was verified on both branches, and its premise is gone. This is a withdrawal, not a correction — nothing about it was wrong. :main a second moving name for whatever :latest already pointed at, justified by nothing but symmetry with :dev. Kept, and note the inversion: :c- on main. Milestone 313 step 6 planned to retire it on "haven't used, not important" — amended rule 145 makes it the rollback unit and, with version tags gone, the only immutable identifier left. #3139 is cancelled as superseded rather than quietly dropped. No retention job is built. One already runs (#3157): a nightly Gitea package_cleanup_rule with keep_count=10. Two consequences worth writing down. Its keep_pattern protects any dotted-numeric tag, so the date tags this commit stops minting would have been exempt from retention and accumulated without bound — silently, in the direction of unbounded growth. That trap is now dissolved rather than needing the pattern change #3139 planned. And :c- IS prunable under that pattern, so the honest statement is that it is the rollback unit for roughly the last 10 builds, not for every main commit ever. cmd_tag goes with its last consumer rather than being left as a dead subcommand for a later step to notice. The report step logs version and revision, which is the same information — the date is the first three fields of the version. Docs needed no change: every consumer already tracks a moving tag (docker-compose.yml on :dev, agent/README.md on :latest), which is the evidence the whole stance rests on. --- .forgejo/workflows/build.yml | 147 +++++++++++------------------------ scripts/artifacts.sh | 16 +--- 2 files changed, 48 insertions(+), 115 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 764975d..0334769 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -123,10 +123,9 @@ jobs: run: | set -u A=extension - T=$(sh scripts/artifacts.sh tag "$A" 2>&1 || echo UNAVAILABLE) V=$(sh scripts/artifacts.sh version "$A" 2>&1 || echo UNAVAILABLE) R=$(sh scripts/artifacts.sh revision "$A" 2>&1 || echo UNAVAILABLE) - echo "derived: artifact=$A tag=$T version=$V revision=$R sha=$GITHUB_SHA" + echo "derived: artifact=$A version=$V revision=$R sha=$GITHUB_SHA" - name: Guard — the derived version must never go backwards env: @@ -369,57 +368,44 @@ jobs: run: | set -u A=web - T=$(sh scripts/artifacts.sh tag "$A" 2>&1 || echo UNAVAILABLE) V=$(sh scripts/artifacts.sh version "$A" 2>&1 || echo UNAVAILABLE) R=$(sh scripts/artifacts.sh revision "$A" 2>&1 || echo UNAVAILABLE) - echo "derived: artifact=$A tag=$T version=$V revision=$R sha=$GITHUB_SHA" + echo "derived: artifact=$A version=$V revision=$R sha=$GITHUB_SHA" - name: Determine tag id: tag run: | - # Two trigger shapes: - # refs/heads/main → push to main: publish :main + :latest - # (floating) AND :c- (immutable - # per-commit rollback substrate, per family - # release-posture rule "Tags are milestones, - # not gates — commit-SHA images are the - # rollback unit"). Rollback to any commit - # becomes `docker pull …:c-` without a - # release ceremony. - # refs/heads/dev → push to dev: publish :dev, the rolling test - # channel (family rule 146). Rolling means it may - # carry newer contents than the :c- of the - # same commit; it never writes :c- itself, - # because that is the rollback unit (rule 145). + # Two trigger shapes, and between them they publish three tags: + # main → :latest (production, moving — rule 147: main IS production) + # :c- (immutable, the rollback unit — rule 145) + # dev → :dev (the rolling test channel — rule 146) + # + # That is the whole list. No :, and no :main — rule 145, + # narrowed 2026-08-28 once it was verified that nothing pins: + # "a third name for the same thing is upkeep for a model we do not + # run." The date tag published between milestone 313 step 3 and + # milestone 318 was exactly that; :main was a second moving name for + # whatever :latest already pointed at. + # + # `dev` gets no :c- deliberately. On a channel whose entire + # contract is that it moves, a per-push immutable tag is a rollback + # target nobody has ever pulled, accumulating forever. The accepted + # cost: on dev there is no rollback but the previous :dev, which is + # gone — recovery is revert-on-git plus a CI cycle. + # + # Reinstating : is a real decision, not a default. It earns + # its place when something genuinely pins: a second instance held on + # a known-good build, or a deliberately frozen window. Tag at the + # moment you decide to freeze; no back-catalogue is needed. + # # POSIX-safe substring (the runner shell is dash/BusyBox sh, not # bash — `${var:0:7}` errors with "Bad substitution"; cut works - # everywhere). Operator-flagged 2026-06-01 after first :c- + # everywhere). Operator-flagged 2026-06-01 after the first :c- # main-push build failed at this step. SHORT_SHA=$(printf '%s' "$GITHUB_SHA" | cut -c1-7) - # The pinnable tag (milestone 313 step 3): YYYY.M.D of the commit - # THIS artifact's shipped files last changed in. Day precision is - # deliberate — same-day work is not something worth pinning, so a - # second main build the same day replaces the first rather than - # accumulating a tag nobody would roll back to. - # - # Derived per artifact, so an image whose files did not change keeps - # the tag it already had: the agent reads 2026.7.17 today while web - # reads 2026.8.27 — and the reuse step below turns that into a - # skipped build rather than a rebuild of bytes that already exist. - # `channel` is baked into the image as FC_CHANNEL and reported by - # /api/extension/manifest (milestone 271 step 7). + # Mirrors build-web's tag list; see the comment there. if [ "${GITHUB_REF##*/}" = "main" ]; then - CALVER=$(sh scripts/artifacts.sh tag web) - # Guarded, and computed only on this path. There is no `set -e` in - # this step, so a failed derivation would otherwise leave CALVER - # empty and publish the tag `fabledcurator:` — an invalid - # name, from a green step. An empty pin must never reach the - # registry. - if [ -z "$CALVER" ]; then - echo "ERROR: could not derive a web version tag" >&2 - exit 1 - fi - echo "tags=git.fabledsword.com/bvandeusen/fabledcurator:main,git.fabledsword.com/bvandeusen/fabledcurator:latest,git.fabledsword.com/bvandeusen/fabledcurator:c-${SHORT_SHA},git.fabledsword.com/bvandeusen/fabledcurator:${CALVER}" >> "$GITHUB_OUTPUT" + echo "tags=git.fabledsword.com/bvandeusen/fabledcurator:latest,git.fabledsword.com/bvandeusen/fabledcurator:c-${SHORT_SHA}" >> "$GITHUB_OUTPUT" echo "channel=main" >> "$GITHUB_OUTPUT" else echo "tags=git.fabledsword.com/bvandeusen/fabledcurator:dev" >> "$GITHUB_OUTPUT" @@ -646,9 +632,10 @@ jobs: for t in $TAGS; do ARGS="$ARGS -t $t"; done unset IFS # Source is the channel tag itself — the image we just confirmed - # carries this revision. One of the targets is that same tag, which - # makes its copy a no-op; the others (:main, :c-, the date pin) - # are what this exists for. + # carries this revision. On dev the only target IS that tag, so this + # is a no-op that keeps the code path uniform. On main it is what + # gives the new commit its :c-, which rule 145 requires of every + # main push whether or not a build ran. # shellcheck disable=SC2086 docker buildx imagetools create $ARGS "$SOURCE" echo "repointed to $SOURCE: $TAGS" @@ -691,44 +678,22 @@ jobs: run: | set -u A=ml - T=$(sh scripts/artifacts.sh tag "$A" 2>&1 || echo UNAVAILABLE) V=$(sh scripts/artifacts.sh version "$A" 2>&1 || echo UNAVAILABLE) R=$(sh scripts/artifacts.sh revision "$A" 2>&1 || echo UNAVAILABLE) - echo "derived: artifact=$A tag=$T version=$V revision=$R sha=$GITHUB_SHA" + echo "derived: artifact=$A version=$V revision=$R sha=$GITHUB_SHA" - name: Determine tag id: tag run: | - # Mirrors build-web's two-shape logic (main-push / dev-push), - # including the per-commit :c- tag on main — the rollback - # unit per rule 145. The -ml image follows the same cadence as web. + # Mirrors build-web's tag list; see the comment there. # POSIX-safe substring (the runner shell is dash/BusyBox sh, not # bash — `${var:0:7}` errors with "Bad substitution"; cut works # everywhere). Operator-flagged 2026-06-01 after first :c- # main-push build failed at this step. SHORT_SHA=$(printf '%s' "$GITHUB_SHA" | cut -c1-7) - # The pinnable tag (milestone 313 step 3): YYYY.M.D of the commit - # THIS artifact's shipped files last changed in. Day precision is - # deliberate — same-day work is not something worth pinning, so a - # second main build the same day replaces the first rather than - # accumulating a tag nobody would roll back to. - # - # Derived per artifact, so an image whose files did not change keeps - # the tag it already had: the agent reads 2026.7.17 today while web - # reads 2026.8.27 — and the reuse step below turns that into a - # skipped build rather than a rebuild of bytes that already exist. + # Mirrors build-web's tag list; see the comment there. if [ "${GITHUB_REF##*/}" = "main" ]; then - CALVER=$(sh scripts/artifacts.sh tag ml) - # Guarded, and computed only on this path. There is no `set -e` in - # this step, so a failed derivation would otherwise leave CALVER - # empty and publish the tag `fabledcurator-ml:` — an invalid - # name, from a green step. An empty pin must never reach the - # registry. - if [ -z "$CALVER" ]; then - echo "ERROR: could not derive a ml version tag" >&2 - exit 1 - fi - echo "tags=git.fabledsword.com/bvandeusen/fabledcurator-ml:main,git.fabledsword.com/bvandeusen/fabledcurator-ml:latest,git.fabledsword.com/bvandeusen/fabledcurator-ml:c-${SHORT_SHA},git.fabledsword.com/bvandeusen/fabledcurator-ml:${CALVER}" >> "$GITHUB_OUTPUT" + echo "tags=git.fabledsword.com/bvandeusen/fabledcurator-ml:latest,git.fabledsword.com/bvandeusen/fabledcurator-ml:c-${SHORT_SHA}" >> "$GITHUB_OUTPUT" echo "channel=main" >> "$GITHUB_OUTPUT" else echo "tags=git.fabledsword.com/bvandeusen/fabledcurator-ml:dev" >> "$GITHUB_OUTPUT" @@ -857,9 +822,10 @@ jobs: for t in $TAGS; do ARGS="$ARGS -t $t"; done unset IFS # Source is the channel tag itself — the image we just confirmed - # carries this revision. One of the targets is that same tag, which - # makes its copy a no-op; the others (:main, :c-, the date pin) - # are what this exists for. + # carries this revision. On dev the only target IS that tag, so this + # is a no-op that keeps the code path uniform. On main it is what + # gives the new commit its :c-, which rule 145 requires of every + # main push whether or not a build ran. # shellcheck disable=SC2086 docker buildx imagetools create $ARGS "$SOURCE" echo "repointed to $SOURCE: $TAGS" @@ -905,37 +871,17 @@ jobs: run: | set -u A=agent - T=$(sh scripts/artifacts.sh tag "$A" 2>&1 || echo UNAVAILABLE) V=$(sh scripts/artifacts.sh version "$A" 2>&1 || echo UNAVAILABLE) R=$(sh scripts/artifacts.sh revision "$A" 2>&1 || echo UNAVAILABLE) - echo "derived: artifact=$A tag=$T version=$V revision=$R sha=$GITHUB_SHA" + echo "derived: artifact=$A version=$V revision=$R sha=$GITHUB_SHA" - name: Determine tag id: tag run: | SHORT_SHA=$(printf '%s' "$GITHUB_SHA" | cut -c1-7) - # The pinnable tag (milestone 313 step 3): YYYY.M.D of the commit - # THIS artifact's shipped files last changed in. Day precision is - # deliberate — same-day work is not something worth pinning, so a - # second main build the same day replaces the first rather than - # accumulating a tag nobody would roll back to. - # - # Derived per artifact, so an image whose files did not change keeps - # the tag it already had: the agent reads 2026.7.17 today while web - # reads 2026.8.27 — and the reuse step below turns that into a - # skipped build rather than a rebuild of bytes that already exist. + # Mirrors build-web's tag list; see the comment there. if [ "${GITHUB_REF##*/}" = "main" ]; then - CALVER=$(sh scripts/artifacts.sh tag agent) - # Guarded, and computed only on this path. There is no `set -e` in - # this step, so a failed derivation would otherwise leave CALVER - # empty and publish the tag `fabledcurator-agent:` — an invalid - # name, from a green step. An empty pin must never reach the - # registry. - if [ -z "$CALVER" ]; then - echo "ERROR: could not derive a agent version tag" >&2 - exit 1 - fi - echo "tags=git.fabledsword.com/bvandeusen/fabledcurator-agent:main,git.fabledsword.com/bvandeusen/fabledcurator-agent:latest,git.fabledsword.com/bvandeusen/fabledcurator-agent:c-${SHORT_SHA},git.fabledsword.com/bvandeusen/fabledcurator-agent:${CALVER}" >> "$GITHUB_OUTPUT" + echo "tags=git.fabledsword.com/bvandeusen/fabledcurator-agent:latest,git.fabledsword.com/bvandeusen/fabledcurator-agent:c-${SHORT_SHA}" >> "$GITHUB_OUTPUT" echo "channel=main" >> "$GITHUB_OUTPUT" else echo "tags=git.fabledsword.com/bvandeusen/fabledcurator-agent:dev" >> "$GITHUB_OUTPUT" @@ -1064,9 +1010,10 @@ jobs: for t in $TAGS; do ARGS="$ARGS -t $t"; done unset IFS # Source is the channel tag itself — the image we just confirmed - # carries this revision. One of the targets is that same tag, which - # makes its copy a no-op; the others (:main, :c-, the date pin) - # are what this exists for. + # carries this revision. On dev the only target IS that tag, so this + # is a no-op that keeps the code path uniform. On main it is what + # gives the new commit its :c-, which rule 145 requires of every + # main push whether or not a build ran. # shellcheck disable=SC2086 docker buildx imagetools create $ARGS "$SOURCE" echo "repointed to $SOURCE: $TAGS" diff --git a/scripts/artifacts.sh b/scripts/artifacts.sh index 7fff628..0b19cb2 100755 --- a/scripts/artifacts.sh +++ b/scripts/artifacts.sh @@ -58,7 +58,7 @@ AGENT_PATHS='agent/Dockerfile agent/requirements.txt agent/fc_agent' usage() { - echo "usage: artifacts.sh {paths|revision|version|tag} {web|ml|agent|extension}" >&2 + echo "usage: artifacts.sh {paths|revision|version} {web|ml|agent|extension}" >&2 exit 2 } @@ -137,24 +137,10 @@ cmd_version() { "$(strip0 "$(fmt "$sha" %H%M)")" } -# The PUBLISHED IMAGE TAG: day precision, YYYY.M.D. Deliberately coarser than -# the ordering key, per the operator 2026-08-28 — same-day work is not -# something worth pinning, so a second build the same day replaces the first -# rather than accumulating a tag nobody would roll back to. Safe only because -# skip decisions key on cmd_revision, never on this. -cmd_tag() { - sha=$(echo "$(newest "$1")" | cut -d' ' -f2) - printf '%s.%s.%s\n' \ - "$(fmt "$sha" %Y)" \ - "$(strip0 "$(fmt "$sha" %m)")" \ - "$(strip0 "$(fmt "$sha" %d)")" -} - [ $# -ge 2 ] || usage case "$1" in paths) cmd_paths "$2" ;; revision) cmd_revision "$2" ;; version) cmd_version "$2" ;; - tag) cmd_tag "$2" ;; *) usage ;; esac