diff --git a/.github/workflows/phase-gate.yml b/.github/workflows/phase-gate.yml new file mode 100644 index 00000000..85da7d08 --- /dev/null +++ b/.github/workflows/phase-gate.yml @@ -0,0 +1,128 @@ +name: Phase Gate + +# Runs `phase-gate.py` from the repo-factory skill against this repo's live +# GitHub state and fails the build when a phase's conditions are not met. +# +# The script itself has always exited non-zero on failure; the gap this +# workflow closes is that it previously could not run anywhere but the +# operator's own machine (`~/.claude/skills/repo-factory/scripts/`), which no +# CI runner can reach — recorded in docs/ROADMAP-TO-DONE.md's Goal 3 table. +# See github.com/MongLong0214/repo-factory-gate for the published, pinned +# copy this workflow fetches, and that repo's README for why it is fetched +# by commit SHA + sha256 rather than vendored into this repo (vendoring would +# create a second copy of the gate that can answer the same question +# differently than gitseed's copy — the exact class of defect ADR/issue +# history here already closed repeatedly for CommitLore's own trust logic). +# +# Trigger design (deliberately not "every push"): Phase 5's own condition — +# zero open non-Backlog issues — is false for most of an active wave by +# construction (planned work sits open until closed; #52 is open right now +# for exactly that reason). Gating it on every commit to `dev` would keep +# this repo's CI red through ordinary, unremarkable development and train +# reviewers to ignore it — the "brittle gate" failure `github_checks()` +# itself was written to avoid (see its own comment on exact vs. prefix +# matching for the Backlog milestone). Phase 4 and 5 are therefore on-demand +# (`workflow_dispatch`), asked at the moment someone actually wants the +# phase-completion answer, exactly as `references/self-improvement-loop.md` +# describes using the gate. Phase 6 (ship) is different: once its conditions +# are met they stay met (the tag stays reachable from `main`/`dev`, CI at +# that commit does not un-succeed), so it is safe — and useful — to also run +# automatically on every release tag push and on every PR into `dev` (a +# cheap, non-brittle continuous check that the shipped release stays intact). + +on: + push: + tags: + - "v*.*.*" + pull_request: + branches: + - dev + workflow_dispatch: + inputs: + phase: + description: "repo-factory phase to check (4 = repo genesis, 5 = wave execution, 6 = ship)" + required: true + type: choice + options: + - "4" + - "5" + - "6" + default: "5" + release_tag: + description: "Phase 6 only — override which release tag to check (default: latest annotated vX.Y.Z reachable from main)" + required: false + type: string + +permissions: + contents: read + issues: read + actions: read + +env: + GATE_MIRROR_REPO: MongLong0214/repo-factory-gate + # Pin by commit SHA, not a branch — bump both together via + # `~/.claude/skills/repo-factory/scripts/publish-gate.sh`, never by editing + # just one. + GATE_SHA: b98ffa3e144c02b17a1da73c84c5fe1b07e3eef1 + GATE_SHA256: bc8987b63fc7a2111e18fb4d08e69bb96083f677e604be2d5c2a316d3bc7a397 + +jobs: + phase-gate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine which phase to check + id: which + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "phase=${{ inputs.phase }}" >> "$GITHUB_OUTPUT" + else + # Tag push (v*.*.*) or a PR into dev: both automatically run the + # Phase 6 (ship) gate — the only phase safe to run unconditionally. + echo "phase=6" >> "$GITHUB_OUTPUT" + fi + + - name: Fetch phase-gate.py (pinned commit SHA, verified by sha256) + run: | + set -euo pipefail + curl -fsSL "https://raw.githubusercontent.com/${GATE_MIRROR_REPO}/${GATE_SHA}/phase-gate.py" -o phase-gate.py + echo "${GATE_SHA256} phase-gate.py" | sha256sum -c - + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - uses: actions/setup-node@v4 + if: steps.which.outputs.phase == '4' + with: + node-version: 22 + + - name: "Phase 4 only: point commitlore.bin/commitlore.node at this checkout's own build" + if: steps.which.outputs.phase == '4' + run: | + set -euo pipefail + test -f "$GITHUB_WORKSPACE/dist/commitlore.mjs" || { echo "::error::dist/commitlore.mjs missing — is dist/ checked in at this ref?"; exit 1; } + git config --local commitlore.bin "$GITHUB_WORKSPACE/dist/commitlore.mjs" + git config --local commitlore.node "$(command -v node)" + + - name: "Phase gate — fails the build (nonzero exit) when the phase's conditions are not met" + env: + # gh needs a token to answer branch/milestone/issue/tag/CI-run + # questions. If this were absent, phase-gate.py does not skip those + # checks — `github_checks()` / `_gh_unavailable_reason()` report each + # of them as an explicit FAIL ("gh CLI 인증 실패"), so a + # misconfigured runner shows up as a red build, not a quiet pass. + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + phase="${{ steps.which.outputs.phase }}" + cmd=(python3 phase-gate.py "$phase" --repo "${{ github.repository }}" --path "$PWD") + release_tag="${{ inputs.release_tag }}" + if [ "$phase" = "6" ] && [ -n "$release_tag" ]; then + cmd+=(--release-tag "$release_tag") + fi + "${cmd[@]}" diff --git a/docs/ROADMAP-TO-DONE.md b/docs/ROADMAP-TO-DONE.md index b9b1f215..7d706216 100644 --- a/docs/ROADMAP-TO-DONE.md +++ b/docs/ROADMAP-TO-DONE.md @@ -128,7 +128,7 @@ Current: SKILL.md (6 phases, 11 invariants) · 9 references · 3 scripts. |---|---| | `create-issues.py` and `verify-citations.py` produce raw tracebacks on directories and unreadable files | Discovery complete (STATUS.md). Fix in a separate ticket — mixing discovery and the fix in one commit lets the fix skip review | | The gate covers **only Phase 4** | 11 invariants were created but applied to only one phase. Phase 5 and 6 are next | -| `phase-gate.py` lives in `~/.claude/skills/`, so it is **absent from the CI runner** | CI use requires vendoring. Nobody is doing it | +| ~~`phase-gate.py` lives in `~/.claude/skills/`, so it is **absent from the CI runner**~~ — **resolved**, pending review | Published to `github.com/MongLong0214/repo-factory-gate`, pinned by commit SHA + sha256. Both this repo and gitseed gained `.github/workflows/phase-gate.yml` (`workflow_dispatch` for Phase 4/5, automatic on `vX.Y.Z` tag push for Phase 6). Vendoring was ruled out — it would create a second copy of the gate free to drift from CommitLore's own, the same defect class #89/#90 closed. Moving the gate into a project was ruled out too — it inverts the dependency. See gitseed#76, commitlore#103, and the PRs each references | ### Completion condition