From c993bdace5b379c8ad906d65a2309c8b15a17f5b Mon Sep 17 00:00:00 2001 From: MongLong0214 Date: Tue, 28 Jul 2026 12:08:09 +0900 Subject: [PATCH 1/2] Wire the repo-factory phase gate into CI phase-gate.py decides Phase 4/5/6 completion by checking local docs plus live GitHub state, and has always exited nonzero on failure -- but it lives at ~/.claude/skills/repo-factory/scripts/, a path no CI runner can reach. docs/ROADMAP-TO-DONE.md (Goal 3) recorded this as outstanding: CI use required vendoring, and nobody was doing it. That made the gate advisory in exactly the place a gate should be binding. Adds .github/workflows/phase-gate.yml: fetches phase-gate.py from a new public mirror (github.com/MongLong0214/repo-factory-gate) pinned by commit SHA + sha256, then runs it with GH_TOKEN set so gh-backed checks answer for real instead of reporting "auth failed" as an explicit FAIL. Phase 4/5 run on workflow_dispatch (on-demand); Phase 6 also runs automatically on every vX.Y.Z tag push, since its conditions stay met once met. Updates the ROADMAP-TO-DONE.md row to record the resolution and what was ruled out. Fixes #103 Ruled-out: vendor phase-gate.py into this repo and gitseed | second copy free to drift from CommitLore's own trust logic -- the defect class #89/#90 closed Ruled-out: move the gate into this repo, have the skill reference it | inverts the dependency -- a project should not own the factory's gate Ruled-out: gate every dev push on Phase 4/5 | Phase 5's own condition (#52 open, non-Backlog) is false mid-wave by construction; trains reviewers to ignore CI Limit: the mirror repo is published-to, not edited directly -- patch the skill, then re-publish Evidence: docs/ROADMAP-TO-DONE.md#goal-3--factory-skill Blast: module Certainty: firm --- .github/workflows/phase-gate.yml | 124 +++++++++++++++++++++++++++++++ docs/ROADMAP-TO-DONE.md | 2 +- 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/phase-gate.yml diff --git a/.github/workflows/phase-gate.yml b/.github/workflows/phase-gate.yml new file mode 100644 index 00000000..1a141081 --- /dev/null +++ b/.github/workflows/phase-gate.yml @@ -0,0 +1,124 @@ +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. + +on: + push: + tags: + - "v*.*.*" + 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*.*.*): the natural, automatic trigger for the + # Phase 6 (ship) gate. + 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 From aa89670801d20b136a604f51e226efd96f093d23 Mon Sep 17 00:00:00 2001 From: MongLong0214 Date: Tue, 28 Jul 2026 12:10:58 +0900 Subject: [PATCH 2/2] Also run the Phase 6 gate automatically on PRs into dev Gives a real, permanent trigger for the ship gate beyond tag pushes -- and, practically, a way to see this workflow actually execute on GitHub's runners without merging to dev first (workflow_dispatch only becomes callable once a workflow file exists on the default branch, so it cannot be exercised from a feature-branch PR by itself). Fixes #103 --- .github/workflows/phase-gate.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/phase-gate.yml b/.github/workflows/phase-gate.yml index 1a141081..85da7d08 100644 --- a/.github/workflows/phase-gate.yml +++ b/.github/workflows/phase-gate.yml @@ -27,12 +27,16 @@ name: Phase Gate # 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. +# 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: @@ -77,8 +81,8 @@ jobs: if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "phase=${{ inputs.phase }}" >> "$GITHUB_OUTPUT" else - # Tag push (v*.*.*): the natural, automatic trigger for the - # Phase 6 (ship) gate. + # 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