CI wrapper action for the review service
  • TypeScript 98.9%
  • Shell 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Łukasz Jerciński 95366cf922
All checks were successful
Dedupe check / dedupe-check (push) Successful in 33s
chore: update fta-check to 1.5.8 (#60)
2026-10-07 05:59:23 +00:00
.forgejo/workflows chore: update review wrapper pin (#50) 2026-10-05 12:47:03 +00:00
.githooks feat: implement the review wrapper action (#4) 2026-08-08 13:47:54 +00:00
.vscode chore: add the align-managed TypeScript project config 2026-08-07 10:29:41 +02:00
dist feat: tell users to wait out a full sandbox fleet (#48) 2026-10-05 11:01:43 +00:00
scripts feat: implement the review wrapper action (#4) 2026-08-08 13:47:54 +00:00
src fix: bundle freshness should handle pnpm store paths with spaces (#59) 2026-10-06 06:25:35 +00:00
types chore: add the package manifest, README, and toolchain scaffold 2026-08-07 10:27:10 +02:00
.gitattributes feat: implement the review wrapper action (#4) 2026-08-08 13:47:54 +00:00
.gitignore chore: add the package manifest, README, and toolchain scaffold 2026-08-07 10:27:10 +02:00
.node-version chore: scaffold the repository via j4k-align 2026-08-05 07:35:52 +02:00
.oxfmtrc.json feat: implement the review wrapper action (#4) 2026-08-08 13:47:54 +00:00
.review-enrolled ci: enroll in the managed review path (#9) 2026-08-28 09:53:11 +00:00
action.yml docs: say where the token goes when the origin pin is off (#41) 2026-10-05 09:34:49 +00:00
AGENTS.md chore: regenerate agent instructions 2026-10-03 07:27:33 +02:00
CLAUDE.md chore: scaffold the repository via j4k-align 2026-08-05 07:35:52 +02:00
knip.json feat: implement the review wrapper action (#4) 2026-08-08 13:47:54 +00:00
oxlint.config.ts feat: implement the review wrapper action (#4) 2026-08-08 13:47:54 +00:00
package.json chore: update fta-check to 1.5.8 (#60) 2026-10-07 05:59:23 +00:00
pnpm-lock.yaml chore: update fta-check to 1.5.8 (#60) 2026-10-07 05:59:23 +00:00
pnpm-workspace.yaml feat: implement the review wrapper action (#4) 2026-08-08 13:47:54 +00:00
README.md feat: tell users to wait out a full sandbox fleet (#48) 2026-10-05 11:01:43 +00:00
tsconfig.app.json chore: add the align-managed TypeScript project config 2026-08-07 10:29:41 +02:00
tsconfig.base.json chore: add the align-managed TypeScript project config 2026-08-07 10:29:41 +02:00
tsconfig.json feat: implement the review wrapper action (#4) 2026-08-08 13:47:54 +00:00
tsconfig.scripts.json feat: implement the review wrapper action (#4) 2026-08-08 13:47:54 +00:00
tsconfig.test.json feat: implement the review wrapper action (#4) 2026-08-08 13:47:54 +00:00
vitest.config.ts feat: implement the review wrapper action (#4) 2026-08-08 13:47:54 +00:00

review-wrapper

The Forgejo Actions wrapper for the j4k review service: the action a repository's CI runs to request a review of a pull request's head, wait for execution and triage to settle, and reconcile the review's findings onto the PR's comment surface. The job's conclusion is the PR's "Review" check. The service itself lives in j4k/review; the wrapper only speaks its HTTP contract, which section 8 of that repository's docs/SPEC.md defines.

The action is a bundled JS action: action.yml declares runs.using: node24 with main: dist/index.mjs, a committed esbuild bundle with no runtime dependencies.

Environment contract

Variable Meaning
REVIEW_SERVICE_URL Base URL of the review service (an org-managed variable). serviceOrigin in src/credentials.ts gives the form it must take; the client appends the /v1 API prefix itself.
REVIEW_CAPABILITY_TOKEN Capability token for the service's /v1 API (an org secret).
FORGEJO_TOKEN The job's own task token, used for every forge write.
GITHUB_API_URL Forge API base, supplied by the runner.
GITHUB_EVENT_*, GITHUB_RUN_ID, … Runner event environment. It identifies the pull request; a workflow_dispatch run fetches the PR's current state from the forge. readWrapperInputs in src/wrapper/event-context.ts derives the PR inputs.
GITHUB_RUN_ATTEMPT Forgejo run attempt (1-based). Required on every path. The wrapper re-triages stalled or failed triage only when this is ≥ 2.

The forge and runner variables are required on every path: forgeCredentials in src/credentials.ts, readWrapperInputs in src/wrapper/event-context.ts and src/main.ts read them before the fork gate. REVIEW_SERVICE_URL and REVIEW_CAPABILITY_TOKEN are required only on the same-repository path: a fork pull request returns at the fork gate without opening a service session, so neither is ever read. Where a variable is required, a missing one is a configuration defect and fails the run immediately rather than sending an empty-string request.

Action inputs

Each action input is declared, with its behaviour, under inputs in action.yml.

Security model

The workflow trigger is pull_request_target, so the base branch's workflow and the pinned action bundle execute — a pull request cannot rewrite the code that reads the secret.

  • The wrapper checks out nothing and executes nothing from the pull request. Its only PR-derived inputs are event-payload strings and read-only forge API data; file bytes fetched for snippet matching are treated as data and never evaluated.
  • Forgejo does pass secrets to fork pull_request_target runs, so the protection is in the code. A fork run returns at the fork gate without opening a review session, so it never reads REVIEW_CAPABILITY_TOKEN.
  • The capability token is read in exactly one module with zero internal imports (src/credentials.ts) and flows only into the Authorization header of requests to REVIEW_SERVICE_URL. That variable is parsed as an https: URL before the token is read, and pinning expected-service-origin in the calling workflow makes a repointed org variable fail the run rather than post the token somewhere else.
  • Forge writes use the job's own task token: write mode on same-repository pull requests and Forgejo AccessModeRead on fork pull requests. That mode permits comments on unlocked issues and pull requests, and edits to comments posted by the same Actions identity. The fork path uses these capabilities only to create or update the fork-skip summary comment.
  • The wrapper adopts a summary comment only when its body begins with <!-- review:summary --> and its author is the Forgejo Actions system user (id -2). This excludes marker comments posted by other users, but does not distinguish the wrapper from other Actions tasks sharing that identity.
  • The wrapper holds no model credentials and never receives the review service's run tokens or triage-pass tokens. Run tokens authorize a lens agent for one review run; triage-pass tokens authorize a triage agent for one triage pass.

Outcomes and exit codes

OUTCOMES in src/contract/types.ts names the outcomes, with a comment beside each that summarizes its behaviour, and OUTCOME_EXIT in the same file maps each outcome to its exit code. When the service session fails, the log line's remedy comes from FAILURE_REMEDIES in src/wrapper/orchestrator.ts. A stuck or partial-coverage run's log line, remedy included, comes from formatStuckRemedy and formatExecutionRecovery in src/wrapper/remedies.ts.

Supersession, a newer dispatch pass becoming latest during the wait, is a flag on WaitResult, not an outcome. It never changes the exit code. Requests that take a pass id keep the selected pass, and the run's other reads return review-wide state; ReviewSession in src/contract/types.ts shows which requests take one.

A head-moved run has already created or attached to the review, selected a dispatch pass, and finished its initial wait. It may also have asked for a dispatch pass or requested re-triage and waited again before detecting the head change.

HTTP 422 problem types that toFailure in src/review/session.ts maps to an outcome receive that outcome's remedy and are not retried. Unclassified service rejections fail with exit 1 and retain the HTTP status, problem type, title, and detail in the job log, including unknown-tree, unknown-commit, and HTTP 413 tree-too-large. Unparseable error responses retain the HTTP status and raw body. A rejection before reconciliation writes no comments.

Rerun semantics

A pull_request_target run (called an event run in job logs) pins the review's latest_pass when one exists; otherwise it asks using GITHUB_RUN_ID as the idempotent ask key. Rerunning that job pins whatever pass is now latest, which can be a different pass if a workflow_dispatch run asked in between.

A workflow_dispatch run always asks using its own GITHUB_RUN_ID. Rerunning that job reuses the same pass when the capability-token identity and head branch are unchanged; otherwise the service returns an ask-key conflict.

An event run also sends attach_same_diff: true with its create request, so the service may return an earlier review of the same code diff instead of a new one, as after a rebase or retarget that leaves the diff unchanged. When the returned review was created for another commit, the job log says so (attached to review <id>, created for commit <sha>; head <sha> has the same diff) and the wrapper reconciles that review onto the current head. A workflow_dispatch run sends false: it attaches by exact identity only, so its ask runs on the current tree.

A thread labelled superseded is final, so it does not count as a posted finding. When a review returns after another review superseded its threads (A, then B, then A again), the inline reconciler posts the returning review's findings as new threads and the disposition projector projects onto those, not the superseded ones. Each finding ends with one thread without the label, plus one superseded thread per return.

On stalled or failed triage, when GITHUB_RUN_ATTEMPT ≥ 2, the wrapper confirms the PR head is unchanged, then asks for a triage pass. Its ask key is ${GITHUB_RUN_ID}:re-triage:${GITHUB_RUN_ATTEMPT} (idempotent for that attempt). A successful request starts another wait on the pinned dispatch pass; latest_pass stays that dispatch pass. If no eligible claims remain, the wrapper reconciles the original stalled or failed result without waiting again. A head move detected before re-triage prevents the triage-pass request. A move detected after the second wait may follow an already-created triage pass. Either detection suppresses PR comment and conversation-resolution writes.

Explicit execution failures use the service's fixed messages, including provider session limit reached. When the service sends a failure code the bundled service package doesn't name, the job log line reads execution failed for an unrecognized reason followed by the JSON-quoted code, (code "…"). Failed triage logs use review did not settle (triage-failed):; failed lens slots include their cause in the partial-coverage diagnostic. A triage with no recorded failure retains the generic stalled message.

Before retrying, follow the remedy printed with the cause on the failure log line. formatExecutionRecovery in src/wrapper/remedies.ts formats it.

Once the cause is resolved, rerun failed triage. For failed lens slots, use the dispatch re-ask command printed in the partial-coverage diagnostic.

Composition

src/main.ts is the only place the modules meet. It reads the forge credentials and GITHUB_REPOSITORY, builds the forge client, derives the run's inputs from the event payload, and hands the orchestrator the collaborators that WrapperDeps in src/contract/types.ts names. The orchestrator's exit code becomes the process exit code, so the job's conclusion follows the outcome's exit code.

The review session arrives as a thunk rather than a value. That is the fork gate in code: reviewCredentials() runs only when the orchestrator opens a session, which the fork path never does, so REVIEW_CAPABILITY_TOKEN is never read on a fork pull request.

Notes

A workflow_dispatch run cannot flip the PR check: Forgejo writes commit statuses only for push and pull-request-family events. Dispatch runs are for manual reconciliation, and only of an open, unmerged pull request — the dispatch arm reads the PR's state from the forge and refuses anything else, including a pull whose state it cannot establish. The calling workflow must declare a pr_number input for workflow_dispatch: the dispatch arm reads the pull request's number from it, and the run fails without it.

Release

dist/index.mjs is committed and must match the sources — pnpm bundle rebuilds it and a test fails when it drifts. To release, rebuild the bundle, commit it, then move the v1 tag to that commit; consumers pin the action at v1.