Files
orchestrator/tests/test_deploy_hook_provenance.py
claude-bot 6ddff5583d
All checks were successful
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 18s
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>
2026-06-07 09:24:38 +00:00

160 lines
8.0 KiB
Python

"""ORCH-058 TC-07/08: static + caller-contract guarantees of the provenance plumbing.
These assert the *shape* of the deploy artefacts that can't be unit-tested by
running them (they shell out to docker/ssh on the host):
* TC-07 — the deploy hook fail-closes BEFORE `docker tag` when the staging
image's git-revision label != EXPECTED_REVISION (exit 1), and the
new `--build-staging` rebuild mode (a) stamps GIT_SHA into the image,
(b) uses $BUILD_CONTEXT as the build context, (c) recreates 8501 +
health-checks, (d) runs staging_check against the FRESH image
(Strategy A step 3, AC-4), and (e) never recomputes GIT_SHA from $REPO.
* TC-08 — the Dockerfile declares `ARG GIT_SHA` and stamps it into the
`org.opencontainers.image.revision` OCI label (the anchor B reads).
* TC-09 — the caller↔hook contract: `rebuild_staging_image` invokes the hook
in `--build-staging` mode with BUILD_CONTEXT=<host-worktree>,
GIT_SHA=<validated sha>, and an EXPLICIT staging target (never prod).
"""
import pathlib
_ROOT = pathlib.Path(__file__).resolve().parents[1]
_HOOK = _ROOT / "scripts" / "orchestrator-deploy-hook.sh"
_DOCKERFILE = _ROOT / "Dockerfile"
# ---------------------------------------------------------------------------
# TC-07: hook fail-closed provenance guard + --build-staging rebuild mode
# ---------------------------------------------------------------------------
def test_tc07_hook_has_fail_closed_provenance_guard():
text = _HOOK.read_text(encoding="utf-8")
# The label key the hook inspects must be the OCI revision label.
assert 'REVISION_LABEL="org.opencontainers.image.revision"' in text
# EXPECTED_REVISION is read (default unset -> backward compatible).
assert 'EXPECTED_REVISION="${EXPECTED_REVISION:-}"' in text
# The guard must inspect the source image's label and normalise <no value>.
assert "docker image inspect --format" in text
assert '"<no value>"' in text
# Fail-closed: empty OR mismatch -> abort with exit 1.
assert '-z "$IMG_REV" || "$IMG_REV" != "$EXPECTED_REVISION"' in text
def test_tc07_provenance_guard_precedes_docker_tag():
"""The fail-closed `exit 1` must sit BEFORE the `docker tag` retag line."""
text = _HOOK.read_text(encoding="utf-8")
guard = text.index("$EXPECTED_REVISION")
retag = text.index('docker tag "$SOURCE_IMAGE" "$TARGET_IMAGE"')
assert guard < retag, "provenance guard must run before the prod retag"
def test_tc07_build_staging_mode_stamps_git_sha():
text = _HOOK.read_text(encoding="utf-8")
# The new Strategy-A rebuild mode exists and is keyed on --build-staging.
assert '"${1:-}" == "--build-staging"' in text
# It rebuilds the staging image stamping the validated commit as a build-arg.
assert 'docker build --build-arg GIT_SHA="$GIT_SHA"' in text
def test_tc07_build_staging_uses_build_context_and_recreates_8501():
"""The rebuild must use $BUILD_CONTEXT as the docker build context and recreate
the staging service with a health-check (not a bare build)."""
text = _HOOK.read_text(encoding="utf-8")
# $BUILD_CONTEXT is the build context of the rebuild (validated worktree).
assert 'docker build --build-arg GIT_SHA="$GIT_SHA" -t "$TARGET_IMAGE" "$BUILD_CONTEXT"' in text
# Recreate the staging service on the fresh image (no-build) + health-check.
assert 'up -d --no-build "$TARGET_SERVICE"' in text
assert 'health_check 10 6 "build-staging-health"' in text
def test_tc07_build_staging_does_not_recompute_git_sha_from_repo():
"""Regression guard (root cause of the silent-stale-promote class): the
--build-staging mode must NOT derive GIT_SHA itself from the prod $REPO clone —
it must consume the GIT_SHA passed in by the caller (the validated commit)."""
text = _HOOK.read_text(encoding="utf-8")
# Anchor on the actual block guard (not the header comment mentions).
after = text[text.index('"${1:-}" == "--build-staging"'):]
assert 'GIT_SHA="${GIT_SHA:-}"' in after
assert "git rev-parse" not in after, "GIT_SHA must come from the caller, not the prod clone"
def test_tc07_build_staging_runs_staging_check_against_fresh_image():
"""Strategy A step 3 (ADR-001, AC-4): after recreate+health, the FRESH image is
validated by staging_check.py (not health-only). This is the P1 the reviewer
flagged: validate exactly the artefact later retagged to prod."""
text = _HOOK.read_text(encoding="utf-8")
# Anchor on the actual block guard (not the header comment mentions).
after = text[text.index('"${1:-}" == "--build-staging"'):]
# staging_check is invoked, inside the staging container, --mode stub by default.
assert "staging_check.py" in after
assert 'docker exec "$STAGING_CONTAINER"' in after
assert '--mode "$STAGING_CHECK_MODE"' in after
assert 'STAGING_CHECK_MODE="${STAGING_CHECK_MODE:-stub}"' in after
# The staging_check run must come AFTER the health-check (health gates readiness).
assert after.index('health_check 10 6 "build-staging-health"') < after.index("staging_check.py")
# ---------------------------------------------------------------------------
# TC-08: Dockerfile stamps the OCI revision label from a build-arg
# ---------------------------------------------------------------------------
def test_tc08_dockerfile_stamps_revision_label():
text = _DOCKERFILE.read_text(encoding="utf-8")
assert "ARG GIT_SHA" in text
assert "LABEL org.opencontainers.image.revision=$GIT_SHA" in text
# ---------------------------------------------------------------------------
# TC-09: caller↔hook contract — rebuild_staging_image builds the right command
# ---------------------------------------------------------------------------
def test_tc09_rebuild_staging_image_passes_validated_context_and_staging_target(monkeypatch):
"""`rebuild_staging_image` must invoke the hook `--build-staging` over ssh with
BUILD_CONTEXT=<host-worktree>, GIT_SHA=<validated sha>, and an EXPLICIT staging
target (service/port/profile/container) — never the prod 8500 target. The absence
of this contract test is what hid the earlier P0s (review P2)."""
import src.image_freshness as imgf
captured = {}
class _FakeCompleted:
returncode = 0
stdout = ""
stderr = ""
def _fake_run(cmd, *a, **kw):
captured["cmd"] = cmd
return _FakeCompleted()
monkeypatch.setattr(imgf, "_ssh_target", lambda: "slin@host")
monkeypatch.setattr(imgf, "_host_worktree_path",
lambda repo, branch: "/home/slin/repos/_wt/orchestrator/feature_X")
monkeypatch.setattr(imgf.subprocess, "run", _fake_run)
ok, msg = imgf.rebuild_staging_image("orchestrator", "feature/ORCH-058", "abc123def456")
assert ok, msg
cmd = captured["cmd"]
assert cmd[0] == "ssh"
inner = cmd[-1] # the remote shell command string
# Validated commit + validated worktree as build context.
assert "GIT_SHA=abc123def456" in inner
assert "BUILD_CONTEXT=/home/slin/repos/_wt/orchestrator/feature_X" in inner
# Explicit STAGING target — never the prod 8500 service/port.
assert "TARGET_SERVICE=orchestrator-staging" in inner
assert "TARGET_PORT=8501" in inner
assert "COMPOSE_PROFILE=staging" in inner
assert "STAGING_CONTAINER=orchestrator-staging" in inner
assert "orchestrator-orchestrator-staging" in inner # staging TARGET_IMAGE
assert "--build-staging" in inner
# Hard safety: the prod service/port must NOT leak into the staging rebuild.
assert "TARGET_PORT=8500" not in inner
assert "TARGET_SERVICE=orchestrator " not in inner
def test_tc09_rebuild_staging_image_no_ssh_host_fails_closed(monkeypatch):
"""No ssh host configured -> never-raise, fail-closed (False), no command run."""
import src.image_freshness as imgf
monkeypatch.setattr(imgf, "_ssh_target", lambda: None)
ok, reason = imgf.rebuild_staging_image("orchestrator", "feature/ORCH-058", "abc123")
assert ok is False
assert "ssh host" in reason