fix(ORCH-058): parametrize staging_check in --build-staging + explicit staging target
Round-3 review follow-up on c53d625 (P1/P2):
- P1: --build-staging now runs staging_check via parametrized
STAGING_CONTAINER / STAGING_CHECK_PATH / STAGING_CHECK_MODE (default
orchestrator-staging / bind-mount path / stub) instead of hardcoding
$TARGET_SERVICE + the script path. docker exec runs INSIDE the staging
container (ORCH-048 canonical: B6 registry isolation), after health,
before exit 0. Fail-closed: any non-zero -> exit 1. STAGING only (8501).
- P2a: rebuild_staging_image now passes the STAGING target EXPLICITLY
(TARGET_SERVICE/TARGET_PORT/COMPOSE_PROFILE/STAGING_CONTAINER) so the
self-rebuild can never drift onto prod 8500 if hook defaults change (AC-9).
- P2b: TC-09 caller<->hook contract tests assert the ssh command carries
GIT_SHA + BUILD_CONTEXT + the staging target and never the prod 8500 one;
no-ssh-host fails closed.
- P3: consolidated the three duplicate README footers into one.
- Docs (golden source): DEPLOY_HOOK.md step 4 + env rows, README footer,
CHANGELOG, Dockerfile ARG GIT_SHA="" comment, .env.example freshness block.
Validates exactly the artefact later BUILD-ONCE retagged to prod (AC-4,
ADR-001 step 3). 632 tests pass, ruff clean, bash -n OK.
Refs: ORCH-058
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -14,17 +14,18 @@
|
||||
# TARGET_IMAGE instead of rebuilding — guarantees prod runs the
|
||||
# exact artefact that passed staging (no `docker build`).
|
||||
# EXPECTED_REVISION- expected git SHA of SOURCE_IMAGE (default: unset; ORCH-58)
|
||||
# Strategy-B fail-closed provenance guard: when set, the
|
||||
# Strategy B fail-closed provenance guard: when set, the
|
||||
# SOURCE_IMAGE's org.opencontainers.image.revision label MUST
|
||||
# equal this value before the BUILD-ONCE retag, else exit 1
|
||||
# (a stale image is never promoted). Unset -> no check (legacy).
|
||||
# GIT_SHA - --build-staging build-arg (default: unset; ORCH-58)
|
||||
# Commit stamped into the rebuilt staging image's revision
|
||||
# label. Supplied by the caller (validated commit) — NOT
|
||||
# recomputed from the host clone's HEAD.
|
||||
# BUILD_CONTEXT - --build-staging build context (default: $REPO; ORCH-58)
|
||||
# Host worktree of the validated commit; the staging image is
|
||||
# rebuilt FROM this tree (not the prod clone on main).
|
||||
# GIT_SHA - build-arg for --build-staging (default: unset; ORCH-58)
|
||||
# BUILD_CONTEXT - docker build context dir (default: $REPO; --build-staging)
|
||||
# STAGING_CONTAINER- container to docker-exec staging_check in (--build-staging;
|
||||
# default: $TARGET_SERVICE → orchestrator-staging; ORCH-58)
|
||||
# STAGING_CHECK_PATH- staging_check.py path inside that container (--build-staging;
|
||||
# default: /repos/orchestrator/scripts/staging_check.py; ORCH-58)
|
||||
# STAGING_CHECK_MODE- staging_check mode stub|full-real (--build-staging;
|
||||
# default: stub — fast, no LLM spend; ORCH-58)
|
||||
# LOG - log file path (default: /var/log/orchestrator/deploy-hook.log)
|
||||
#
|
||||
# Usage:
|
||||
@@ -45,11 +46,11 @@ PREV_IMAGE_FILE="${PREV_IMAGE_FILE:-$REPO/.deploy-prev-image-staging}"
|
||||
# Build-once (ORCH-36): optional prevalidated source image to retag onto
|
||||
# TARGET_IMAGE. Unset -> backward-compatible (no retag), exit-code contract intact.
|
||||
SOURCE_IMAGE="${SOURCE_IMAGE:-}"
|
||||
# Provenance guard (ORCH-58 Strategy-B): the OCI revision label the hook
|
||||
# inspects on SOURCE_IMAGE, and the git revision it MUST match before retag
|
||||
# onto prod. EXPECTED_REVISION unset -> backward-compatible (guard skipped).
|
||||
REVISION_LABEL="org.opencontainers.image.revision"
|
||||
# Provenance guard (ORCH-58, Strategy B): expected git SHA of SOURCE_IMAGE. Unset
|
||||
# -> backward-compatible (no provenance check), exit-code contract intact.
|
||||
EXPECTED_REVISION="${EXPECTED_REVISION:-}"
|
||||
# The OCI-standard label key the Dockerfile stamps with the build commit.
|
||||
REVISION_LABEL="org.opencontainers.image.revision"
|
||||
|
||||
# ---- Log setup -------------------------------------------------------------
|
||||
LOG_DIR=/var/log/orchestrator
|
||||
@@ -149,20 +150,19 @@ fi
|
||||
|
||||
# ============================================================================
|
||||
# --build-staging mode (ORCH-58, Strategy A): rebuild the STAGING image from the
|
||||
# VALIDATED commit and recreate 8501, so the artefact we validate is the EXACT one
|
||||
# later BUILD-ONCE retagged to prod (INV-FRESH). Builds/recreates STAGING ONLY
|
||||
# (8501) — never prod (8500). Same exit-code contract (0 = healthy, !=0 = failed).
|
||||
#
|
||||
# Uses the caller-supplied GIT_SHA + BUILD_CONTEXT (the validated worktree) — it
|
||||
# must NOT recompute HEAD from $REPO (the prod clone on `main`): on the
|
||||
# deploy-staging -> deploy edge the PR is not yet merged, so `main` HEAD != the
|
||||
# validated SHA, which would stamp the wrong revision label and deadlock the
|
||||
# Strategy-B guard on every valid self-deploy.
|
||||
# VALIDATED commit, recreate 8501, and run the AUTHORITATIVE staging_check against
|
||||
# the fresh image, so the artefact we validate is the exact one later BUILD-ONCE
|
||||
# retagged to prod (INV-FRESH, AC-4). Builds/recreates STAGING ONLY (8501) — never
|
||||
# prod (8500). Same exit-code contract (0 = healthy + staging_check PASS).
|
||||
# GIT_SHA - commit stamped into the image revision label (build-arg).
|
||||
# BUILD_CONTEXT - docker build context (host worktree of the validated commit).
|
||||
# Steps: (1) docker build → (2) recreate 8501 → (3a) health-check →
|
||||
# (3b) staging_check.py --mode stub against the fresh 8501 (ADR-001 step 3).
|
||||
# ============================================================================
|
||||
if [[ "${1:-}" == "--build-staging" ]]; then
|
||||
BUILD_CONTEXT="${BUILD_CONTEXT:-$REPO}"
|
||||
GIT_SHA="${GIT_SHA:-}"
|
||||
log "BUILD-STAGING: rebuilding $TARGET_IMAGE from $BUILD_CONTEXT (GIT_SHA=$GIT_SHA, service=$TARGET_SERVICE, port=$TARGET_PORT)"
|
||||
log "BUILD-STAGING: rebuilding $TARGET_IMAGE from $BUILD_CONTEXT (GIT_SHA=$GIT_SHA, port=$TARGET_PORT)"
|
||||
if ! docker build --build-arg GIT_SHA="$GIT_SHA" -t "$TARGET_IMAGE" "$BUILD_CONTEXT" >> "$LOG" 2>&1; then
|
||||
log "BUILD-STAGING: docker build failed - aborting (exit 1)"
|
||||
exit 1
|
||||
@@ -174,24 +174,28 @@ if [[ "${1:-}" == "--build-staging" ]]; then
|
||||
docker compose up -d --no-build "$TARGET_SERVICE" >> "$LOG" 2>&1
|
||||
fi
|
||||
log "BUILD-STAGING: running health-check on port $TARGET_PORT (10x6s)"
|
||||
if health_check 10 6 "build-staging-health"; then
|
||||
log "BUILD-STAGING: $TARGET_SERVICE healthy on the fresh image"
|
||||
# AC-4 / ADR-001 step 3: validate the EXACT fresh artefact that will be
|
||||
# BUILD-ONCE retagged to prod by running staging_check.py against the
|
||||
# freshly recreated STAGING stand (8501, never prod 8500 - AC-9).
|
||||
# --mode stub: fast, deterministic, no LLM spend (ADR). Run INSIDE the
|
||||
# container so B6 reads the running instance own env (.env.staging).
|
||||
log "BUILD-STAGING: running staging_check.py --mode stub against fresh 8501 (port $TARGET_PORT)"
|
||||
if docker exec "$TARGET_SERVICE" \\
|
||||
python3 /repos/orchestrator/scripts/staging_check.py \\
|
||||
--base-url "http://localhost:$TARGET_PORT" --mode stub >> "$LOG" 2>&1; then
|
||||
log "BUILD-STAGING: staging_check --mode stub PASS on fresh image (exit 0)"
|
||||
exit 0
|
||||
fi
|
||||
log "BUILD-STAGING: staging_check --mode stub FAILED on fresh image - not promoting (exit 1)"
|
||||
if ! health_check 10 6 "build-staging-health"; then
|
||||
log "BUILD-STAGING: health FAILED after rebuild (exit 1)"
|
||||
exit 1
|
||||
fi
|
||||
log "BUILD-STAGING: health FAILED after rebuild (exit 1)"
|
||||
log "BUILD-STAGING: $TARGET_SERVICE healthy on fresh image"
|
||||
# (3b) ORCH-58 (Strategy A, step 3 — ADR-001): authoritative e2e validation of
|
||||
# the FRESH image. Run staging_check.py against the just-rebuilt 8501 INSIDE the
|
||||
# staging container (ORCH-048 canonical: it reads its OWN staging registry env, so
|
||||
# B6 is correct; the script lives at /repos/... via bind-mount, not in /app). This
|
||||
# is the same artefact later BUILD-ONCE retagged to prod, so we validate exactly
|
||||
# what we promote (AC-4). Any non-zero (FAIL or ORCH_STAGING safety-abort) -> exit 1
|
||||
# -> freshness gate FAIL -> rollback to development. Same exit-code contract.
|
||||
STAGING_CONTAINER="${STAGING_CONTAINER:-$TARGET_SERVICE}"
|
||||
STAGING_CHECK_PATH="${STAGING_CHECK_PATH:-/repos/orchestrator/scripts/staging_check.py}"
|
||||
STAGING_CHECK_MODE="${STAGING_CHECK_MODE:-stub}"
|
||||
log "BUILD-STAGING: running staging_check (--mode $STAGING_CHECK_MODE) against fresh http://localhost:$TARGET_PORT inside $STAGING_CONTAINER"
|
||||
if docker exec "$STAGING_CONTAINER" python3 "$STAGING_CHECK_PATH" \
|
||||
--base-url "http://localhost:$TARGET_PORT" --mode "$STAGING_CHECK_MODE" >> "$LOG" 2>&1; then
|
||||
log "BUILD-STAGING: staging_check PASS on fresh image (exit 0)"
|
||||
exit 0
|
||||
fi
|
||||
log "BUILD-STAGING: staging_check FAILED on fresh image - artefact not promotable (exit 1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -222,21 +226,19 @@ git pull origin main >> "$LOG" 2>&1
|
||||
# Backward compatible: skipped when SOURCE_IMAGE is unset.
|
||||
if [[ -n "$SOURCE_IMAGE" ]]; then
|
||||
if docker image inspect "$SOURCE_IMAGE" >/dev/null 2>&1; then
|
||||
# Fail-closed provenance guard: when EXPECTED_REVISION is set, the
|
||||
# source image MUST carry the matching git-revision OCI label, else
|
||||
# abort BEFORE the prod retag. Empty EXPECTED_REVISION -> guard
|
||||
# skipped (ORCH-36 backward-compat).
|
||||
# ORCH-58 (Strategy B): fail-closed provenance guard BEFORE docker tag.
|
||||
# When EXPECTED_REVISION is set, SOURCE_IMAGE's git-commit label MUST match,
|
||||
# else exit 1 (FAILED -> БАГ-8 rollback); prod is NEVER touched. Empty label
|
||||
# / inspect error / mismatch all fail-close. Unset EXPECTED_REVISION -> no
|
||||
# check (backward-compatible for non-self repos / legacy calls).
|
||||
if [[ -n "$EXPECTED_REVISION" ]]; then
|
||||
IMG_REV=$(docker image inspect --format '{{ index .Config.Labels "'"$REVISION_LABEL"'" }}' "$SOURCE_IMAGE" 2>/dev/null || true)
|
||||
# docker emits "<no value>" when the label is absent -> normalise.
|
||||
if [[ "$IMG_REV" == "<no value>" ]]; then
|
||||
IMG_REV=""
|
||||
fi
|
||||
IMG_REV=$(docker image inspect --format "{{ index .Config.Labels \"$REVISION_LABEL\" }}" "$SOURCE_IMAGE" 2>/dev/null || true)
|
||||
if [[ "$IMG_REV" == "<no value>" ]]; then IMG_REV=""; fi
|
||||
if [[ -z "$IMG_REV" || "$IMG_REV" != "$EXPECTED_REVISION" ]]; then
|
||||
log "PROVENANCE: SOURCE_IMAGE revision '$IMG_REV' != expected '$EXPECTED_REVISION' - aborting before retag (exit 1)"
|
||||
log "PROVENANCE: SOURCE_IMAGE revision '$IMG_REV' != expected '$EXPECTED_REVISION' (fail-closed) - aborting (exit 1)"
|
||||
exit 1
|
||||
fi
|
||||
log "PROVENANCE: SOURCE_IMAGE revision matches expected ($EXPECTED_REVISION)"
|
||||
log "PROVENANCE: SOURCE_IMAGE revision matches expected ($EXPECTED_REVISION) - retag allowed"
|
||||
fi
|
||||
log "BUILD-ONCE: retagging $SOURCE_IMAGE -> $TARGET_IMAGE (no rebuild)"
|
||||
docker tag "$SOURCE_IMAGE" "$TARGET_IMAGE" >> "$LOG" 2>&1
|
||||
|
||||
Reference in New Issue
Block a user