From 2816c521c8ae7e35f65f31164328b165264b00c9 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 20 Jun 2026 06:08:26 +0000 Subject: [PATCH] =?UTF-8?q?ci(hcg):=20wire=20surface-drift=20check=20into?= =?UTF-8?q?=20Actions=20(standards#100=20=C2=A71.5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #228 landed `scripts/hcg-surface-drift-check.sh` and explicitly flagged CI wiring as the follow-up step ("a CI wiring PR should follow the always-trigger + changes-job pattern. Out of scope here"). This is that follow-up. The new workflow `.github/workflows/hcg-surface-drift.yml` follows the boj-server pattern in `docs/wikis/CI-and-Required-Checks.adoc` and `.claude/CLAUDE.md` §"CI / Required Status Checks": * No `on.*.paths` — the check is always created so it can never be a path-filtered required gate that strands a PR (the failure mode #213/#215 hit and #216 fixed). * Always-run `changes` job recomputes the relevant path set (router, policy, drift script, the workflow file itself) via `git diff origin/...HEAD`, defaulting to `run=true`. * Heavy `check` job is `needs: changes` + `if: needs.changes.outputs.run == 'true'`; a skip reports SUCCESS to any future required-context list. This converts the ADR's "policy lagging the surface" risk (the largest declared in `docs/decisions/0004-adopt-http-capability- gateway.md`) from a manual re-verification stamp into a per-PR gate, advancing Phase E §1.5 ("Gateway-side prerequisites"). Co-Authored-By: Claude Opus 4.7 Claude-Session: https://claude.ai/code/session_019cKmxx6AkNjzhXT6ZoxGfx --- .github/workflows/hcg-surface-drift.yml | 98 +++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .github/workflows/hcg-surface-drift.yml diff --git a/.github/workflows/hcg-surface-drift.yml b/.github/workflows/hcg-surface-drift.yml new file mode 100644 index 00000000..cbfd55e4 --- /dev/null +++ b/.github/workflows/hcg-surface-drift.yml @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# HCG Surface Drift Gate (standards#100 / standards#91 — Phase E §1.5) +# +# Runs `scripts/hcg-surface-drift-check.sh` to assert every wired +# `BojRest.Router` route is covered by at least one rule in the HCG live +# Verb Governance Spec (`config/gateway-policy-boj.yaml`). The ADR's +# largest declared risk is "policy lagging the surface" — a wired route +# landing without a matching policy rule would default-deny in +# production (an outage on a route that should be live). The §1.5 +# pre-rollout checklist relied on a manual re-verification stamp in the +# live policy header; PR #228 made the check executable; this workflow +# makes it part of every PR build so the risk is gated at merge time. +# +# Bracket-style relationship with `scripts/hcg-policy-smoke.sh`: +# * Smoke script runs against a live gateway (out of CI's scope here). +# * Drift check runs against the source files — fits inside an +# ordinary GitHub Actions job. +# +# Follows the boj-server "always-trigger + changes job" pattern +# documented in `docs/wikis/CI-and-Required-Checks.adoc` and +# `.claude/CLAUDE.md` §"CI / Required Status Checks": no `on.*.paths` +# so the check is always created, with a lightweight `changes` job +# computing relevance and the heavy `check` job gated on it. A skipped +# `check` is reported as success to any future required-context list. + +name: HCG Surface Drift Gate + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: hcg-surface-drift-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + changes: + name: Detect relevant changes + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + run: ${{ steps.detect.outputs.run }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - id: detect + env: + EVENT: ${{ github.event_name }} + BASE: ${{ github.base_ref }} + BEFORE: ${{ github.event.before }} + run: | + # Fail-safe: default to running; only skip on a SUCCESSFUL diff + # showing nothing in this gate's path set changed. The path set + # is the script's three inputs (router, policy, script itself) + # plus the workflow file — any edit to one of those four must + # re-prove the surface⊆policy invariant. + set -uo pipefail + run=true + RE='^elixir/lib/boj_rest/router\.ex$|^config/gateway-policy-boj\.yaml$|^scripts/hcg-surface-drift-check\.sh$|^\.github/workflows/hcg-surface-drift\.yml$' + if [ "$EVENT" = pull_request ]; then + git fetch --no-tags --depth=200 origin "$BASE" 2>/dev/null \ + && changed=$(git diff --name-only "origin/${BASE}...HEAD" 2>/dev/null) \ + && { printf '%s\n' "$changed" | grep -qE "$RE" && run=true || run=false; } + elif [ "$EVENT" = push ] && [ -n "$BEFORE" ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ]; then + changed=$(git diff --name-only "${BEFORE}...${GITHUB_SHA}" 2>/dev/null) \ + && { printf '%s\n' "$changed" | grep -qE "$RE" && run=true || run=false; } + fi + printf 'run=%s\n' "$run" >> "$GITHUB_OUTPUT" + echo "relevant=$run; changed files:"; printf '%s\n' "${changed:-}" + + check: + name: Surface ⊆ policy + needs: changes + if: needs.changes.outputs.run == 'true' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Confirm script + inputs are present + run: | + set -euo pipefail + test -f scripts/hcg-surface-drift-check.sh + test -f elixir/lib/boj_rest/router.ex + test -f config/gateway-policy-boj.yaml + - name: Run hcg-surface-drift-check.sh (verbose) + # The script in PR #228 was committed as 0644 (not executable); + # invoke it via bash so this works regardless of the file mode + # — matches the PR #228 test plan exactly. + run: bash scripts/hcg-surface-drift-check.sh -v