|
|
|
@@ -122,7 +122,7 @@ jobs:
|
|
|
|
|
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 "shadow: artifact=$A tag=$T version=$V revision=$R sha=$GITHUB_SHA"
|
|
|
|
|
echo "derived: artifact=$A tag=$T version=$V revision=$R sha=$GITHUB_SHA"
|
|
|
|
|
|
|
|
|
|
- name: Guard — the derived version must never go backwards
|
|
|
|
|
env:
|
|
|
|
@@ -341,11 +341,19 @@ jobs:
|
|
|
|
|
# that exists perfectly well under its real name.
|
|
|
|
|
fetch-depth: 0
|
|
|
|
|
|
|
|
|
|
# --- shadow mode (milestone 313, step 2) -----------------------------
|
|
|
|
|
# Informational ONLY. Nothing reads this and it must never fail the
|
|
|
|
|
# build — no `set -e`, and every derivation falls back to UNAVAILABLE.
|
|
|
|
|
# --- derived values, one line (milestone 313) ------------------------
|
|
|
|
|
# These stopped being shadow output at step 3: `tag` is published on
|
|
|
|
|
# main and `revision` decides whether the build below runs at all. This
|
|
|
|
|
# step prints all three anyway, because the load-bearing steps each
|
|
|
|
|
# print only the one they use, and on dev the date tag is computed
|
|
|
|
|
# nowhere else. When a build is skipped or a pin looks wrong, this is
|
|
|
|
|
# the line that says what the commit derived.
|
|
|
|
|
#
|
|
|
|
|
# What to watch across pushes, because this is what step 3 will trust:
|
|
|
|
|
# Still diagnostic, so it still must not fail the build — no `set -e`,
|
|
|
|
|
# and every derivation falls back to UNAVAILABLE. A broken echo must
|
|
|
|
|
# never be the reason an image does not ship.
|
|
|
|
|
#
|
|
|
|
|
# What it should say:
|
|
|
|
|
# * a push touching only agent/ moves the agent and leaves web and ml
|
|
|
|
|
# STILL. If web moves, its path set is too wide.
|
|
|
|
|
# * a push touching only docs moves nothing.
|
|
|
|
@@ -353,14 +361,162 @@ jobs:
|
|
|
|
|
# web bakes in the XPI. If web does not move, its set is too narrow
|
|
|
|
|
# — the direction that serves stale bytes on a pin.
|
|
|
|
|
# * dev and main derive the same values for the same source.
|
|
|
|
|
- name: Shadow — derived artifact version (informational)
|
|
|
|
|
- name: Report the derived artifact version
|
|
|
|
|
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 "shadow: artifact=$A tag=$T version=$V revision=$R sha=$GITHUB_SHA"
|
|
|
|
|
echo "derived: artifact=$A tag=$T version=$V revision=$R sha=$GITHUB_SHA"
|
|
|
|
|
|
|
|
|
|
- name: Determine tag
|
|
|
|
|
id: tag
|
|
|
|
|
run: |
|
|
|
|
|
# Three trigger shapes:
|
|
|
|
|
# refs/tags/v… → tag-push: opt-in milestone label (vYY.MM.DD,
|
|
|
|
|
# plus `.N` when the day already carries a tag —
|
|
|
|
|
# family rule 148, amended 2026-08-24 after a
|
|
|
|
|
# same-day tag was retargeted and a release
|
|
|
|
|
# deleted to make room, note 2813).
|
|
|
|
|
# Publish ONLY the immutable version tag;
|
|
|
|
|
# don't touch :latest (the main-push build
|
|
|
|
|
# for the merge commit already did that).
|
|
|
|
|
# refs/heads/main → push to main: publish :main + :latest
|
|
|
|
|
# (floating) AND :c-<short_sha> (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-<sha>` 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-<sha> of the
|
|
|
|
|
# same commit; it never writes :c-<sha> itself,
|
|
|
|
|
# because that is the rollback unit (rule 145).
|
|
|
|
|
# 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-<sha>
|
|
|
|
|
# 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). A tag-push counts as
|
|
|
|
|
# `main`: a vYY.MM.DD tag is cut from main, so that image is a
|
|
|
|
|
# main-channel artifact wearing an immutable name.
|
|
|
|
|
if [ "${GITHUB_REF#refs/tags/}" != "${GITHUB_REF}" ]; then
|
|
|
|
|
TAG_NAME="${GITHUB_REF#refs/tags/}"
|
|
|
|
|
echo "tags=git.fabledsword.com/bvandeusen/fabledcurator:${TAG_NAME}" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "channel=main" >> "$GITHUB_OUTPUT"
|
|
|
|
|
elif [ "${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 "channel=main" >> "$GITHUB_OUTPUT"
|
|
|
|
|
else
|
|
|
|
|
echo "tags=git.fabledsword.com/bvandeusen/fabledcurator:dev" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "channel=dev" >> "$GITHUB_OUTPUT"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# A shell step, not docker/login-action@v3, because the action's shared
|
|
|
|
|
# cache races itself (#3118). act_runner caches a remote action under one
|
|
|
|
|
# /root/.cache/act/<hash> per runner, and build-web, build-ml and
|
|
|
|
|
# build-agent all start in the same second and all want this same action.
|
|
|
|
|
# One job re-clones the directory — which empties and repopulates it —
|
|
|
|
|
# while another is walking it to copy into its container, and the walker
|
|
|
|
|
# lstat()s a file that has just vanished. It failed twice on 2026-08-27,
|
|
|
|
|
# naming a DIFFERENT missing file each time (`eslint.config.mjs`, then
|
|
|
|
|
# `jest.config.ts`), which is what rules out a corrupt cache and points at
|
|
|
|
|
# a race. The loser dies with MODULE_NOT_FOUND on dist/index.js before the
|
|
|
|
|
# action runs at all, so the secret is never even reached.
|
|
|
|
|
#
|
|
|
|
|
# Nothing is lost by dropping it: logging in is one command, the docker
|
|
|
|
|
# CLI is already in the CI image (ci-requirements.md), and the same
|
|
|
|
|
# reasoning as family rule 5 applies — a marketplace action buys nothing
|
|
|
|
|
# when the tool is baked into the image the workflow already selected.
|
|
|
|
|
#
|
|
|
|
|
# Password on stdin, never as an argument: an argument lands in the
|
|
|
|
|
# process table and draws docker's own deprecation warning.
|
|
|
|
|
- name: Login to Forgejo registry
|
|
|
|
|
env:
|
|
|
|
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
|
|
|
ACTOR: ${{ github.actor }}
|
|
|
|
|
run: echo "$TOKEN" | docker login git.fabledsword.com -u "$ACTOR" --password-stdin
|
|
|
|
|
|
|
|
|
|
# --- reuse-if-published (milestone 313, step 4) ----------------------
|
|
|
|
|
# The identity tag names this artifact's CONTENT — r-<revision>, the
|
|
|
|
|
# commit its shipped files last changed in, plus the channel for images
|
|
|
|
|
# that bake one in. If the registry already carries it, the bytes this
|
|
|
|
|
# job would produce are already published and the build is pure waste:
|
|
|
|
|
# the channel and date tags get repointed at the existing manifest
|
|
|
|
|
# instead, registry-side, in seconds.
|
|
|
|
|
#
|
|
|
|
|
# This is what stops a push that touched only `agent/` from rebuilding
|
|
|
|
|
# web and ml, and a merge to main from rebuilding what dev already built.
|
|
|
|
|
#
|
|
|
|
|
# The failure direction is deliberate. An inspect that errors for ANY
|
|
|
|
|
# reason — network, auth, a registry hiccup — reads as a miss and the
|
|
|
|
|
# build runs. Only a genuine 200 skips one, so there is no path here
|
|
|
|
|
# that skips a build that was actually needed; the worst case is paying
|
|
|
|
|
# for a build we could have avoided.
|
|
|
|
|
#
|
|
|
|
|
# BASE-IMAGE FRESHNESS, decided rather than left implicit: an artifact
|
|
|
|
|
# whose source stops moving stops picking up base-image updates under
|
|
|
|
|
# its pinned tag. That is what a pin MEANS — a date tag has to keep
|
|
|
|
|
# serving the bytes it served (fabledcurator:2026.7.17 still
|
|
|
|
|
# resolves to July's image), or it is not a pin — and family rule
|
|
|
|
|
# 145 already says where the refresh goes instead: a rebuild with
|
|
|
|
|
# different contents publishes only the MOVING tag, never the immutable
|
|
|
|
|
# one. A scheduled channel-only refresh is tracked separately (#3154);
|
|
|
|
|
# it does not belong in the push path.
|
|
|
|
|
- name: Is this content already published?
|
|
|
|
|
id: reuse
|
|
|
|
|
env:
|
|
|
|
|
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator
|
|
|
|
|
CHANNEL: ${{ steps.tag.outputs.channel }}
|
|
|
|
|
TAGS: ${{ steps.tag.outputs.tags }}
|
|
|
|
|
IS_TAG_PUSH: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
|
|
|
run: |
|
|
|
|
|
set -eu
|
|
|
|
|
ID=$(sh scripts/artifacts.sh identity web "$CHANNEL")
|
|
|
|
|
echo "identity=$ID" >> "$GITHUB_OUTPUT"
|
|
|
|
|
|
|
|
|
|
# A tag-push builds a revision that main already published, so it
|
|
|
|
|
# must NOT claim the identity: image configs are not bit-reproducible
|
|
|
|
|
# (embedded timestamps), so re-pushing r-<rev> would point an
|
|
|
|
|
# immutable tag at fresh bytes — rule 145's exact prohibition. It
|
|
|
|
|
# publishes only its own :v… label and otherwise reuses.
|
|
|
|
|
if [ "$IS_TAG_PUSH" = "true" ]; then
|
|
|
|
|
echo "build_tags=$TAGS" >> "$GITHUB_OUTPUT"
|
|
|
|
|
else
|
|
|
|
|
echo "build_tags=$TAGS,$IMAGE:$ID" >> "$GITHUB_OUTPUT"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if docker buildx imagetools inspect "$IMAGE:$ID" >/dev/null 2>&1; then
|
|
|
|
|
echo "hit=true" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "reuse: $IMAGE:$ID is already published — skipping the build"
|
|
|
|
|
else
|
|
|
|
|
echo "hit=false" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "reuse: $IMAGE:$ID is not published — building"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
- name: Download signed XPI from Forgejo release asset
|
|
|
|
|
# Fires on every trigger shape. dev and main each bundle the XPI their
|
|
|
|
@@ -381,7 +537,10 @@ jobs:
|
|
|
|
|
# for up to 10min total) before giving up. Main-push's signing
|
|
|
|
|
# eventually wins and tag-push picks the release up on a later
|
|
|
|
|
# iteration.
|
|
|
|
|
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')
|
|
|
|
|
# Gated on the reuse miss as well: if the image is already published it
|
|
|
|
|
# already contains its XPI, so this would download (and on a tag-push,
|
|
|
|
|
# poll up to 10 minutes for) a file nothing then reads.
|
|
|
|
|
if: steps.reuse.outputs.hit != 'true' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/'))
|
|
|
|
|
env:
|
|
|
|
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
|
|
|
run: |
|
|
|
|
@@ -441,109 +600,47 @@ jobs:
|
|
|
|
|
cp "$DEST" "frontend/public/extension/fabledcurator-latest.xpi"
|
|
|
|
|
ls -la frontend/public/extension/
|
|
|
|
|
|
|
|
|
|
- name: Determine tag
|
|
|
|
|
id: tag
|
|
|
|
|
run: |
|
|
|
|
|
# Three trigger shapes:
|
|
|
|
|
# refs/tags/v… → tag-push: opt-in milestone label (vYY.MM.DD,
|
|
|
|
|
# plus `.N` when the day already carries a tag —
|
|
|
|
|
# family rule 148, amended 2026-08-24 after a
|
|
|
|
|
# same-day tag was retargeted and a release
|
|
|
|
|
# deleted to make room, note 2813).
|
|
|
|
|
# Publish ONLY the immutable version tag;
|
|
|
|
|
# don't touch :latest (the main-push build
|
|
|
|
|
# for the merge commit already did that).
|
|
|
|
|
# refs/heads/main → push to main: publish :main + :latest
|
|
|
|
|
# (floating) AND :c-<short_sha> (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-<sha>` 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-<sha> of the
|
|
|
|
|
# same commit; it never writes :c-<sha> itself,
|
|
|
|
|
# because that is the rollback unit (rule 145).
|
|
|
|
|
# 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-<sha>
|
|
|
|
|
# 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. Step 4 uses that to stop rebuilding it at all.
|
|
|
|
|
# `channel` is baked into the image as FC_CHANNEL and reported by
|
|
|
|
|
# /api/extension/manifest (milestone 271 step 7). A tag-push counts as
|
|
|
|
|
# `main`: a vYY.MM.DD tag is cut from main, so that image is a
|
|
|
|
|
# main-channel artifact wearing an immutable name.
|
|
|
|
|
if [ "${GITHUB_REF#refs/tags/}" != "${GITHUB_REF}" ]; then
|
|
|
|
|
TAG_NAME="${GITHUB_REF#refs/tags/}"
|
|
|
|
|
echo "tags=git.fabledsword.com/bvandeusen/fabledcurator:${TAG_NAME}" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "channel=main" >> "$GITHUB_OUTPUT"
|
|
|
|
|
elif [ "${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 "channel=main" >> "$GITHUB_OUTPUT"
|
|
|
|
|
else
|
|
|
|
|
echo "tags=git.fabledsword.com/bvandeusen/fabledcurator:dev" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "channel=dev" >> "$GITHUB_OUTPUT"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# A shell step, not docker/login-action@v3, because the action's shared
|
|
|
|
|
# cache races itself (#3118). act_runner caches a remote action under one
|
|
|
|
|
# /root/.cache/act/<hash> per runner, and build-web, build-ml and
|
|
|
|
|
# build-agent all start in the same second and all want this same action.
|
|
|
|
|
# One job re-clones the directory — which empties and repopulates it —
|
|
|
|
|
# while another is walking it to copy into its container, and the walker
|
|
|
|
|
# lstat()s a file that has just vanished. It failed twice on 2026-08-27,
|
|
|
|
|
# naming a DIFFERENT missing file each time (`eslint.config.mjs`, then
|
|
|
|
|
# `jest.config.ts`), which is what rules out a corrupt cache and points at
|
|
|
|
|
# a race. The loser dies with MODULE_NOT_FOUND on dist/index.js before the
|
|
|
|
|
# action runs at all, so the secret is never even reached.
|
|
|
|
|
#
|
|
|
|
|
# Nothing is lost by dropping it: logging in is one command, the docker
|
|
|
|
|
# CLI is already in the CI image (ci-requirements.md), and the same
|
|
|
|
|
# reasoning as family rule 5 applies — a marketplace action buys nothing
|
|
|
|
|
# when the tool is baked into the image the workflow already selected.
|
|
|
|
|
#
|
|
|
|
|
# Password on stdin, never as an argument: an argument lands in the
|
|
|
|
|
# process table and draws docker's own deprecation warning.
|
|
|
|
|
- name: Login to Forgejo registry
|
|
|
|
|
env:
|
|
|
|
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
|
|
|
ACTOR: ${{ github.actor }}
|
|
|
|
|
run: echo "$TOKEN" | docker login git.fabledsword.com -u "$ACTOR" --password-stdin
|
|
|
|
|
|
|
|
|
|
- name: Build and push web image
|
|
|
|
|
if: steps.reuse.outputs.hit != 'true'
|
|
|
|
|
uses: docker/build-push-action@v5
|
|
|
|
|
with:
|
|
|
|
|
context: .
|
|
|
|
|
file: Dockerfile
|
|
|
|
|
push: true
|
|
|
|
|
tags: ${{ steps.tag.outputs.tags }}
|
|
|
|
|
tags: ${{ steps.reuse.outputs.build_tags }}
|
|
|
|
|
# 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.
|
|
|
|
|
build-args: |
|
|
|
|
|
FC_CHANNEL=${{ steps.tag.outputs.channel }}
|
|
|
|
|
|
|
|
|
|
# Registry-side manifest copy: no layer transfer, no local daemon, no
|
|
|
|
|
# rebuild. Each -t becomes another reference to the SAME manifest the
|
|
|
|
|
# identity tag holds, so :latest and the date pin are byte-identical to
|
|
|
|
|
# what was 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'
|
|
|
|
|
env:
|
|
|
|
|
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator
|
|
|
|
|
IDENTITY: ${{ steps.reuse.outputs.identity }}
|
|
|
|
|
TAGS: ${{ steps.tag.outputs.tags }}
|
|
|
|
|
run: |
|
|
|
|
|
set -euf
|
|
|
|
|
# steps.tag emits ONE comma-separated list, because that is the shape
|
|
|
|
|
# docker/build-push-action takes; imagetools wants a -t per ref.
|
|
|
|
|
ARGS=""
|
|
|
|
|
IFS=,
|
|
|
|
|
for t in $TAGS; do ARGS="$ARGS -t $t"; done
|
|
|
|
|
unset IFS
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
docker buildx imagetools create $ARGS "$IMAGE:$IDENTITY"
|
|
|
|
|
echo "repointed to $IMAGE:$IDENTITY: $TAGS"
|
|
|
|
|
|
|
|
|
|
build-ml:
|
|
|
|
|
runs-on: python-ci
|
|
|
|
|
container:
|
|
|
|
@@ -558,11 +655,19 @@ jobs:
|
|
|
|
|
# the build would otherwise notice.
|
|
|
|
|
fetch-depth: 0
|
|
|
|
|
|
|
|
|
|
# --- shadow mode (milestone 313, step 2) -----------------------------
|
|
|
|
|
# Informational ONLY. Nothing reads this and it must never fail the
|
|
|
|
|
# build — no `set -e`, and every derivation falls back to UNAVAILABLE.
|
|
|
|
|
# --- derived values, one line (milestone 313) ------------------------
|
|
|
|
|
# These stopped being shadow output at step 3: `tag` is published on
|
|
|
|
|
# main and `revision` decides whether the build below runs at all. This
|
|
|
|
|
# step prints all three anyway, because the load-bearing steps each
|
|
|
|
|
# print only the one they use, and on dev the date tag is computed
|
|
|
|
|
# nowhere else. When a build is skipped or a pin looks wrong, this is
|
|
|
|
|
# the line that says what the commit derived.
|
|
|
|
|
#
|
|
|
|
|
# What to watch across pushes, because this is what step 3 will trust:
|
|
|
|
|
# Still diagnostic, so it still must not fail the build — no `set -e`,
|
|
|
|
|
# and every derivation falls back to UNAVAILABLE. A broken echo must
|
|
|
|
|
# never be the reason an image does not ship.
|
|
|
|
|
#
|
|
|
|
|
# What it should say:
|
|
|
|
|
# * a push touching only agent/ moves the agent and leaves web and ml
|
|
|
|
|
# STILL. If web moves, its path set is too wide.
|
|
|
|
|
# * a push touching only docs moves nothing.
|
|
|
|
@@ -570,14 +675,14 @@ jobs:
|
|
|
|
|
# web bakes in the XPI. If web does not move, its set is too narrow
|
|
|
|
|
# — the direction that serves stale bytes on a pin.
|
|
|
|
|
# * dev and main derive the same values for the same source.
|
|
|
|
|
- name: Shadow — derived artifact version (informational)
|
|
|
|
|
- name: Report the derived artifact version
|
|
|
|
|
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 "shadow: artifact=$A tag=$T version=$V revision=$R sha=$GITHUB_SHA"
|
|
|
|
|
echo "derived: artifact=$A tag=$T version=$V revision=$R sha=$GITHUB_SHA"
|
|
|
|
|
|
|
|
|
|
- name: Determine tag
|
|
|
|
|
id: tag
|
|
|
|
@@ -599,10 +704,12 @@ jobs:
|
|
|
|
|
#
|
|
|
|
|
# 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. Step 4 uses that to stop rebuilding it at all.
|
|
|
|
|
# reads 2026.8.27 — and the reuse step below turns that into a
|
|
|
|
|
# skipped build rather than a rebuild of bytes that already exist.
|
|
|
|
|
if [ "${GITHUB_REF#refs/tags/}" != "${GITHUB_REF}" ]; then
|
|
|
|
|
TAG_NAME="${GITHUB_REF#refs/tags/}"
|
|
|
|
|
echo "tags=git.fabledsword.com/bvandeusen/fabledcurator-ml:${TAG_NAME}" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "channel=main" >> "$GITHUB_OUTPUT"
|
|
|
|
|
elif [ "${GITHUB_REF##*/}" = "main" ]; then
|
|
|
|
|
CALVER=$(sh scripts/artifacts.sh tag ml)
|
|
|
|
|
# Guarded, and computed only on this path. There is no `set -e` in
|
|
|
|
@@ -615,8 +722,10 @@ jobs:
|
|
|
|
|
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 "channel=main" >> "$GITHUB_OUTPUT"
|
|
|
|
|
else
|
|
|
|
|
echo "tags=git.fabledsword.com/bvandeusen/fabledcurator-ml:dev" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "channel=dev" >> "$GITHUB_OUTPUT"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Shell step rather than docker/login-action — see build-web's note on
|
|
|
|
@@ -627,13 +736,98 @@ jobs:
|
|
|
|
|
ACTOR: ${{ github.actor }}
|
|
|
|
|
run: echo "$TOKEN" | docker login git.fabledsword.com -u "$ACTOR" --password-stdin
|
|
|
|
|
|
|
|
|
|
# --- reuse-if-published (milestone 313, step 4) ----------------------
|
|
|
|
|
# The identity tag names this artifact's CONTENT — r-<revision>, the
|
|
|
|
|
# commit its shipped files last changed in, plus the channel for images
|
|
|
|
|
# that bake one in. If the registry already carries it, the bytes this
|
|
|
|
|
# job would produce are already published and the build is pure waste:
|
|
|
|
|
# the channel and date tags get repointed at the existing manifest
|
|
|
|
|
# instead, registry-side, in seconds.
|
|
|
|
|
#
|
|
|
|
|
# This is what stops a push that touched only `agent/` from rebuilding
|
|
|
|
|
# web and ml, and a merge to main from rebuilding what dev already built.
|
|
|
|
|
#
|
|
|
|
|
# The failure direction is deliberate. An inspect that errors for ANY
|
|
|
|
|
# reason — network, auth, a registry hiccup — reads as a miss and the
|
|
|
|
|
# build runs. Only a genuine 200 skips one, so there is no path here
|
|
|
|
|
# that skips a build that was actually needed; the worst case is paying
|
|
|
|
|
# for a build we could have avoided.
|
|
|
|
|
#
|
|
|
|
|
# BASE-IMAGE FRESHNESS, decided rather than left implicit: an artifact
|
|
|
|
|
# whose source stops moving stops picking up base-image updates under
|
|
|
|
|
# its pinned tag. That is what a pin MEANS — a date tag has to keep
|
|
|
|
|
# serving the bytes it served (fabledcurator-ml:2026.7.17 still
|
|
|
|
|
# resolves to July's image), or it is not a pin — and family rule
|
|
|
|
|
# 145 already says where the refresh goes instead: a rebuild with
|
|
|
|
|
# different contents publishes only the MOVING tag, never the immutable
|
|
|
|
|
# one. A scheduled channel-only refresh is tracked separately (#3154);
|
|
|
|
|
# it does not belong in the push path.
|
|
|
|
|
- name: Is this content already published?
|
|
|
|
|
id: reuse
|
|
|
|
|
env:
|
|
|
|
|
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator-ml
|
|
|
|
|
CHANNEL: ${{ steps.tag.outputs.channel }}
|
|
|
|
|
TAGS: ${{ steps.tag.outputs.tags }}
|
|
|
|
|
IS_TAG_PUSH: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
|
|
|
run: |
|
|
|
|
|
set -eu
|
|
|
|
|
ID=$(sh scripts/artifacts.sh identity ml "$CHANNEL")
|
|
|
|
|
echo "identity=$ID" >> "$GITHUB_OUTPUT"
|
|
|
|
|
|
|
|
|
|
# A tag-push builds a revision that main already published, so it
|
|
|
|
|
# must NOT claim the identity: image configs are not bit-reproducible
|
|
|
|
|
# (embedded timestamps), so re-pushing r-<rev> would point an
|
|
|
|
|
# immutable tag at fresh bytes — rule 145's exact prohibition. It
|
|
|
|
|
# publishes only its own :v… label and otherwise reuses.
|
|
|
|
|
if [ "$IS_TAG_PUSH" = "true" ]; then
|
|
|
|
|
echo "build_tags=$TAGS" >> "$GITHUB_OUTPUT"
|
|
|
|
|
else
|
|
|
|
|
echo "build_tags=$TAGS,$IMAGE:$ID" >> "$GITHUB_OUTPUT"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if docker buildx imagetools inspect "$IMAGE:$ID" >/dev/null 2>&1; then
|
|
|
|
|
echo "hit=true" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "reuse: $IMAGE:$ID is already published — skipping the build"
|
|
|
|
|
else
|
|
|
|
|
echo "hit=false" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "reuse: $IMAGE:$ID is not published — building"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
- name: Build and push ml image
|
|
|
|
|
if: steps.reuse.outputs.hit != 'true'
|
|
|
|
|
uses: docker/build-push-action@v5
|
|
|
|
|
with:
|
|
|
|
|
context: .
|
|
|
|
|
file: Dockerfile.ml
|
|
|
|
|
push: true
|
|
|
|
|
tags: ${{ steps.tag.outputs.tags }}
|
|
|
|
|
tags: ${{ steps.reuse.outputs.build_tags }}
|
|
|
|
|
|
|
|
|
|
# Registry-side manifest copy: no layer transfer, no local daemon, no
|
|
|
|
|
# rebuild. Each -t becomes another reference to the SAME manifest the
|
|
|
|
|
# identity tag holds, so :latest and the date pin are byte-identical to
|
|
|
|
|
# what was 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'
|
|
|
|
|
env:
|
|
|
|
|
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator-ml
|
|
|
|
|
IDENTITY: ${{ steps.reuse.outputs.identity }}
|
|
|
|
|
TAGS: ${{ steps.tag.outputs.tags }}
|
|
|
|
|
run: |
|
|
|
|
|
set -euf
|
|
|
|
|
# steps.tag emits ONE comma-separated list, because that is the shape
|
|
|
|
|
# docker/build-push-action takes; imagetools wants a -t per ref.
|
|
|
|
|
ARGS=""
|
|
|
|
|
IFS=,
|
|
|
|
|
for t in $TAGS; do ARGS="$ARGS -t $t"; done
|
|
|
|
|
unset IFS
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
docker buildx imagetools create $ARGS "$IMAGE:$IDENTITY"
|
|
|
|
|
echo "repointed to $IMAGE:$IDENTITY: $TAGS"
|
|
|
|
|
|
|
|
|
|
# The desktop GPU agent (#114) — published so the operator pulls + runs it on
|
|
|
|
|
# the GPU machine instead of building locally. Independent of web/ml (its own
|
|
|
|
@@ -652,11 +846,19 @@ jobs:
|
|
|
|
|
# the build would otherwise notice.
|
|
|
|
|
fetch-depth: 0
|
|
|
|
|
|
|
|
|
|
# --- shadow mode (milestone 313, step 2) -----------------------------
|
|
|
|
|
# Informational ONLY. Nothing reads this and it must never fail the
|
|
|
|
|
# build — no `set -e`, and every derivation falls back to UNAVAILABLE.
|
|
|
|
|
# --- derived values, one line (milestone 313) ------------------------
|
|
|
|
|
# These stopped being shadow output at step 3: `tag` is published on
|
|
|
|
|
# main and `revision` decides whether the build below runs at all. This
|
|
|
|
|
# step prints all three anyway, because the load-bearing steps each
|
|
|
|
|
# print only the one they use, and on dev the date tag is computed
|
|
|
|
|
# nowhere else. When a build is skipped or a pin looks wrong, this is
|
|
|
|
|
# the line that says what the commit derived.
|
|
|
|
|
#
|
|
|
|
|
# What to watch across pushes, because this is what step 3 will trust:
|
|
|
|
|
# Still diagnostic, so it still must not fail the build — no `set -e`,
|
|
|
|
|
# and every derivation falls back to UNAVAILABLE. A broken echo must
|
|
|
|
|
# never be the reason an image does not ship.
|
|
|
|
|
#
|
|
|
|
|
# What it should say:
|
|
|
|
|
# * a push touching only agent/ moves the agent and leaves web and ml
|
|
|
|
|
# STILL. If web moves, its path set is too wide.
|
|
|
|
|
# * a push touching only docs moves nothing.
|
|
|
|
@@ -664,14 +866,14 @@ jobs:
|
|
|
|
|
# web bakes in the XPI. If web does not move, its set is too narrow
|
|
|
|
|
# — the direction that serves stale bytes on a pin.
|
|
|
|
|
# * dev and main derive the same values for the same source.
|
|
|
|
|
- name: Shadow — derived artifact version (informational)
|
|
|
|
|
- name: Report the derived artifact version
|
|
|
|
|
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 "shadow: artifact=$A tag=$T version=$V revision=$R sha=$GITHUB_SHA"
|
|
|
|
|
echo "derived: artifact=$A tag=$T version=$V revision=$R sha=$GITHUB_SHA"
|
|
|
|
|
|
|
|
|
|
- name: Determine tag
|
|
|
|
|
id: tag
|
|
|
|
@@ -685,10 +887,12 @@ jobs:
|
|
|
|
|
#
|
|
|
|
|
# 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. Step 4 uses that to stop rebuilding it at all.
|
|
|
|
|
# reads 2026.8.27 — and the reuse step below turns that into a
|
|
|
|
|
# skipped build rather than a rebuild of bytes that already exist.
|
|
|
|
|
if [ "${GITHUB_REF#refs/tags/}" != "${GITHUB_REF}" ]; then
|
|
|
|
|
TAG_NAME="${GITHUB_REF#refs/tags/}"
|
|
|
|
|
echo "tags=git.fabledsword.com/bvandeusen/fabledcurator-agent:${TAG_NAME}" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "channel=main" >> "$GITHUB_OUTPUT"
|
|
|
|
|
elif [ "${GITHUB_REF##*/}" = "main" ]; then
|
|
|
|
|
CALVER=$(sh scripts/artifacts.sh tag agent)
|
|
|
|
|
# Guarded, and computed only on this path. There is no `set -e` in
|
|
|
|
@@ -701,8 +905,10 @@ jobs:
|
|
|
|
|
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 "channel=main" >> "$GITHUB_OUTPUT"
|
|
|
|
|
else
|
|
|
|
|
echo "tags=git.fabledsword.com/bvandeusen/fabledcurator-agent:dev" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "channel=dev" >> "$GITHUB_OUTPUT"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Shell step rather than docker/login-action — see build-web's note on
|
|
|
|
@@ -713,10 +919,95 @@ jobs:
|
|
|
|
|
ACTOR: ${{ github.actor }}
|
|
|
|
|
run: echo "$TOKEN" | docker login git.fabledsword.com -u "$ACTOR" --password-stdin
|
|
|
|
|
|
|
|
|
|
# --- reuse-if-published (milestone 313, step 4) ----------------------
|
|
|
|
|
# The identity tag names this artifact's CONTENT — r-<revision>, the
|
|
|
|
|
# commit its shipped files last changed in, plus the channel for images
|
|
|
|
|
# that bake one in. If the registry already carries it, the bytes this
|
|
|
|
|
# job would produce are already published and the build is pure waste:
|
|
|
|
|
# the channel and date tags get repointed at the existing manifest
|
|
|
|
|
# instead, registry-side, in seconds.
|
|
|
|
|
#
|
|
|
|
|
# This is what stops a push that touched only `agent/` from rebuilding
|
|
|
|
|
# web and ml, and a merge to main from rebuilding what dev already built.
|
|
|
|
|
#
|
|
|
|
|
# The failure direction is deliberate. An inspect that errors for ANY
|
|
|
|
|
# reason — network, auth, a registry hiccup — reads as a miss and the
|
|
|
|
|
# build runs. Only a genuine 200 skips one, so there is no path here
|
|
|
|
|
# that skips a build that was actually needed; the worst case is paying
|
|
|
|
|
# for a build we could have avoided.
|
|
|
|
|
#
|
|
|
|
|
# BASE-IMAGE FRESHNESS, decided rather than left implicit: an artifact
|
|
|
|
|
# whose source stops moving stops picking up base-image updates under
|
|
|
|
|
# its pinned tag. That is what a pin MEANS — a date tag has to keep
|
|
|
|
|
# serving the bytes it served (fabledcurator-agent:2026.7.17 still
|
|
|
|
|
# resolves to July's image), or it is not a pin — and family rule
|
|
|
|
|
# 145 already says where the refresh goes instead: a rebuild with
|
|
|
|
|
# different contents publishes only the MOVING tag, never the immutable
|
|
|
|
|
# one. A scheduled channel-only refresh is tracked separately (#3154);
|
|
|
|
|
# it does not belong in the push path.
|
|
|
|
|
- name: Is this content already published?
|
|
|
|
|
id: reuse
|
|
|
|
|
env:
|
|
|
|
|
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator-agent
|
|
|
|
|
CHANNEL: ${{ steps.tag.outputs.channel }}
|
|
|
|
|
TAGS: ${{ steps.tag.outputs.tags }}
|
|
|
|
|
IS_TAG_PUSH: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
|
|
|
run: |
|
|
|
|
|
set -eu
|
|
|
|
|
ID=$(sh scripts/artifacts.sh identity agent "$CHANNEL")
|
|
|
|
|
echo "identity=$ID" >> "$GITHUB_OUTPUT"
|
|
|
|
|
|
|
|
|
|
# A tag-push builds a revision that main already published, so it
|
|
|
|
|
# must NOT claim the identity: image configs are not bit-reproducible
|
|
|
|
|
# (embedded timestamps), so re-pushing r-<rev> would point an
|
|
|
|
|
# immutable tag at fresh bytes — rule 145's exact prohibition. It
|
|
|
|
|
# publishes only its own :v… label and otherwise reuses.
|
|
|
|
|
if [ "$IS_TAG_PUSH" = "true" ]; then
|
|
|
|
|
echo "build_tags=$TAGS" >> "$GITHUB_OUTPUT"
|
|
|
|
|
else
|
|
|
|
|
echo "build_tags=$TAGS,$IMAGE:$ID" >> "$GITHUB_OUTPUT"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if docker buildx imagetools inspect "$IMAGE:$ID" >/dev/null 2>&1; then
|
|
|
|
|
echo "hit=true" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "reuse: $IMAGE:$ID is already published — skipping the build"
|
|
|
|
|
else
|
|
|
|
|
echo "hit=false" >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "reuse: $IMAGE:$ID is not published — building"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
- name: Build and push agent image
|
|
|
|
|
if: steps.reuse.outputs.hit != 'true'
|
|
|
|
|
uses: docker/build-push-action@v5
|
|
|
|
|
with:
|
|
|
|
|
context: agent
|
|
|
|
|
file: agent/Dockerfile
|
|
|
|
|
push: true
|
|
|
|
|
tags: ${{ steps.tag.outputs.tags }}
|
|
|
|
|
tags: ${{ steps.reuse.outputs.build_tags }}
|
|
|
|
|
|
|
|
|
|
# Registry-side manifest copy: no layer transfer, no local daemon, no
|
|
|
|
|
# rebuild. Each -t becomes another reference to the SAME manifest the
|
|
|
|
|
# identity tag holds, so :latest and the date pin are byte-identical to
|
|
|
|
|
# what was 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'
|
|
|
|
|
env:
|
|
|
|
|
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator-agent
|
|
|
|
|
IDENTITY: ${{ steps.reuse.outputs.identity }}
|
|
|
|
|
TAGS: ${{ steps.tag.outputs.tags }}
|
|
|
|
|
run: |
|
|
|
|
|
set -euf
|
|
|
|
|
# steps.tag emits ONE comma-separated list, because that is the shape
|
|
|
|
|
# docker/build-push-action takes; imagetools wants a -t per ref.
|
|
|
|
|
ARGS=""
|
|
|
|
|
IFS=,
|
|
|
|
|
for t in $TAGS; do ARGS="$ARGS -t $t"; done
|
|
|
|
|
unset IFS
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
docker buildx imagetools create $ARGS "$IMAGE:$IDENTITY"
|
|
|
|
|
echo "repointed to $IMAGE:$IDENTITY: $TAGS"
|
|
|
|
|