Skip to content

Add fork-safe PR build workflow#442

Merged
bschwedler merged 16 commits into
mainfrom
feature/fork-safe-pr-workflow
Apr 10, 2026
Merged

Add fork-safe PR build workflow#442
bschwedler merged 16 commits into
mainfrom
feature/fork-safe-pr-workflow

Conversation

@bschwedler
Copy link
Copy Markdown
Contributor

@bschwedler bschwedler commented Apr 10, 2026

Summary

  • New bakery-build-pr.yml reusable workflow for pull request builds
  • Auto-detects fork PRs via github.event.pull_request.head.repo.fork
  • Fork PRs: no GHCR login, no cache, no push, skips arm64 runners
  • Internal PRs: GHCR cache enabled, still no push
  • No secrets: section — only inherited GITHUB_TOKEN
  • No merge or readme jobs (PRs never push)
  • Wired into ci.yml with a bakery-pr test job

Product repos will adopt this in follow-up PRs by creating pr.yml and removing pull_request from existing workflows.

Depends on #440 (script injection + permissions fixes should merge first).

Addresses the core of rstudio/platform-team#435.

Test plan

  • CI passes — bakery-pr job runs on this PR
  • detect job outputs is-fork: false for internal PRs
  • Build completes with GHCR cache on internal PRs
  • Fork behavior verified by inspecting conditional logic (can't test forks in the same repo)

Move all ${{ }} expressions out of run: blocks into env:
blocks to prevent script injection from runtime values
(matrix outputs, secrets). This makes the "no expressions
in run blocks" rule enforceable by zizmor without
per-expression exceptions.

- bakery-build-native.yml: matrix, build-test (filter,
  build, test), merge (filter, merge/push), readme steps
- bakery-build.yml: matrix, build (filter, build, test,
  push), readme steps
- Convert conditional push flag to shell logic
- Quote all shell variable references
Move all ${{ }} expressions out of run: blocks into env:
blocks in clean.yml, product-release.yml, hadolint.yml,
and ci.yml (release job).

- clean.yml: Convert conditional flags (dry-run, untagged,
  older-than) from expression ternaries to shell logic
- product-release.yml: Move inputs.version and step
  outputs to env vars
- hadolint.yml: Move inputs.context to env var
- ci.yml: Move github.ref_name to env var in release step
Declare least-privilege permissions on all shared
reusable workflows. Each workflow gets permissions: {}
at the top level to drop all default permissions, then
per-job permissions grant only what is needed.

- bakery-build-native.yml: matrix {}, build-test and
  merge {contents: read, packages: write}, readme
  {contents: read}
- bakery-build.yml: matrix {}, build {contents: read,
  packages: write}, readme {contents: read}
- clean.yml: both jobs {contents: read, packages: write}
- hadolint.yml: {contents: read}
- product-release.yml: top-level {} (job-level already
  existed)
Declare least-privilege permissions for the CI and
issue automation workflows.

- ci.yml: top-level {}, ci meta-job {}, test
  {contents: read}, release {contents: write}. Bakery
  and clean jobs already had permissions declared.
- issues.yml: top-level {} and job-level {} since the
  job uses a GitHub App token, not GITHUB_TOKEN
Matrix jobs in bakery-build-native.yml and bakery-build.yml had
permissions: {} but use actions/checkout, which requires
contents: read on private repos.

Build and merge jobs use aws-actions/configure-aws-credentials
with OIDC role assumption, which requires id-token: write. Add
the permission to build-test and merge in the native workflow
and build in the QEMU workflow.

The ci.yml test job uses publish-unit-test-result-action which
needs checks: write to post results.
@bschwedler bschwedler requested a review from ianpittwood as a code owner April 10, 2026 17:16
Dependabot and fork PRs get a restricted GITHUB_TOKEN
without checks:write permission. The publish-unit-test-
result-action fails with 403 in that context. Skip it
rather than switching to pull_request_target, which is
unsafe for code checkout workflows.

Tests still run and report pass/fail via job status.
Resolve conflict in hadolint.yml where main added the
base workflow and this branch added the security policy
comment, permissions, and env var pattern.
Fork PRs have the same restricted GITHUB_TOKEN as
Dependabot PRs — checks:write is unavailable. Check
both github.actor and the fork flag.
Reusable workflow (workflow_call) top-level permissions
act as a ceiling — the caller can never grant more than
what the reusable workflow declares. With permissions: {},
the caller's job-level permissions (contents:read,
packages:write, etc.) are blocked entirely, causing a
startup_failure.

Job-level permissions within the reusable workflow are
kept — they constrain what each job uses without blocking
what the caller grants.
Top-level permissions: {} on a workflow that calls
reusable workflows may constrain the caller-to-callee
permission grant, causing startup_failure. Remove it
and rely on per-job permissions declarations instead.
Job-level permissions in a reusable workflow cannot
exceed what the caller grants. The caller (ci.yml)
grants contents:read + packages:write but not
id-token:write, causing startup_failure when the
callee's job declares it.

The id-token:write permission for AWS OIDC must be
granted by the caller, not declared inside the
reusable workflow.
The publish-unit-test-result-action posts a comment on
the PR which requires pull-requests:write in addition to
checks:write.
New bakery-build-pr.yml shared workflow for PR builds that
works safely with fork PRs. Key differences from the native
build workflow:

- Detects fork PRs and conditionally skips GHCR login/cache
- No push, no merge, no readme jobs
- Skips arm64 builds for fork PRs (paid runners unavailable)
- No secrets section — uses only inherited GITHUB_TOKEN
- All expressions in env: blocks, never in run: blocks
Add bakery-pr job to ci.yml that exercises the new fork-safe
PR workflow on pull_request events. Added to allowed-skips
since it only runs on PRs (not push/merge_group).
The head_ref pattern used by other CI jobs breaks on fork
PRs because the fork's branch doesn't exist in the base
repo. The PR build job uses the default "main" version
instead, which always resolves.
Top-level permissions on workflow_call workflows act as
a ceiling that blocks caller-granted permissions, causing
startup_failure. Use per-job permissions only.
@bschwedler bschwedler force-pushed the feature/fork-safe-pr-workflow branch from 77c622b to 5a07624 Compare April 10, 2026 20:29
@bschwedler bschwedler enabled auto-merge April 10, 2026 20:51
@bschwedler bschwedler disabled auto-merge April 10, 2026 20:51
@bschwedler bschwedler merged commit 1415b20 into main Apr 10, 2026
1 check passed
@bschwedler bschwedler deleted the feature/fork-safe-pr-workflow branch April 10, 2026 20:51
@bschwedler bschwedler mentioned this pull request Apr 10, 2026
2 tasks
bschwedler added a commit that referenced this pull request Apr 13, 2026
bakery-build-pr.yml was added in #442 but missed the SHA-pinning
pass in #443. Pin all third-party actions to full-length commit
SHAs matching bakery-build-native.yml, and add timeout-minutes
to all three jobs (detect: 5m, matrix: 10m, build-test: 120m).

- Upgrade oras-project/setup-oras from v1 to v2.0.0 for
  consistency with the native build workflow
- First-party composite actions (setup-bakery, setup-goss)
  intentionally kept at @main per zizmor ref-pin policy
bschwedler added a commit that referenced this pull request Apr 13, 2026
Now that bakery-build-pr.yml exists on main (merged in #442),
the relative ref resolves correctly. This matches the pattern
used by bakery-build.yml and bakery-build-native.yml, and lets
PRs that modify bakery-build-pr.yml test their own changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant