Skip to content

feat!: convert reusable workflows to composite actions - #2

Merged
kyle-sexton merged 1 commit into
mainfrom
feat/composite-actions
Jun 22, 2026
Merged

feat!: convert reusable workflows to composite actions#2
kyle-sexton merged 1 commit into
mainfrom
feat/composite-actions

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

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

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>
@kyle-sexton
kyle-sexton merged commit 0024579 into main Jun 22, 2026
7 checks passed
@kyle-sexton
kyle-sexton deleted the feat/composite-actions branch June 22, 2026 18:55
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant