Release: dev → main (first public release) #258
+116
-18
@@ -506,6 +506,10 @@ jobs:
|
|||||||
# it by digest rather than by tag: a tag can move between the build and
|
# it by digest rather than by tag: a tag can move between the build and
|
||||||
# the smoke, and then the check reports on bytes nobody built here.
|
# the smoke, and then the check reports on bytes nobody built here.
|
||||||
digest: ${{ steps.build.outputs.digest }}
|
digest: ${{ steps.build.outputs.digest }}
|
||||||
|
# What the channel tag already names. This is what smoke-web checks on a
|
||||||
|
# reuse hit — deliberately NOT folded into `digest`, which the :c-<sha>
|
||||||
|
# repoint reads and which must keep meaning "what this run built" (#4290).
|
||||||
|
published_digest: ${{ steps.reuse.outputs.published_digest }}
|
||||||
# A plain `needs` — no `always()`. That expression existed to let a
|
# A plain `needs` — no `always()`. That expression existed to let a
|
||||||
# SKIPPED sign-extension through on a tag push while still blocking a
|
# SKIPPED sign-extension through on a tag push while still blocking a
|
||||||
# FAILED one. With no tag trigger, sign-extension always runs, so the
|
# FAILED one. With no tag trigger, sign-extension always runs, so the
|
||||||
@@ -808,6 +812,16 @@ jobs:
|
|||||||
PUBLISHED=$(docker buildx imagetools inspect "$IMAGE:$T" \
|
PUBLISHED=$(docker buildx imagetools inspect "$IMAGE:$T" \
|
||||||
--format '{{ index .Image.Config.Labels "fc.revision" }}' \
|
--format '{{ index .Image.Config.Labels "fc.revision" }}' \
|
||||||
2>/dev/null || echo "")
|
2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# The manifest the channel tag names RIGHT NOW. Exported so the
|
||||||
|
# smoke has something to check on a reuse hit, when this job builds
|
||||||
|
# nothing and emits no digest of its own (#4323). Resolved here
|
||||||
|
# because this step is already resolving the tag to read its label —
|
||||||
|
# one lookup, one answer, rather than a second one that could name a
|
||||||
|
# different manifest if anything moved the tag in between.
|
||||||
|
PUBLISHED_DIGEST=$(docker buildx imagetools inspect "$IMAGE:$T" \
|
||||||
|
--format '{{ .Manifest.Digest }}' 2>/dev/null || echo "")
|
||||||
|
echo "published_digest=$PUBLISHED_DIGEST" >> "$GITHUB_OUTPUT"
|
||||||
echo "reuse: $IMAGE:$T carries fc.revision=${PUBLISHED:-<none>}; derived=$DERIVED"
|
echo "reuse: $IMAGE:$T carries fc.revision=${PUBLISHED:-<none>}; derived=$DERIVED"
|
||||||
if [ -z "$PUBLISHED" ] && docker buildx imagetools inspect "$IMAGE:$T" >/dev/null 2>&1; then
|
if [ -z "$PUBLISHED" ] && docker buildx imagetools inspect "$IMAGE:$T" >/dev/null 2>&1; then
|
||||||
# The tag resolves but carries no readable label. Expected exactly
|
# The tag resolves but carries no readable label. Expected exactly
|
||||||
@@ -1172,9 +1186,29 @@ jobs:
|
|||||||
# shape the refresh already has — per-channel candidate tags, promote
|
# shape the refresh already has — per-channel candidate tags, promote
|
||||||
# learning its channel, and the :c-<sha> repoint moving after the gate.
|
# learning its channel, and the :c-<sha> repoint moving after the gate.
|
||||||
# That is a redesign of the production publish path and is its own task.
|
# That is a redesign of the production publish path and is its own task.
|
||||||
if: >-
|
#
|
||||||
needs.build-web.outputs.candidate == 'true'
|
# NO `if:` — this job always runs (#4323). It used to be gated on the
|
||||||
|| needs.build-web.outputs.digest != ''
|
# build having published something, which skipped it on a reuse hit. That
|
||||||
|
# sounds like an optimisation and is a hole: this workflow file is in no
|
||||||
|
# artifact's path set (correctly — editing it changes no shipped byte), so
|
||||||
|
# a commit that touches ONLY the smoke moves no revision, hits reuse,
|
||||||
|
# emits no digest, and skips the smoke. The one commit whose purpose is
|
||||||
|
# changing this check was the one commit that could not run it.
|
||||||
|
#
|
||||||
|
# That is not hypothetical twice over. 5ca1058 added the egress sandbox
|
||||||
|
# and went green three times with this job SKIPPED. 7175ace fixed the
|
||||||
|
# bug that hid behind those greens (#4319) and needed a manual
|
||||||
|
# force_build to exercise at all. Both relied on someone remembering.
|
||||||
|
#
|
||||||
|
# It is also where the other historical failure lived: on run 5290 this
|
||||||
|
# expression read `env`, which a job `if:` cannot see, so it evaluated
|
||||||
|
# empty and skipped silently. Two skips, one expression. The expression
|
||||||
|
# goes.
|
||||||
|
#
|
||||||
|
# The cost is one pull and boot on a reuse-hit push, re-smoking bytes
|
||||||
|
# that were smoked when they were built. That is the price of a harness
|
||||||
|
# that tests itself, and it overlaps ci.yml's lanes, so little wall-clock
|
||||||
|
# moves. Not running is not the same as passing.
|
||||||
runs-on: python-ci
|
runs-on: python-ci
|
||||||
container:
|
container:
|
||||||
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||||
@@ -1216,6 +1250,8 @@ jobs:
|
|||||||
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
ACTOR: ${{ github.actor }}
|
ACTOR: ${{ github.actor }}
|
||||||
BUILT_DIGEST: ${{ needs.build-web.outputs.digest }}
|
BUILT_DIGEST: ${{ needs.build-web.outputs.digest }}
|
||||||
|
PUBLISHED_DIGEST: ${{ needs.build-web.outputs.published_digest }}
|
||||||
|
IS_CANDIDATE: ${{ needs.build-web.outputs.candidate }}
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
# Service discovery mirrors ci.yml's integration lane: these jobs run
|
# Service discovery mirrors ci.yml's integration lane: these jobs run
|
||||||
@@ -1245,14 +1281,32 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$TOKEN" | docker login git.fabledsword.com -u "$ACTOR" --password-stdin
|
echo "$TOKEN" | docker login git.fabledsword.com -u "$ACTOR" --password-stdin
|
||||||
# A refresh publishes to the candidate tag; a push writes the
|
# WHAT GETS SMOKED, in order of preference — always a DIGEST, never
|
||||||
# channel tag directly and hands us its digest. Address the digest
|
# a tag: a tag can move between the build and this job, and then the
|
||||||
# where we have one — it names the exact manifest this run built,
|
# check reports on bytes nobody here decided to ship (#4290).
|
||||||
# which a tag stops doing the moment anything else moves it.
|
#
|
||||||
if [ -n "${BUILT_DIGEST:-}" ]; then
|
# 1. what this run built, when it built;
|
||||||
CANDIDATE="$IMAGE@$BUILT_DIGEST"
|
# 2. what the channel tag already names, on a reuse hit — the
|
||||||
else
|
# digest the reuse step resolved while reading its label. This
|
||||||
|
# is the case that makes the job able to verify a change to
|
||||||
|
# ITSELF (#4323), since such a change rebuilds nothing.
|
||||||
|
#
|
||||||
|
# A refresh always lands in (1); the candidate tag stays only as the
|
||||||
|
# last resort for one, and says so rather than being a silent else.
|
||||||
|
DIGEST="${BUILT_DIGEST:-}"
|
||||||
|
[ -n "$DIGEST" ] || DIGEST="${PUBLISHED_DIGEST:-}"
|
||||||
|
if [ -n "$DIGEST" ]; then
|
||||||
|
CANDIDATE="$IMAGE@$DIGEST"
|
||||||
|
elif [ "${IS_CANDIDATE:-}" = "true" ]; then
|
||||||
CANDIDATE="$IMAGE:refresh-candidate"
|
CANDIDATE="$IMAGE:refresh-candidate"
|
||||||
|
else
|
||||||
|
# Nothing built and nothing published. There is no artifact this
|
||||||
|
# job could honestly report on, so it fails rather than passing
|
||||||
|
# quietly — a guard with nothing to check is not a passing guard.
|
||||||
|
echo "smoke: FAILED — no image to smoke. build-web neither built" >&2
|
||||||
|
echo "smoke: one nor resolved a published digest, so there is" >&2
|
||||||
|
echo "smoke: nothing here to verify." >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
docker pull "$CANDIDATE"
|
docker pull "$CANDIDATE"
|
||||||
|
|
||||||
@@ -1284,9 +1338,55 @@ jobs:
|
|||||||
# They keep their original network too — that is fine, since what
|
# They keep their original network too — that is fine, since what
|
||||||
# must be offline is the APP container, and it is created with only
|
# must be offline is the APP container, and it is created with only
|
||||||
# this network.
|
# this network.
|
||||||
NET=smoke-noegress-$$
|
# Named for the RUN, not for `$$`. The shell's pid is deterministic
|
||||||
|
# in this runner — every execution of this step got 157 — so `$$`
|
||||||
|
# produced one shared name, and the second run died on "network with
|
||||||
|
# name smoke-noegress-157 already exists". A pid is unique among
|
||||||
|
# LIVE processes, which is not the same as unique over time, and in
|
||||||
|
# a fresh container it is neither.
|
||||||
|
# The date fallback matters: if the runner does not set these,
|
||||||
|
# a literal default would put every run back on one shared name
|
||||||
|
# — the bug, with different letters.
|
||||||
|
NET="smoke-noegress-${GITHUB_RUN_ID:-$(date +%s)}-${GITHUB_RUN_ATTEMPT:-0}"
|
||||||
|
|
||||||
|
# Sweep anything an earlier run left behind. Needed because the
|
||||||
|
# cleanup below used to be destroyed before it could fire (see the
|
||||||
|
# trap note), so every execution leaked its network. `rm` on one
|
||||||
|
# still in use fails, and `|| true` keeps that harmless — so a
|
||||||
|
# concurrent run's network survives this.
|
||||||
|
docker network ls --filter name=^smoke-noegress- -q \
|
||||||
|
| while read -r stale; do
|
||||||
|
docker network rm "$stale" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
|
||||||
docker network create --internal "$NET"
|
docker network create --internal "$NET"
|
||||||
trap 'docker network rm "$NET" >/dev/null 2>&1 || true' EXIT
|
|
||||||
|
# ONE exit trap, for everything. `trap ... EXIT` REPLACES the
|
||||||
|
# previous handler rather than adding to it, so the network's own
|
||||||
|
# trap used to be silently discarded the moment the container's was
|
||||||
|
# installed further down — and the network was never removed. That
|
||||||
|
# is invisible in a passing run and only ever surfaces on the NEXT
|
||||||
|
# one, as a name collision.
|
||||||
|
#
|
||||||
|
# CID is empty until the app container exists, so this is safe to
|
||||||
|
# arm now and still covers a failure before that point.
|
||||||
|
CID=""
|
||||||
|
cleanup() {
|
||||||
|
rc=$?
|
||||||
|
if [ -n "$CID" ]; then
|
||||||
|
# The log ONLY on failure — a boot that never answered must fail
|
||||||
|
# with the reason visible rather than as a bare timeout (rule
|
||||||
|
# 156), while a green run has nothing to say.
|
||||||
|
[ $rc -eq 0 ] || docker logs "$CID" 2>&1 | tail -40
|
||||||
|
docker rm -f "$CID" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
docker network rm "$NET" >/dev/null 2>&1 || true
|
||||||
|
# Preserve the real status, which a trap ending on a successful
|
||||||
|
# `docker rm` would otherwise mask.
|
||||||
|
exit $rc
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
docker network connect "$NET" "$PG"
|
docker network connect "$NET" "$PG"
|
||||||
docker network connect "$NET" "$RD"
|
docker network connect "$NET" "$RD"
|
||||||
# Re-read the addresses ON THIS NETWORK. The IPs discovered above
|
# Re-read the addresses ON THIS NETWORK. The IPs discovered above
|
||||||
@@ -1344,13 +1444,11 @@ jobs:
|
|||||||
# siblings directly and a published port would collide with
|
# siblings directly and a published port would collide with
|
||||||
# whatever else the runner is hosting.
|
# whatever else the runner is hosting.
|
||||||
echo "smoke: web boots and answers /api/health"
|
echo "smoke: web boots and answers /api/health"
|
||||||
|
# Assigning CID is all that is needed — the single EXIT trap armed
|
||||||
|
# beside the network already covers the container, and it checks CID
|
||||||
|
# for emptiness precisely so it can be installed before this line.
|
||||||
|
# Installing a second trap here is what used to discard the first.
|
||||||
CID=$(docker run -d $ENVOPTS "$CANDIDATE" web)
|
CID=$(docker run -d $ENVOPTS "$CANDIDATE" web)
|
||||||
# Clean up the container however this ends, and dump its log ONLY
|
|
||||||
# on failure — a boot that never answers must fail with the reason
|
|
||||||
# visible rather than as a bare timeout (rule 156), while a green run
|
|
||||||
# has nothing to say. `exit $rc` preserves the real status, which a
|
|
||||||
# trap that ends on a successful `docker rm` would otherwise mask.
|
|
||||||
trap 'rc=$?; [ $rc -eq 0 ] || docker logs "$CID" 2>&1 | tail -40; docker rm -f "$CID" >/dev/null 2>&1 || true; exit $rc' EXIT
|
|
||||||
WEB_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CID")
|
WEB_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CID")
|
||||||
test -n "$WEB_IP"
|
test -n "$WEB_IP"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user