|
1 | 1 | name: CI |
2 | | -# SECURITY NOTE: This workflow uses pull_request_target which has access to secrets. |
3 | | -# This is needed because tests require access to external services with credentials. |
4 | | -# `pull_request_target` will always run without manual approval, even if "Require approval for all external contributors" is enabled in the repo settings. |
5 | | -# Therefore we implement a "safe to test" label that must be manually added once we have checked that the diff is safe. |
6 | | -# For PRs from forks, secrets are only provided when the "safe to test" label is present. |
7 | | -# This allows maintainers to safely test external contributions while preventing |
8 | | -# malicious actors from accessing secrets. |
| 2 | +# SECURITY: Uses environment protection for external PRs instead of unsafe "safe to test" labels. |
| 3 | +# Environment protection provides secure manual approval tied to specific commits, |
| 4 | +# eliminating race conditions and ensuring maintainer review before secrets access. |
9 | 5 | on: |
10 | 6 | push: |
11 | 7 | branches: [main] |
12 | 8 | paths-ignore: |
13 | 9 | - "**.md" |
14 | 10 | - ".changeset/**" |
15 | 11 | pull_request_target: |
16 | | - types: [opened, synchronize, reopened, labeled] |
| 12 | + types: [opened, synchronize, reopened] |
17 | 13 | paths-ignore: |
18 | 14 | - "**.md" |
19 | 15 | - ".changeset/**" |
|
25 | 21 | concurrency: ${{ github.workflow }}--${{ github.ref }} |
26 | 22 |
|
27 | 23 | permissions: |
| 24 | + contents: read |
28 | 25 | pull-requests: write |
29 | 26 |
|
30 | 27 | jobs: |
31 | 28 | main: |
32 | 29 | name: Node.js 20 |
33 | 30 | runs-on: ubuntu-latest |
34 | | - # Only run tests with secrets if: |
35 | | - # 1. This is a push to main, OR |
36 | | - # 2. PR is from the same repository (trusted), OR |
37 | | - # 3. PR has the "safe to test" label (maintainer approved) |
| 31 | + # SECURITY: Use environment protection for external contributors |
| 32 | + environment: ${{ github.event.pull_request.head.repo.full_name != github.repository && 'external-testing' || '' }} |
| 33 | + # Run tests with secrets for: |
| 34 | + # 1. Push to main (trusted), OR |
| 35 | + # 2. PR from same repository (trusted) |
| 36 | + # For external PRs: environment protection requires manual approval |
38 | 37 | if: | |
39 | | - github.event_name == 'push' || |
40 | | - github.event.pull_request.head.repo.full_name == github.repository || |
41 | | - contains(github.event.pull_request.labels.*.name, 'safe to test') |
| 38 | + github.event_name == 'push' || |
| 39 | + github.event.pull_request.head.repo.full_name == github.repository |
42 | 40 |
|
43 | 41 | steps: |
44 | 42 | - name: Checkout sources |
45 | 43 | uses: actions/checkout@v4 |
46 | 44 | with: |
47 | | - ref: ${{ github.event.pull_request.head.sha || github.sha }} |
| 45 | + # SECURITY: For external PRs, only checkout trusted base branch |
| 46 | + ref: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.sha || github.sha }} |
48 | 47 |
|
49 | 48 | - name: Decrypt keyfile |
50 | 49 | run: ./.github/scripts/decrypt_secret.sh |
|
0 commit comments