- TypeScript 98.9%
- Shell 1.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Dedupe check / dedupe-check (push) Successful in 33s
|
||
| .forgejo/workflows | ||
| .githooks | ||
| .vscode | ||
| dist | ||
| scripts | ||
| src | ||
| types | ||
| .gitattributes | ||
| .gitignore | ||
| .node-version | ||
| .oxfmtrc.json | ||
| .review-enrolled | ||
| action.yml | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| knip.json | ||
| oxlint.config.ts | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.app.json | ||
| tsconfig.base.json | ||
| tsconfig.json | ||
| tsconfig.scripts.json | ||
| tsconfig.test.json | ||
| vitest.config.ts | ||
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_targetruns, so the protection is in the code. A fork run returns at the fork gate without opening a review session, so it never readsREVIEW_CAPABILITY_TOKEN. - The capability token is read in exactly one module with zero internal imports
(
src/credentials.ts) and flows only into theAuthorizationheader of requests toREVIEW_SERVICE_URL. That variable is parsed as anhttps:URL before the token is read, and pinningexpected-service-originin 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
AccessModeReadon 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 thefork-skipsummary 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.