Repository navigation
feat(actions): extract the Claude lane composite trio (PR-A1) #274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f63a6b3
docs(claude-review-lanes): add approved implementation plan
kyle-sexton 8e5fef1
feat(actionlint): suppress the concurrency-queue syntax-check false p…
kyle-sexton e3438f8
feat(actions): extract the Claude lane composite trio (freshness, out…
kyle-sexton 07e76fe
docs(claude-review-lanes): record verifier addenda and fix markdownli…
kyle-sexton a968a83
fix(actions): reject an empty or malformed marker in claude-lane-mark…
kyle-sexton File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # actionlint 1.7.12 (current pin) rejects the GA `concurrency.queue` key | ||
| # (upstream gap: https://github.com/rhysd/actionlint/issues/654). GitHub | ||
| # accepts and honors the key at both workflow and job level — verified by | ||
| # live probe runs recorded in docs/topics/claude-review-lanes/PLAN.md | ||
| # (Phase 0). Suppression approved 2026-07-26 (PLAN.md Approval record, | ||
| # item 3), scoped to exactly that one syntax-check message. | ||
| # | ||
| # REMOVAL TRIGGER: delete this ignore (and this file, if nothing else has | ||
| # accrued) when rhysd/actionlint#654 ships in the pinned actionlint version. | ||
| paths: | ||
| .github/workflows/**/*.yml: | ||
| ignore: | ||
| - 'unexpected key "queue" for "concurrency" section' | ||
| .github/workflows/**/*.yaml: | ||
| ignore: | ||
| - 'unexpected key "queue" for "concurrency" section' |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| name: claude-lane-freshness | ||
| description: >- | ||
| Live freshness guard for the Claude lanes. A stale run must retire itself, | ||
| not rely on being cancelled: with a per-head concurrency key, nothing | ||
| cancels a run whose head was superseded while it sat in a queue. Asks the | ||
| live API whether this event's head is still the PR's head; when it is not, | ||
| a newer run owns the review, so every runner-consuming step downstream | ||
| should skip (posting feedback for an obsolete head is worse than doing | ||
| nothing, and the newer run also owns the marker-comment lifecycle). Fails | ||
| open — `!= 'true'` step gates treat a missing output as fresh, and only a | ||
| response shaped like a commit SHA may retire the run (a degenerate success | ||
| such as a literal `null` must not) — so an API anomaly degrades to | ||
| reviewing a possibly-stale head, never to silence. actions/github-script | ||
| rather than a `gh api` run step: the calling reusable runs on whatever | ||
| runner its caller selected, and a self-hosted image is not guaranteed to | ||
| ship the gh CLI (same pattern as do-not-merge-gate.yml). | ||
|
|
||
| outputs: | ||
| superseded: | ||
| description: >- | ||
| "true" when the PR head moved past this event's head, "false" when the | ||
| head is current or the check could not be made, empty when the step | ||
| skipped (no PR in the event). Gate downstream steps with `!= 'true'`. | ||
| value: ${{ steps.freshness.outputs.superseded }} | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Check whether this head is still current | ||
| id: freshness | ||
| if: github.event.pull_request.number != '' | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| env: | ||
| EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | ||
| with: | ||
| script: | | ||
| let liveHead = ""; | ||
| try { | ||
| const { data: pr } = await github.rest.pulls.get({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: context.payload.pull_request.number, | ||
| }); | ||
| liveHead = pr.head.sha; | ||
| } catch (error) { | ||
| core.warning(`Could not refetch the PR head; treating this run as fresh: ${error.message}`); | ||
| } | ||
| const eventHeadSha = process.env.EVENT_HEAD_SHA ?? ""; | ||
| if (/^[0-9a-f]{40}$/.test(liveHead) && liveHead !== eventHeadSha) { | ||
| core.notice(`Skipping review: the PR head moved from ${eventHeadSha} to ${liveHead}, so a newer run owns the review.`); | ||
| core.setOutput("superseded", "true"); | ||
| } else { | ||
| core.setOutput("superseded", "false"); | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| name: claude-lane-marker-comment | ||
| description: >- | ||
| Marker-comment lifecycle for the Claude lanes. In post-failure mode it | ||
| upserts a warning comment on the PR identifying an infrastructure failure; | ||
| in clear-on-success mode it deletes a stale warning left by an earlier | ||
| failed run. Targets the marker comment by ID (never --edit-last, which | ||
| edits the PR's last comment from ANY author) so a stuck placeholder comment | ||
| from track_progress is never mistaken for "no findings" and an unrelated | ||
| bot comment is never clobbered. Owns the fork-PR guard: fork-triggered | ||
| pull_request runs get a read-only GITHUB_TOKEN and no secrets by design, so | ||
| commenting would just fail there — the action no-ops instead. Runs on the | ||
| github-script Node runtime — no gh CLI or other runner-image dependency. | ||
| Reporting an outage must never become one: callers should set | ||
| continue-on-error on the step invoking this action so a rejected API call | ||
| never turns the lane red. | ||
|
|
||
| inputs: | ||
| marker: | ||
| description: >- | ||
| HTML-comment marker identifying this lane's status comment, e.g. | ||
| "<!-- claude-review-infra-status -->". | ||
| required: true | ||
| mode: | ||
| description: '"post-failure" to upsert the warning, "clear-on-success" to delete it.' | ||
| required: true | ||
| headline: | ||
| description: >- | ||
| Bold headline for the warning body (post-failure mode), e.g. | ||
| "Automated review did not complete". | ||
| required: false | ||
| default: "Automated review did not complete" | ||
| failure-class: | ||
| description: "The failure class token to show (post-failure mode)." | ||
| required: false | ||
| default: "" | ||
| review-detail: | ||
| description: "The safe SDK-result projection to show (post-failure mode)." | ||
| required: false | ||
| default: "" | ||
| body-copy: | ||
| description: >- | ||
| Trailing guidance line of the warning body (post-failure mode). | ||
| required: false | ||
| default: "Re-running the job, or pushing a new commit, will retry the review." | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Manage lane marker comment | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| env: | ||
| MARKER: ${{ inputs.marker }} | ||
| MODE: ${{ inputs.mode }} | ||
| HEADLINE: ${{ inputs.headline }} | ||
| REVIEW_CLASS: ${{ inputs.failure-class }} | ||
| REVIEW_DETAIL: ${{ inputs.review-detail }} | ||
| BODY_COPY: ${{ inputs.body-copy }} | ||
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | ||
| with: | ||
| script: | | ||
| const marker = process.env.MARKER; | ||
| const mode = process.env.MODE; | ||
| if (!/^<!--.+-->$/.test(marker ?? "")) { | ||
| core.setFailed( | ||
| `Marker must be a nonempty HTML comment like "<!-- lane-status -->" (got "${marker}") — ` + | ||
| "an empty or malformed marker would match unrelated bot comments.", | ||
| ); | ||
| return; | ||
| } | ||
| if (mode !== "post-failure" && mode !== "clear-on-success") { | ||
| core.setFailed(`Unknown mode "${mode}" — expected post-failure or clear-on-success.`); | ||
| return; | ||
| } | ||
|
|
||
| const pr = context.payload.pull_request; | ||
| if (!pr?.number) { | ||
| core.info("No pull request in this event; nothing to do."); | ||
| return; | ||
| } | ||
| if (pr.head?.repo?.full_name !== `${context.repo.owner}/${context.repo.repo}`) { | ||
| core.info("Fork-triggered run has a read-only token; skipping comment management."); | ||
| return; | ||
| } | ||
|
|
||
| const comments = await github.paginate(github.rest.issues.listComments, { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: pr.number, | ||
| }); | ||
| const marked = comments.filter( | ||
| (comment) => comment.user?.login === "github-actions[bot]" && comment.body?.startsWith(marker), | ||
| ); | ||
| const existing = marked.length > 0 ? marked[marked.length - 1] : undefined; | ||
|
|
||
| if (mode === "clear-on-success") { | ||
| if (existing) { | ||
| await github.rest.issues.deleteComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: existing.id, | ||
| }); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const body = [ | ||
| marker, | ||
| "> [!WARNING]", | ||
| `> **${process.env.HEADLINE}** — this is an infrastructure failure, not a review verdict.`, | ||
| ">", | ||
| "> Treat any Claude comment on this PR (including a placeholder like \"I'll analyze this and get back to you\") as incomplete, not \"no findings.\"", | ||
| ">", | ||
| `> - Run: ${process.env.RUN_URL}`, | ||
| `> - Failure class: \`${process.env.REVIEW_CLASS}\``, | ||
| `> - Last SDK result: \`${process.env.REVIEW_DETAIL}\``, | ||
| ">", | ||
| `> ${process.env.BODY_COPY}`, | ||
| ].join("\n"); | ||
|
|
||
| if (existing) { | ||
| await github.rest.issues.updateComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: existing.id, | ||
| body, | ||
| }); | ||
| } else { | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: pr.number, | ||
| body, | ||
| }); | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| name: claude-lane-outcome | ||
| description: >- | ||
| Outcome reporter for the Claude lanes. On a successful action outcome it | ||
| records review-failed=false and stops. On any other outcome it projects the | ||
| last SDK result message from the execution file into safe, structured | ||
| metadata (classify.cjs — the sole reader of the file's free-text fields; | ||
| see its header for the projection and classification contract), emits the | ||
| machine-readable `class=<token>` ::error annotation the downstream | ||
| aggregator polls for, and records the failure class and detail for the | ||
| marker-comment step. Runs entirely on the github-script Node runtime — no | ||
| jq or other runner-image tool dependency. | ||
|
|
||
| inputs: | ||
| outcome: | ||
| description: >- | ||
| The Claude action step's `outcome` (e.g. steps.claude-review.outcome). | ||
| Anything other than "success" is treated as an infrastructure failure. | ||
| required: true | ||
| execution-file: | ||
| description: >- | ||
| The Claude action step's execution_file output. May be unset, missing, | ||
| or unparsable — all degrade to failure class `other`. | ||
| required: false | ||
| default: "" | ||
| lane: | ||
| description: >- | ||
| Human-readable lane name used in the annotation text, e.g. | ||
| "Claude review" or "Claude security review". | ||
| required: true | ||
|
|
||
| outputs: | ||
| review-failed: | ||
| description: '"true" on a genuine infrastructure failure, else "false".' | ||
| value: ${{ steps.outcome.outputs.review_failed }} | ||
| failure-class: | ||
| description: "auth | rate-limit | overloaded | other (empty on success)." | ||
| value: ${{ steps.outcome.outputs.failure_class }} | ||
| review-detail: | ||
| description: >- | ||
| Compact JSON projection of the last SDK result message (safe fields | ||
| only), or a parenthesized placeholder string (empty on success). | ||
| value: ${{ steps.outcome.outputs.review_detail }} | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Report review outcome | ||
| id: outcome | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| env: | ||
| LANE_OUTCOME: ${{ inputs.outcome }} | ||
| EXECUTION_FILE: ${{ inputs.execution-file }} | ||
| LANE_NAME: ${{ inputs.lane }} | ||
| ACTION_PATH: ${{ github.action_path }} | ||
| with: | ||
| script: | | ||
| const path = require("node:path"); | ||
| const { classifyExecutionFile } = require( | ||
| path.join(process.env.ACTION_PATH, "classify.cjs"), | ||
| ); | ||
|
|
||
| const outcome = process.env.LANE_OUTCOME ?? ""; | ||
| const lane = process.env.LANE_NAME ?? "Claude review"; | ||
|
|
||
| if (outcome === "success") { | ||
| core.info(`${lane} completed. Findings (if any) are in the PR comments.`); | ||
| core.setOutput("review_failed", "false"); | ||
| return; | ||
| } | ||
|
|
||
| const { reviewDetail, failureClass } = classifyExecutionFile( | ||
| process.env.EXECUTION_FILE ?? "", | ||
| ); | ||
|
|
||
| // `class=<token>` is a machine-readable term, not prose: this | ||
| // annotation is the detection surface a receiver polls through the | ||
| // check-run API, so keep it bare and greppable. | ||
| core.error( | ||
| `${lane} exited with: ${outcome} class=${failureClass} ` + | ||
| "(infrastructure error, not a code-quality signal — e.g. usage limit, " + | ||
| "OIDC failure, SDK crash, is_error result, or max-turns exhaustion). " + | ||
| `Last SDK result: ${reviewDetail}`, | ||
| ); | ||
|
|
||
| core.setOutput("failure_class", failureClass); | ||
| core.setOutput("review_detail", reviewDetail); | ||
| core.setOutput("review_failed", "true"); |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.