Skip to content

Commit 24a42e4

Browse files
authored
[BRE-1510] Adding templates and README for community contribution workflows (#591)
1 parent f2ba1be commit 24a42e4

3 files changed

Lines changed: 213 additions & 0 deletions

File tree

templates/pull-request/README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Pull Request and Pull Request Target Workflow Templates
2+
3+
Templates for safely handling internal and external fork pull requests in GitHub Actions workflows that require secrets, following Bitwarden security patterns.
4+
5+
## Overview
6+
7+
Different strategies are needed for internal vs. fork PRs when workflows require secrets:
8+
9+
- **Internal PRs**: Use `pull_request` trigger (runs with limited permissions, no secrets needed)
10+
- **Fork PRs**: Use `pull_request_target` (runs in target repo context with approval gate for secret access)
11+
12+
## The Two-Workflow Pattern
13+
14+
### `example-workflow.yml` - Main Workflow
15+
16+
Contains actual job logic (build, test, deploy, etc.). Triggered by `pull_request` for internal PRs and `workflow_call` for reuse. Skips fork PRs via condition: `if: github.event.pull_request.head.repo.full_name == github.repository`
17+
18+
### `example-workflow-target.yml` - Fork Handler
19+
20+
Safely handles fork PRs needing secrets. Triggered by `pull_request_target` (targets default branch only). Runs `check-run` security gate first, then calls main workflow with `secrets: inherit`. Only executes for forks via condition: `if: github.event.pull_request.head.repo.full_name != github.repository`
21+
22+
## Security Model
23+
24+
**The Problem**: Fork PRs can't access secrets with `pull_request`, but `pull_request_target` runs untrusted code in your repo's context with secret access.
25+
26+
**The Solution**: Two-phase approval process:
27+
1. `pull_request_target` workflow runs immediately for fork PRs
28+
2. `check-run` job fails (fork user lacks write permissions)
29+
3. Maintainer reviews code for malicious behavior
30+
4. Maintainer re-runs workflow (passes check-run with maintainer identity, grants secret access)
31+
32+
**Protection Layers**:
33+
- Permission check validates write access
34+
- Conditional execution separates internal/fork paths
35+
- Mandatory code review before secret access
36+
- Workflow linter enforces `pull_request_target` targets default branch only and workflows include `check-run`
37+
38+
## ⚠️ Critical Security Warning
39+
40+
**ALWAYS review fork PR code BEFORE re-running failed workflows.** Once re-run, workflows access repository secrets and can exfiltrate data.
41+
42+
**Check for**:
43+
- Uploads to external services
44+
- Unexpected network requests
45+
- Base64 encoding/obfuscation
46+
- Secrets exposed in artifacts or logs
47+
48+
## Approval Process
49+
50+
**For `pull_request` (no secrets)**: Click "Approve and run" to unblock workflow execution.
51+
52+
**For `pull_request_target` (with secrets)**:
53+
1. Workflow runs immediately, `check-run` fails (expected)
54+
2. Review PR code thoroughly
55+
3. Click "Re-run failed jobs" (grants secret access via maintainer identity)
56+
57+
## Usage Instructions
58+
59+
1. **Copy templates** to `.github/workflows/` and rename appropriately
60+
2. **Customize main workflow**: Update name, paths, jobs, secrets, and inputs
61+
3. **Customize target workflow**: Update name, workflow reference in `uses:`, paths, permissions, and inputs
62+
4. **Test both scenarios**: Create internal PR (should trigger main workflow) and fork PR (should trigger target workflow with failed check-run)
63+
64+
## Best Practices
65+
66+
**DO**:
67+
- Use `check-run` job first in `pull_request_target` workflows
68+
- Review fork code before approving
69+
- Set minimal permissions per job
70+
- Pin actions to commit hashes
71+
- Use conditions to separate internal/fork execution
72+
73+
**DON'T**:
74+
- Skip `check-run` security gate
75+
- Use `pull_request_target` unless secrets are required
76+
- Grant unnecessary permissions
77+
- Expose secrets in logs/outputs
78+
- Run both workflows for same PR
79+
80+
## Troubleshooting
81+
82+
**Both workflows running for internal PR**: Check conditional expressions match template patterns.
83+
84+
**"Check PR run" failed (EXPECTED)**: This is the security gate working. Review code, then re-run to grant secret access.
85+
86+
**Secrets unavailable in fork PR**: Ensure using `pull_request_target` pattern with `secrets: inherit`.
87+
88+
**Workflow linter failing**: Verify `pull_request_target` targets default branch only.
89+
90+
**Compliance**: Templates comply with all Bitwarden workflow-linter rules (pinned runners/actions, explicit permissions, proper naming, `pull_request_target` restrictions).
91+
92+
## References
93+
94+
- [GitHub Actions: pull_request vs pull_request_target](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target)
95+
- [Bitwarden Workflow Linter Rules](https://github.com/bitwarden/workflow-linter/tree/main/src/bitwarden_workflow_linter/rules)
96+
- [Bitwarden Clients Example](https://github.com/bitwarden/clients/blob/main/.github/workflows/build-web-target.yml)
97+
- [Keeping your GitHub Actions secure](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
# Example pull_request_target workflow for handling external fork PRs
3+
#
4+
# ⚠️ CRITICAL SECURITY WARNING ⚠️
5+
# This workflow runs IMMEDIATELY when a fork PR is opened and has access to repository secrets.
6+
# The check-run job will FAIL initially (expected behavior) - this is the security gate.
7+
#
8+
# REQUIRED STEPS FOR MAINTAINERS:
9+
# 1. REVIEW THE PR CODE for malicious code that could exfiltrate secrets
10+
# 2. Only after code review, click "Re-run failed jobs" in GitHub Actions UI
11+
# 3. Re-running passes your identity to check-run, granting secret access
12+
#
13+
# Key patterns:
14+
# 1. Triggered by pull_request_target (for fork PRs with secrets)
15+
# 2. Runs check-run first - FAILS for fork users (expected), PASSES on maintainer re-run
16+
# 3. Only executes for external forks (internal PRs use pull_request trigger)
17+
# 4. Calls the main workflow and passes secrets
18+
# 5. Must target default branch per workflow-linter rules
19+
20+
name: Example Workflow on PR Target
21+
22+
on:
23+
pull_request_target:
24+
types: [opened, synchronize, reopened]
25+
branches:
26+
- main # Required by workflow-linter: pull_request_target must target default branch
27+
paths:
28+
- "example/**"
29+
- ".github/workflows/example-workflow.yml"
30+
- ".github/workflows/example-workflow-target.yml"
31+
32+
permissions:
33+
contents: read
34+
35+
jobs:
36+
check-run:
37+
name: Check PR run approval
38+
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
39+
permissions:
40+
contents: read
41+
42+
example-job:
43+
name: Run example job for fork
44+
needs: check-run
45+
# Only run if the PR is from an external fork
46+
# Internal PRs are handled by the pull_request trigger in example-workflow.yml
47+
if: github.event.pull_request.head.repo.full_name != github.repository
48+
uses: ./.github/workflows/example-workflow.yml
49+
permissions:
50+
contents: read
51+
id-token: write
52+
secrets: inherit # Pass all repository secrets to the called workflow
53+
with:
54+
custom_parameter: "fork-pr-value"
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
# Example workflow that runs on pull_request and can also be called from pull_request_target
3+
# This workflow skips execution on external forks since it won't have access to secrets when invoked via pull_request
4+
#
5+
# Key patterns:
6+
# 1. Triggered by pull_request (for internal PRs)
7+
# 2. Can be called via workflow_call (from pull_request_target workflow for external PRs)
8+
# 3. Skips execution on forks using condition when triggered with pull_request
9+
# 4. Explicit permissions at workflow level
10+
11+
name: Example Workflow
12+
13+
on:
14+
pull_request:
15+
types: [opened, synchronize]
16+
branches:
17+
- main
18+
paths:
19+
- "example/**"
20+
- ".github/workflows/example-workflow.yml"
21+
workflow_call:
22+
inputs:
23+
custom_parameter:
24+
description: "Optional custom parameter from calling workflow"
25+
required: false
26+
type: string
27+
default: "default-value"
28+
29+
permissions:
30+
contents: read
31+
32+
jobs:
33+
check-event-source:
34+
name: Check event and source
35+
runs-on: ubuntu-24.04
36+
# Only run this job if the event is workflow_call or the PR is from the same repository (not a fork)
37+
# This avoid running it twice: once for pull_request from fork and once from pull_request_target
38+
if: github.event_name == 'workflow_call' || github.event.pull_request.head.repo.full_name == github.repository
39+
steps:
40+
- name: Check PR event and source
41+
run: echo "This PR is from the same repository (Not a fork) or called via workflow_call."
42+
example-job:
43+
name: Run example job
44+
runs-on: ubuntu-24.04
45+
needs: check-event-source # Only run if intended, otherwise skip
46+
permissions:
47+
contents: read
48+
id-token: write # Required if you need to authenticate to external services
49+
steps:
50+
- name: Get secrets
51+
run: echo "Series of steps that would retrieve secrets"
52+
53+
- name: Check out repo
54+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
55+
with:
56+
ref: ${{ github.event.pull_request.head.sha }}
57+
58+
- name: Run some code
59+
run: |
60+
echo "Running workflow on branch: ${{ github.head_ref }}"
61+
echo "Custom parameter: ${{ inputs.custom_parameter }}"
62+
echo "This step could build, test, or deploy your code"

0 commit comments

Comments
 (0)