Release: dev → main (first public release) #258
@@ -502,6 +502,10 @@ jobs:
|
||||
# to know — a candidate was published — rather than restating why.
|
||||
outputs:
|
||||
candidate: ${{ steps.reuse.outputs.promote }}
|
||||
# The manifest THIS run pushed, empty on a reuse hit. smoke-web addresses
|
||||
# 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.
|
||||
digest: ${{ steps.build.outputs.digest }}
|
||||
# A plain `needs` — no `always()`. That expression existed to let 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
|
||||
@@ -1152,7 +1156,25 @@ jobs:
|
||||
# publish.
|
||||
smoke-web:
|
||||
needs: [build-web]
|
||||
if: needs.build-web.outputs.candidate == 'true'
|
||||
# Every run that actually BUILT something, not just the weekly refresh.
|
||||
# The egress property (rule 164) is broken by a code or Dockerfile change,
|
||||
# which is a push — checking it only on the refresh would test it on the
|
||||
# one trigger that changes no source.
|
||||
#
|
||||
# A reuse hit is skipped deliberately: those bytes are already published
|
||||
# and were smoked when they were built. Re-smoking them would burn two
|
||||
# minutes to re-learn a fact.
|
||||
#
|
||||
# HONEST LIMIT, and it is the reason #4299 exists: on a push this runs
|
||||
# AFTER build-web has written the channel tag, so it detects rather than
|
||||
# gates. Rule 164's verify_with asks for the check BETWEEN build and push.
|
||||
# Closing that needs the push path to adopt the candidate-then-promote
|
||||
# shape the refresh already has — per-channel candidate tags, promote
|
||||
# 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.
|
||||
if: >-
|
||||
needs.build-web.outputs.candidate == 'true'
|
||||
|| needs.build-web.outputs.digest != ''
|
||||
runs-on: python-ci
|
||||
container:
|
||||
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||
@@ -1193,6 +1215,7 @@ jobs:
|
||||
env:
|
||||
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
ACTOR: ${{ github.actor }}
|
||||
BUILT_DIGEST: ${{ needs.build-web.outputs.digest }}
|
||||
run: |
|
||||
set -eux
|
||||
# Service discovery mirrors ci.yml's integration lane: these jobs run
|
||||
@@ -1222,10 +1245,59 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "$TOKEN" | docker login git.fabledsword.com -u "$ACTOR" --password-stdin
|
||||
CANDIDATE="$IMAGE:refresh-candidate"
|
||||
# A refresh publishes to the candidate tag; a push writes the
|
||||
# channel tag directly and hands us its digest. Address the digest
|
||||
# where we have one — it names the exact manifest this run built,
|
||||
# which a tag stops doing the moment anything else moves it.
|
||||
if [ -n "${BUILT_DIGEST:-}" ]; then
|
||||
CANDIDATE="$IMAGE@$BUILT_DIGEST"
|
||||
else
|
||||
CANDIDATE="$IMAGE:refresh-candidate"
|
||||
fi
|
||||
docker pull "$CANDIDATE"
|
||||
|
||||
ENVOPTS="-e DB_USER=$DB_USER -e DB_PASSWORD=$DB_PASSWORD -e DB_HOST=$PG_IP"
|
||||
# --- EGRESS BLOCKED from here (rule 164) ---------------------------
|
||||
#
|
||||
# Rule 164 requires a deployed instance to start and serve its full
|
||||
# UI with NO outbound internet, and says to verify it by removing the
|
||||
# network rather than by reading the code. Until now this job proved
|
||||
# the image WORKS; it never proved it works OFFLINE, because every
|
||||
# container below ran on the runner's default network with the
|
||||
# internet one hop away.
|
||||
#
|
||||
# That gap became load-bearing at milestone 422 step 6. The ML role
|
||||
# used to run `download_models` before celery started — a boot that
|
||||
# reached HuggingFace for ~3.5GB — and that fetch moved to a task
|
||||
# enqueued when the lane is enabled. This check is what proves it
|
||||
# actually moved, rather than proving it on the machine that built it
|
||||
# where the model is already cached.
|
||||
#
|
||||
# `--internal` is the mechanism rule 164's own verify_with names, and
|
||||
# `--network none` is explicitly the WRONG check here: it would only
|
||||
# prove the app fails without a database, which proves nothing about
|
||||
# egress. An internal network blocks the default route while leaving
|
||||
# container-to-container traffic and embedded DNS intact, so Postgres
|
||||
# and Redis stay reachable and nothing else is.
|
||||
#
|
||||
# The service containers are SIBLINGS created by the runner, so they
|
||||
# are attached to the internal network rather than created on it.
|
||||
# They keep their original network too — that is fine, since what
|
||||
# must be offline is the APP container, and it is created with only
|
||||
# this network.
|
||||
NET=smoke-noegress-$$
|
||||
docker network create --internal "$NET"
|
||||
trap 'docker network rm "$NET" >/dev/null 2>&1 || true' EXIT
|
||||
docker network connect "$NET" "$PG"
|
||||
docker network connect "$NET" "$RD"
|
||||
# Re-read the addresses ON THIS NETWORK. The IPs discovered above
|
||||
# belong to the runner's default bridge and are not routable from a
|
||||
# container that is only on the internal one.
|
||||
PG_IP=$(docker inspect -f "{{(index .NetworkSettings.Networks \"$NET\").IPAddress}}" "$PG")
|
||||
RD_IP=$(docker inspect -f "{{(index .NetworkSettings.Networks \"$NET\").IPAddress}}" "$RD")
|
||||
test -n "$PG_IP" && test -n "$RD_IP"
|
||||
|
||||
ENVOPTS="--network $NET"
|
||||
ENVOPTS="$ENVOPTS -e DB_USER=$DB_USER -e DB_PASSWORD=$DB_PASSWORD -e DB_HOST=$PG_IP"
|
||||
ENVOPTS="$ENVOPTS -e DB_PORT=5432 -e DB_NAME=$DB_NAME -e SECRET_KEY=$SECRET_KEY"
|
||||
ENVOPTS="$ENVOPTS -e CELERY_BROKER_URL=redis://$RD_IP:6379/0"
|
||||
ENVOPTS="$ENVOPTS -e CELERY_RESULT_BACKEND=redis://$RD_IP:6379/0"
|
||||
@@ -1238,6 +1310,22 @@ jobs:
|
||||
# user-facing file mentions this variable at all.
|
||||
ENVOPTS="$ENVOPTS -e CURATOR_BOOTSTRAP_NEW_KEY=1"
|
||||
|
||||
# 0. PROVE the network is actually blocking egress. Without this the
|
||||
# rest is theatre: if `--internal` silently stopped working, or
|
||||
# the app container picked up a second network, every check below
|
||||
# would pass with the internet available and report an offline
|
||||
# boot that never happened. A guard that cannot fail is not a
|
||||
# guard (rule 167).
|
||||
echo "smoke: confirming the sandbox has no route out"
|
||||
if docker run --rm --network "$NET" "$CANDIDATE" shell -c \
|
||||
'python3 -c "import socket,sys; s=socket.socket(); s.settimeout(5); sys.exit(0 if s.connect_ex((\"1.1.1.1\", 443)) == 0 else 1)"'; then
|
||||
echo "smoke: FAILED — the sandbox reached 1.1.1.1:443." >&2
|
||||
echo "smoke: the network is NOT internal, so nothing below would" >&2
|
||||
echo "smoke: have tested the offline property (rule 164)." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "smoke: no route out, as required"
|
||||
|
||||
# 1. The schema builds from empty, using the image's OWN libpq and
|
||||
# psycopg. This is the same call entrypoint.sh makes before it
|
||||
# serves anything, so a failure here is a failure to boot.
|
||||
@@ -1296,7 +1384,7 @@ jobs:
|
||||
curl -fsS --max-time 5 "http://$WEB_IP:8080/api/health"
|
||||
echo
|
||||
|
||||
echo "smoke: all checks passed against $CANDIDATE"
|
||||
echo "smoke: all checks passed against $CANDIDATE, with egress blocked"
|
||||
|
||||
# Move the channel tags — the whole point of the gate.
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user