feat!: convert reusable workflows to composite actions - #2
Merged
Conversation
A reusable workflow cannot run a runner script that lives in this private
repo when called from another private repo: the caller's GITHUB_TOKEN is
repo-scoped, so a second actions/checkout of ci-workflows fails. Composite
actions are provisioned by the Actions service via a scoped, read-only,
one-hour installation token (distinct from GITHUB_TOKEN), so they work
cross-repo PAT-free and their bundled files are reachable via
$GITHUB_ACTION_PATH with no checkout.
Replace .github/workflows/markdown.yml and powershell.yml with composite
actions at .github/actions/{markdown,powershell}; bundle Invoke-Pssa.ps1
alongside the powershell action. Rewrite the dogfood ci.yml to checkout +
local action refs behind the ci-status gateway. Extend Dependabot to scan
the composite actions' own action.yml files (a "/" target does not descend
into .github/actions/<name>/).
BREAKING CHANGE: consumers now reference
melodic-software/ci-workflows/.github/actions/<tool>@<sha> from a job step
(after actions/checkout), not the old reusable-workflow job-level uses:.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 8, 2026
Merged
This was referenced Jul 16, 2026
kyle-sexton
added a commit
that referenced
this pull request
Aug 6, 2026
…NG (#362) Records the Phase 4 acceptance run in `docs/topics/claude-review-lanes/PLAN.md`. Draft, because the ledger it writes asserts that Phase 4 is **not** closeable yet, and that claim should be checked before it lands. ## Why the tag is `[DOING]`, not `[DONE]` The acceptance test ran end to end for the first time. It did what an acceptance test is for — it found three defects and two spec conflicts that every prior green signal had hidden — so SC4 ("#228/#238 closed with pointers") is unmet and both issues stay open. They also carry `needs-human`, which bars autonomous closure independently of the evidence. What is now **done**: the write-path defect is fixed and merged (#359 → `058ed1a`), and the incident lifecycle is re-demonstrated on `main`. What remains is tracked in #363, #364, and two adjudication items recorded on #238. Phase 3's ENUMERATION FIX is the binding precedent here: a phase's close-out must enumerate its DELIVERABLES, not just its checks. The ledger therefore lists the unbuilt ones and dispositions each, so advancing the tag later cannot strand one. ## What acceptance demonstrated All four lane artifacts plus auto-resolve, on real lane output: | Artifact | Evidence | | --- | --- | | Green check on a dead credential | sandbox run [31079823199](https://github.com/melodic-software/claude-lane-sandbox/actions/runs/31079823199) — `review / review` concluded `success` | | Marker comment | posted on claude-lane-sandbox#2, carrying `Failure class: auth` | | `class=auth` annotation | `api_error_status: 401` in the check-run annotation | | Incident opens | #361, via aggregator run [31082662762](https://github.com/melodic-software/ci-workflows/actions/runs/31082662762) | | Auto-resolve | runs [31083255896](https://github.com/melodic-software/ci-workflows/actions/runs/31083255896), [31083299991](https://github.com/melodic-software/ci-workflows/actions/runs/31083299991) (`action=update`), [31083359365](https://github.com/melodic-software/ci-workflows/actions/runs/31083359365) (`action=close`) — each `read-errors=0 cycle=clean coverage=complete` | The runbook's one empirically unverified choice — the bad-token VALUE — is now confirmed: the SDK reached the API and was rejected 401, yielding `class=auth` rather than degrading to the non-escalating `other`. ### Re-demonstrated on `main`, because the first pass only proved a branch Every incident write in the table above ran on the unmerged branch of #359 — `main` could not write an incident at all. #359 has since merged as `058ed1a`, and the whole cycle was re-run on `main`: | Run (ref `main`) | Deliverable line | Outcome | | --- | --- | --- | | [31095551306](https://github.com/melodic-software/ci-workflows/actions/runs/31095551306) | `read-errors=0 cycle=incident coverage=complete action=open` | incident #365 opened | | [31096144924](https://github.com/melodic-software/ci-workflows/actions/runs/31096144924) | `read-errors=0 cycle=clean coverage=complete action=update` | clean 1 | | [31096193502](https://github.com/melodic-software/ci-workflows/actions/runs/31096193502) | `read-errors=0 cycle=clean coverage=complete action=update` | clean 2 | | [31096244305](https://github.com/melodic-software/ci-workflows/actions/runs/31096244305) | `read-errors=0 cycle=clean coverage=complete action=close` | #365 closed, `cleanCycles: 3`, recovery comment | So the acceptance evidence now covers the shipped product. Teardown is clean: no repo-level override remains, zero incidents open, and the org secret's `updated_at` never moved from `2026-08-05T13:32:31Z` across either pass. ## The three defects **1. The write path had never once executed** — fixed and merged (#359). The poll renders the incident body to the dot-prefixed `.claude-lane-incident.md`, and `upload-artifact` ignores hidden files by default, so the upload collected nothing and `if-no-files-found: error` failed the step. That is provable rather than sampled, and the corpus is pinned by **code state** rather than a run count so it does not drift as the schedule keeps firing. While `main` carried the pre-fix code, any cycle reaching `action != none` **must** have failed at this upload. Every `main` run before `058ed1a` succeeded except the dispatch that forced this incident — therefore every one of those reported `action=none`, and the write path was never exercised. Runs at or after `058ed1a` sit outside that corpus by construction: the four `main` re-demo runs above report `action != none` **and** succeed, which is the fix working, not a counterexample. (Dated observation, deliberately not load-bearing: 143 such pre-fix `main` runs as of 2026-08-06.) Each pre-fix run emitted the `read-errors=0 cycle=clean` line PLAN identifies as the antidote to the `≤ 1` ceiling check, while the write path could not fire. **2. Lane routing was never implemented** — tracked in #364. #238's Contract requires the incident issue to carry the human-gated role label plus a `kind=routed-advisory` escalation-marker comment. #361 and #365 carried neither. Neither `needs-human` (the role label both #228 and #238 themselves wear) nor `routed-advisory` appears anywhere in this repository, on either branch — so this is unbuilt, not misconfigured. The label is applied inside the write-gate's byte-pinned region, so this PR does not patch it. The operational point: an `auth` incident inherently needs a human at the provider layer, and the issue does not wear the label that routes it to one. **3. A validation skip is a silent no-review the aggregator cannot see** — tracked in #363. When `claude-code-action` skips itself on workflow validation it exits 0, the check concludes green, nothing is reviewed, and **no `class=` token is emitted**. The aggregator's whole detection mechanism is that token, so this failure mode is invisible to it by construction. That is the #228 harm class, uncovered. The same issue records two lesser findings: a review-count comment that counted a review which never happened, and marker copy falsely asserting a push does not re-trigger the lane. ## Two spec-vs-implementation conflicts, both recorded on #238 for adjudication Neither is patched here — which side is wrong is a judgement call, and #238's Contract is ratified. **The reopen criterion.** #238's third acceptance criterion says "a second incident reopens the same marker-selected issue (no duplicate)". The aggregator deliberately does not do that; its lookup queries `state: "open"` only and says so: > Open issues only. A closed incident is superseded by a fresh one rather than reopened, so this never paginates closed history. `nextState`'s only actions are `open`/`update`/`close`/`none` — no `reopen` exists — and a test pins the opposite behavior explicitly. So a second episode opens a **new** issue. The "no duplicate" half still holds (never more than one *open* incident, which is what SC3 ceilings), but "reopens the same issue" is contradicted by design, not merely unexercised. **The close condition.** #238's Contract says the incident closes on the "first window whose review runs include a success and no `auth` class". This phase specifies — and the code implements — three consecutive clean cycles. The stricter shape ran, so nothing is broken, but two ratified authorities disagree and #238's wording is the stale one. ## The sixth bullet's two downstream actions, now performed Phase 4's sixth bullet requires more than closing #228/#238 — it also requires comment-closing claude-code-plugins#1327 with root cause and a pointer, and commenting provisioning#215 as folded into the taxonomy. Both were un-dispositioned; both are done, and both stay **open**. - **claude-code-plugins#1327** — commented, **not** closed. It carries `needs-human`, which bars autonomous resolution. The comment states the half that is easy to gloss: that signature classifies as `other`, which is **non-escalating by design**, so detection is proven while no incident opens. Whether the instant-fail signature deserves its own escalating class is exactly the human decision left on it. - **provisioning#215** — commented, folded into the taxonomy, state untouched. Same honesty: `class=runner` is an escalating class in the taxonomy, and **nothing in production emits it** — the only occurrences are the aggregator's unit tests. So that substrate-silence would still be silent today. Unpark trigger: caller-side selector-failure emission shipping. ## The canary property's four targets, each with a verdict The canary bullet names four coverage targets, and its headline ("asserts on REAL lane output") **is** satisfied — which is exactly why a silent three-quarters made the whole bullet read as covered. Review caught that; each now carries its own verdict in the ledger. | Target | Verdict | | --- | --- | | upstream #1501 silent-green + seat/credential death | **covered** — the round's strongest result; one variant (no class token at all) is uncovered and is defect 3 | | claude-code-plugins#1327 SDK instant-fail | **detected, not escalating** — classifies as `other` by design, so no incident opens | | provisioning#215 runner mismatch | **not exercised** — `class=runner` is escalating in the taxonomy but nothing in production emits it | | caller-side `class=runner` selector-failure marker (3a) | **not shipped** — it is the emission the row above needs, so both unblock together | ## Known coverage gaps, recorded rather than claimed - **The multi-repo shape was never exercised.** #238's first criterion describes auth-class annotations "across multiple consumer repos in one window" — the #1122 replay. Acceptance drove one repo (`repositoriesSeen: 1`). - **The hysteresis is not a soak.** The three clean cycles completed in roughly 92 seconds. It is counter arithmetic — three *cycles* however fast they arrive — so auto-close proves recovery was observed three times, never that it held for any duration. Correct per spec; recorded so no one infers a soak test. ## Also recorded - **Runbook Step 1 is wrong as written.** It expects the wiring PR's own run to be "green and genuinely reviewed", which is structurally impossible on the PR that introduces the caller — claude-code-action validates the workflow against the default branch and skips. The baseline moved post-merge, before any credential was broken, which is what makes the later `class=auth` unambiguous instead of confoundable with a wiring gap. - **Canary deferral, with a dated trigger** (re-evaluate 2026-11-06). The rationale is stronger than #238's original: a workflow-validation skip is green, reviews nothing, and emits **no** `class=` token at all, so an annotation-based aggregator is structurally incapable of seeing it. A synthetic canary is the only proposed mechanism that would. - **The org secret visibility flip is still pending and blocked nothing.** Under `visibility: all` the sandbox reads the org credential and the selected-repositories list is legitimately empty — the runbook's own Step 0 disposition. Recorded as a hardening item, not an acceptance gate. The secret was never edited or re-scoped: `updated_at` stayed `2026-08-05T13:32:31Z` throughout, and the forced failure came from a repo-level override that was set and then deleted. ## Sequencing This PR only records state; #359 is already merged, so the ledger's narrative is true on `main` as written. Do not advance the Phase 4 tag on the strength of this PR — the ledger's own "REMAINING TO CLOSE PHASE 4" list is what gates that, and it still names #363, #364, the two #238 adjudications, and the multi-repo gap. It also corrects PLAN's own documented deliverable-line format against the shipped emitter: `cycle` carries a third value `incident`, and there is a `coverage=` field the documented line never had. That omission mattered — `incident` is the value that says the watchdog fired, so the documented enum was missing the one outcome that matters. No linked issue — this PR records evidence and closes nothing. #228 and #238 remain open by design, for the reasons the ledger states. ## Related - #359 — fixed the write-path defect this run uncovered; merged as `058ed1a`. - #367 — hardened the same round trip after review; merged as `fe1b880`. - #363 — the validation-skip silent no-review, plus the false review count and false marker copy. - #364 — the unimplemented lane routing. - #238 — the aggregator this phase consumes; carries the two adjudication items as a comment. - #228 — the parent dead-credential issue; the canary deferral's only anchor while it stays open. - #349 — carries the sandbox wiring bundle and acceptance runbook. - #365 — the incident opened and auto-closed by the `main` re-demonstration. - #361 — the incident issue that opened and then auto-closed. - melodic-software/claude-lane-sandbox#1, #2 — the wiring and probe pull requests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A reusable workflow cannot run a runner script that lives in this private
repo when called from another private repo: the caller's GITHUB_TOKEN is
repo-scoped, so a second actions/checkout of ci-workflows fails. Composite
actions are provisioned by the Actions service via a scoped, read-only,
one-hour installation token (distinct from GITHUB_TOKEN), so they work
cross-repo PAT-free and their bundled files are reachable via
$GITHUB_ACTION_PATH with no checkout.
Replace .github/workflows/markdown.yml and powershell.yml with composite
actions at .github/actions/{markdown,powershell}; bundle Invoke-Pssa.ps1
alongside the powershell action. Rewrite the dogfood ci.yml to checkout +
local action refs behind the ci-status gateway. Extend Dependabot to scan
the composite actions' own action.yml files (a "/" target does not descend
into .github/actions//).
BREAKING CHANGE: consumers now reference
melodic-software/ci-workflows/.github/actions/@ from a job step
(after actions/checkout), not the old reusable-workflow job-level uses:.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com