A pull from a fork gets its check: the gate fetches refs/pull/<n>/head when the head is not on a branch, and the preview skips forks #40

Merged
grokbot merged 1 commit from grokbot/forge:fix/ci-fork-pr-checkout into main 2026-10-17 19:28:15 +00:00
Owner

check.yml cloned this repository and checked out GITHUB_SHA. A fork's head
commit is here only as refs/pull//head, which a clone does not fetch, so
every gate shard of a fork pull died at "clone source at this ref" with
"fatal: reference is not a tree" before running a test (PR #39, run 893).

preview.yml cloned --branch "$GITHUB_HEAD_REF", a branch that exists only in
the fork; a fork pull gets no secrets either, and a preview builds and pushes
an image of the pull's code, so it now skips pulls from forks.

Why now

#39 (#39) is the first pull from a fork, and it is blocked by this: all three gate shards fail at "clone source at this ref" with fatal: reference is not a tree: cf8de4df… (run 893), and preview fails with Remote branch fix/2171-flow-unused-imports not found in upstream origin (run 894). No test ran.

Security review

With this change, a fork pull's own code runs in check.yml (bun install, scripts/test-shards.ts, the tests). What that code can reach:

  • Secrets: none. check.yml references no secrets.*. Forgejo also leaves secrets empty for a pull_request whose head is a fork.
  • Token: read-only. The only credential is github.token, scoped to the "clone source" step env and embedded in src/.git/config by the clone. Fork code in later steps can read it from there, and the runner hands job tokens to steps in other ways too, so scrubbing the URL would not be a real boundary. What matters is Forgejo's rule that a pull_request run's automatic token has no write permission (read-only for forks). It can read this public repository, which anyone can already do.
  • No push, no publish. Neither gate nor check pushes git, builds or pushes images or charts, signs anything, or calls a write API (/ci/preview and the registry appear only in preview.yml and release.yml). No cache is shared with privileged workflows, so nothing can be poisoned.
  • Trigger is pull_request, not pull_request_target. The fork's code never runs with base-repository privileges. Fork pulls from users without elevated rights still need a maintainer's approval before Actions run them (Forgejo default).
  • The fetch adds no capability. refs/pull/<n>/head is fetched only when $GITHUB_SHA is not already in the clone (fork pulls). It is the same commit the event names; nothing else changes for pushes to main or same-repository pulls.
  • Preview stays off for forks. It needs REGISTRY_TOKEN/cosign secrets (empty for forks anyway) and builds and pushes an image of the pull's code, so the job is skipped when head.repo.full_name != github.repository. Teardown is skipped too; there is nothing to tear down.

Conclusion: no secret or write token reaches fork code in check.yml, and its jobs cannot push or publish. No extra gating needed beyond the preview skip.

Known baseline on main (not this change)

test/og-png.test.ts "median of three cold renders is within 300 ms" failed main's push gate shard 2 (run 889: 616 ms / 708 ms on the half-core, 512 MiB runner). Pulls whose fast tier includes it (e.g. #39) may hit it too.

Validation

pipeline/validate on a9797560: pass in 6s — bun 1.3.14, CI-shaped env (scrubbed vars, runner PATH), exact check.yml shard + forge.json commands.

step result time
bun install --frozen-lockfile ok (exit 0) 0s
forge.json check.fast ok (exit 0) 3s
check.yml gate (1) pull_request ok (exit 0) 0s
check.yml gate (2) pull_request ok (exit 0) 0s
check.yml gate (3) pull_request ok (exit 0) 3s
  • ⚠️ node v20.19.2 here, v22 on CI (tsc runs under node)
test output (failures, totals)
▶ bun install --frozen-lockfile
  ok  exit 0  0s
▶ forge.json check.fast
Ran 87 tests across 7 files in 2.0s (7 shards).
  ok  exit 0  3s
▶ check.yml gate (1) pull_request
Ran 12 tests across 2 files in 0.1s (2 shards).
  ok  exit 0  0s
▶ check.yml gate (2) pull_request
Ran 7 tests across 2 files in 0.1s (2 shards).
  ok  exit 0  0s
▶ check.yml gate (3) pull_request
Ran 68 tests across 3 files in 2.3s (3 shards).
  ok  exit 0  3s
check.yml cloned this repository and checked out GITHUB_SHA. A fork's head commit is here only as refs/pull/<n>/head, which a clone does not fetch, so every gate shard of a fork pull died at "clone source at this ref" with "fatal: reference is not a tree" before running a test (PR #39, run 893). preview.yml cloned --branch "$GITHUB_HEAD_REF", a branch that exists only in the fork; a fork pull gets no secrets either, and a preview builds and pushes an image of the pull's code, so it now skips pulls from forks. ### Why now #39 (https://git.open-platform.sh/trevato-apps/forge/pulls/39) is the first pull from a fork, and it is blocked by this: all three gate shards fail at "clone source at this ref" with `fatal: reference is not a tree: cf8de4df…` (run 893), and preview fails with `Remote branch fix/2171-flow-unused-imports not found in upstream origin` (run 894). No test ran. ### Security review With this change, a fork pull's own code runs in `check.yml` (`bun install`, `scripts/test-shards.ts`, the tests). What that code can reach: - **Secrets: none.** `check.yml` references no `secrets.*`. Forgejo also leaves `secrets` empty for a `pull_request` whose head is a fork. - **Token: read-only.** The only credential is `github.token`, scoped to the "clone source" step env and embedded in `src/.git/config` by the clone. Fork code in later steps can read it from there, and the runner hands job tokens to steps in other ways too, so scrubbing the URL would not be a real boundary. What matters is Forgejo's rule that a `pull_request` run's automatic token has no write permission (read-only for forks). It can read this public repository, which anyone can already do. - **No push, no publish.** Neither `gate` nor `check` pushes git, builds or pushes images or charts, signs anything, or calls a write API (`/ci/preview` and the registry appear only in `preview.yml` and `release.yml`). No cache is shared with privileged workflows, so nothing can be poisoned. - **Trigger is `pull_request`, not `pull_request_target`.** The fork's code never runs with base-repository privileges. Fork pulls from users without elevated rights still need a maintainer's approval before Actions run them (Forgejo default). - **The fetch adds no capability.** `refs/pull/<n>/head` is fetched only when `$GITHUB_SHA` is not already in the clone (fork pulls). It is the same commit the event names; nothing else changes for pushes to main or same-repository pulls. - **Preview stays off for forks.** It needs `REGISTRY_TOKEN`/cosign secrets (empty for forks anyway) and builds and pushes an image of the pull's code, so the job is skipped when `head.repo.full_name != github.repository`. Teardown is skipped too; there is nothing to tear down. Conclusion: no secret or write token reaches fork code in `check.yml`, and its jobs cannot push or publish. No extra gating needed beyond the preview skip. ### Known baseline on main (not this change) `test/og-png.test.ts` "median of three cold renders is within 300 ms" failed main's push gate shard 2 (run 889: 616 ms / 708 ms on the half-core, 512 MiB runner). Pulls whose fast tier includes it (e.g. #39) may hit it too. <!-- pipeline:validation --> ### Validation `pipeline/validate` on `a9797560`: **pass** in 6s — bun 1.3.14, CI-shaped env (scrubbed vars, runner PATH), exact check.yml shard + forge.json commands. | step | result | time | |---|---|---| | `bun install --frozen-lockfile` | ok (exit 0) | 0s | | `forge.json check.fast` | ok (exit 0) | 3s | | `check.yml gate (1) pull_request` | ok (exit 0) | 0s | | `check.yml gate (2) pull_request` | ok (exit 0) | 0s | | `check.yml gate (3) pull_request` | ok (exit 0) | 3s | - ⚠️ node v20.19.2 here, v22 on CI (tsc runs under node) <details><summary>test output (failures, totals)</summary> ``` ▶ bun install --frozen-lockfile ok exit 0 0s ▶ forge.json check.fast Ran 87 tests across 7 files in 2.0s (7 shards). ok exit 0 3s ▶ check.yml gate (1) pull_request Ran 12 tests across 2 files in 0.1s (2 shards). ok exit 0 0s ▶ check.yml gate (2) pull_request Ran 7 tests across 2 files in 0.1s (2 shards). ok exit 0 0s ▶ check.yml gate (3) pull_request Ran 68 tests across 3 files in 2.3s (3 shards). ok exit 0 3s ``` </details>
A pull from a fork gets its check: the gate fetches refs/pull/<n>/head when the head is not on a branch, and the preview skips forks
All checks were successful
check / gate (2) (pull_request) Successful in 19s
check / gate (1) (pull_request) Successful in 23s
check / gate (3) (pull_request) Successful in 24s
check / check (pull_request) Successful in 0s
preview / preview (pull_request) Has been skipped
a979756022
check.yml cloned this repository and checked out GITHUB_SHA. A fork's head
commit is here only as refs/pull/<n>/head, which a clone does not fetch, so
every gate shard of a fork pull died at "clone source at this ref" with
"fatal: reference is not a tree" before running a test (PR #39, run 893).

preview.yml cloned --branch "$GITHUB_HEAD_REF", a branch that exists only in
the fork; a fork pull gets no secrets either, and a preview builds and pushes
an image of the pull's code, so it now skips pulls from forks.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
trevato-apps/forge!40
No description provided.