Repository navigation
Description
Fork pull requests never run e2e — build image push is denied, and the skipped required check still lets the PR merge
Summary
Pull requests opened from a fork cannot run the e2e pipeline, and — more importantly — an approved fork PR can still merge with e2e having never executed. External contributions therefore land without any e2e coverage.
Mechanism
.github/workflows/pull-requests.yaml guards every registry login with if: ${{ !github.event.pull_request.head.repo.fork }}, so on a fork PR the Login to ... registry step is skipped (correct — pull_request runs from a fork receive no secrets). But the build itself pushes unconditionally: hack/common-envs.mk sets PUSH := 1 and BUILDX_ARGS := ... --push=$(PUSH), so make image still runs docker buildx build --push. With no login it pushes anonymously and dies in ~20s:
denied: Anonymous users are only allowed read access on public repos
That fails every Build <pkg> and Build Talos job. finalize requires build to have succeeded or been skipped (not failed), so it is skipped; e2e requires finalize to have succeeded, so it is skipped too. The whole build → finalize → e2e chain collapses on any fork PR.
Impact
Two distinct problems:
- Every fork PR shows a wall of red
Build <pkg>❌. These are not required checks, so they do not block merge, but they read as "this PR is broken" to contributors and reviewers. E2E Testsis a required check, and on a fork PR it ends inskipping. Branch protection treats a skipped required check as satisfied, so an approved fork PR reachesMERGEABLEand can be merged with e2e never having run. For a project where e2e is the merge gate, external contributions bypass it entirely.
Recently observed on a batch of fork PRs; the mechanism is documented with a job link in a comment on #2196.
Options
I am filing this to track the gap and settle direction before implementing — not proposing a specific fix yet.
- A. Privileged
workflow_runsplit. The unprivilegedpull_requestrun builds images without pushing (e.g.--output type=oci) and uploads them as artifacts; a privilegedworkflow_runworkflow from the default branch (which has secrets) pushes those pre-built images and runsfinalize+ e2e, reporting status back to the PR head SHA. Restores e2e for forks without handing registry credentials to the untrusted build. Most work. - B.
ok-to-testlabel +pull_request_target. A maintainer applies a label that triggers a privileged run over the fork's code with secrets (the Prow pattern). Less work, but it runs untrusted code with registry credentials, which is a weaker security posture and needs careful per-commit SHA pinning. - C. Minimal — stop failing on forks. Skip the build/push cleanly on fork PRs so the red ❌ disappears. Does not restore e2e, so the coverage hole in (2) remains; it only fixes the cosmetics of (1).
Notes
- This affects all external contributors, not any one fork.
- Required checks on
maintoday arepre-commitandE2E Tests; theBuild <pkg>jobs are not required.
Suggested labels: kind/bug, area/ci, priority/important-soon.
Activity
fix(ci): export OCI archives for buildx calls that bypass image-tags (#…
fix(ci): avoid zizmor github-env finding on fork e2e sandbox name
fix(ci): export talos image as an OCI archive on fork builds (#3257)
fix(ci): emit a single tag for capi-providers under OCI export (#3257)
fix(ci): decide fork docs-only from the PR file list and harden publi…
test(hack): cover OCI_EXPORT_DIR export mode in common-envs (#3257)
docs(agents): document the fork-PR e2e split and check-run gate (#3257)
myasnikovdaniil commentedon Jul 10, 2026
Implementing this as Option A (privileged
workflow_runsplit) in #3262.It restores e2e for fork PRs while never exposing the registry push credentials to untrusted fork code: the unprivileged
pull_requestbuild exports images as OCI archives, and a privilegedworkflow_runfrom the default branch pushes them by digest, runs e2e, and reports the requiredE2E Testscheck on the head SHA.Option B (
pull_request_target+ok-to-test) was rejected — it runs untrusted code with secrets in scope; Option C (build-without-push) restores neither e2e nor the merge gate. The skipped-required-check bypass is closed by makingE2E Testsan explicitly-concluded check-run instead of a skippable job (a skipped required job counts as passing).fix(ci): fork e2e — Chainsaw+TIA run and image-push hardening
myasnikovdaniil commentedon Jul 15, 2026
Recording the direction here, since this issue asked to settle it before implementing.
Decision: Option A — a privileged
workflow_runsplit. The fork's unprivilegedpull_requestrun builds every image to an OCI archive (no push); a privilegedworkflow_run(running from the default branch) then pushes those archives to the existing CI registry by digest and runs the e2e suite in an ephemeral, credential-less sandbox. Fork build and test code never receive registry credentials — a stronger posture thanpull_request_target, which would hand fork code the secrets directly.Alternatives considered and why not:
pull_request_target— exposes repo secrets to the fork's checked-out code. Rejected on security posture.Implemented in #3262. Flagging so the direction is on the record; happy to revisit if there's a preference before it leaves draft.
build(hack): add OCI_EXPORT_DIR archive-export build mode
ci(e2e): run fork-PR e2e via privileged workflow_run (#3257)
22 remaining items
fix(ci): export OCI archives for buildx calls that bypass image-tags (#…
fix(ci): avoid zizmor github-env finding on fork e2e sandbox name
fix(ci): export talos image as an OCI archive on fork builds (#3257)
fix(ci): emit a single tag for capi-providers under OCI export (#3257)
fix(ci): decide fork docs-only from the PR file list and harden publi…
test(hack): cover OCI_EXPORT_DIR export mode in common-envs (#3257)
docs(agents): document the fork-PR e2e split and check-run gate (#3257)
fix(ci): fork e2e — Chainsaw+TIA run and image-push hardening
fix(ci): publish the "E2E Tests" gate as a commit status, not a check…
ci(api-review-gate): publish the gate as a commit status
fix(ci): restore e2e for fork PRs and close the merge-gate bypass (#3262
ci(api-review-gate): publish the gate as a commit status
ci(api-review-gate): publish the gate as a commit status
ci(api-review-gate): publish the gate as a commit status