Release: dev → main (first public release) #258
@@ -502,6 +502,10 @@ jobs:
|
|||||||
# to know — a candidate was published — rather than restating why.
|
# to know — a candidate was published — rather than restating why.
|
||||||
outputs:
|
outputs:
|
||||||
candidate: ${{ steps.reuse.outputs.promote }}
|
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
|
# 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
|
||||||
@@ -1152,7 +1156,25 @@ jobs:
|
|||||||
# publish.
|
# publish.
|
||||||
smoke-web:
|
smoke-web:
|
||||||
needs: [build-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
|
runs-on: python-ci
|
||||||
container:
|
container:
|
||||||
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||||
@@ -1193,6 +1215,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
ACTOR: ${{ github.actor }}
|
ACTOR: ${{ github.actor }}
|
||||||
|
BUILT_DIGEST: ${{ needs.build-web.outputs.digest }}
|
||||||
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
|
||||||
@@ -1222,10 +1245,59 @@ 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
|
||||||
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"
|
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 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_BROKER_URL=redis://$RD_IP:6379/0"
|
||||||
ENVOPTS="$ENVOPTS -e CELERY_RESULT_BACKEND=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.
|
# user-facing file mentions this variable at all.
|
||||||
ENVOPTS="$ENVOPTS -e CURATOR_BOOTSTRAP_NEW_KEY=1"
|
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
|
# 1. The schema builds from empty, using the image's OWN libpq and
|
||||||
# psycopg. This is the same call entrypoint.sh makes before it
|
# psycopg. This is the same call entrypoint.sh makes before it
|
||||||
# serves anything, so a failure here is a failure to boot.
|
# 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"
|
curl -fsS --max-time 5 "http://$WEB_IP:8080/api/health"
|
||||||
echo
|
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.
|
# Move the channel tags — the whole point of the gate.
|
||||||
#
|
#
|
||||||
|
|||||||
Reference in New Issue
Block a user