Skip to content

Commit 2270adc

Browse files
authored
feat: allow PR workflows from forks (#1547)
# Fork PR workflows ## Problems 1. Fork PR previews were blocked by design, because deploy jobs with secrets must not run automatically on untrusted fork code. 2. `size-stats` failed on fork PRs because it required `GH_TOKEN_ACTIONS` to checkout a private repository, and that secret is not available in fork-triggered `pull_request` runs. ## Solution 1. Fork preview deploys now use a controlled manual flow: - dedicated `workflow_dispatch` - PR must be open and from a fork - required label: `safe-to-deploy` - `environment: production` approval before using deploy secrets 2. `size-stats` now works for forks using a two-step flow: - step A (`pull_request`): compute stats and upload artifact (no **private token** needed) - step B (`workflow_run`): trusted workflow reads artifact and updates PR comment ## Result - Internal PR previews continue to run automatically. - Fork PR previews can be deployed under maintainer control. - `size-stats` works for fork PRs without exposing private credentials. ## Note on label safe-to-deploy The `safe-to-deploy` label is a manual signal that a PR is ready for deployment. It is not a security boundary by itself, but it serves as a checkpoint for maintainers to review the PR before allowing deploys. In the current setup, the `environment` approval is the actual security gate that protects secrets, while the label is a workflow control mechanism. ## Repo permissions note for labels Who can apply labels depends on repository permissions. According to the repository configuration, the following teams have elevated roles: - `push` teams: niji4home, novum-engineering, mistica-design, vivo-collaborators - `admin` teams: novum-web-core - `pull` teams (review-focused): network-tokenization, picara Only collaborators or teams with at least `Triage`/`Write`/`Maintain`/`Admin` can add labels; non-collaborator external users cannot. If you rely on label-driven automation, make sure these teams are the ones trusted to apply the `safe-to-deploy` label as it is set in terraform repository settings [here](https://github.com/Telefonica/tf-github-cdo-repos/blob/main/novum/repositories/mistica-web/terraform.tfvars#L23-L32)
1 parent a1f2e7d commit 2270adc

7 files changed

Lines changed: 250 additions & 24 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Deploy Vercel preview
2+
3+
description: Build and deploy a preview to Vercel
4+
5+
inputs:
6+
github-token:
7+
description: GitHub token used by Vercel action to comment on PRs
8+
required: true
9+
vercel-token:
10+
description: Vercel token
11+
required: true
12+
vercel-org-id:
13+
description: Vercel organization id
14+
required: true
15+
vercel-project-id:
16+
description: Vercel project id
17+
required: true
18+
vercel-project-name:
19+
description: Vercel project name
20+
required: true
21+
working-directory:
22+
description: Working directory for deployment
23+
required: false
24+
default: ${{ github.workspace }}
25+
26+
runs:
27+
using: composite
28+
steps:
29+
- name: Prepare build command for preview
30+
working-directory: ${{ inputs.working-directory }}
31+
run: |
32+
sed -i 's/yarn vercel-build/yarn vercel-preview-build/' vercel.json
33+
shell: bash
34+
35+
- name: Deploy to Vercel
36+
uses: amondnet/vercel-action@v42.3.0
37+
id: vercel-deploy
38+
with:
39+
github-token: ${{ inputs.github-token }}
40+
vercel-token: ${{ inputs.vercel-token }}
41+
vercel-org-id: ${{ inputs.vercel-org-id }}
42+
vercel-project-id: ${{ inputs.vercel-project-id }}
43+
vercel-project-name: ${{ inputs.vercel-project-name }}
44+
working-directory: ${{ inputs.working-directory }}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: deploy-fork-pr-preview
2+
on:
3+
workflow_dispatch:
4+
inputs:
5+
prNumber:
6+
description: 'Pull request number to deploy preview for'
7+
required: true
8+
9+
permissions:
10+
contents: read
11+
pull-requests: write
12+
13+
jobs:
14+
deploy:
15+
name: Deploy fork PR preview
16+
runs-on: ubuntu-latest
17+
# Manual approval gate before exposing deployment secrets to reviewed PR code
18+
environment: production
19+
steps:
20+
- name: Validate PR and extract refs
21+
id: pr
22+
uses: actions/github-script@v7
23+
with:
24+
script: |
25+
const REQUIRED_LABEL = 'safe-to-deploy';
26+
const prNumber = Number('${{ github.event.inputs.prNumber }}');
27+
if (!Number.isInteger(prNumber) || prNumber <= 0) {
28+
core.setFailed('Invalid prNumber input');
29+
return;
30+
}
31+
32+
const {owner, repo} = context.repo;
33+
const {data: pr} = await github.rest.pulls.get({owner, repo, pull_number: prNumber});
34+
35+
if (pr.state !== 'open') {
36+
core.setFailed(`PR #${prNumber} is not open`);
37+
return;
38+
}
39+
40+
if (!pr.head.repo.fork) {
41+
core.setFailed(`PR #${prNumber} is not from a fork. Use deploy-pull-requests workflow for internal PRs.`);
42+
return;
43+
}
44+
45+
const labels = (pr.labels || []).map((label) => label.name);
46+
if (!labels.includes(REQUIRED_LABEL)) {
47+
core.setFailed(`PR #${prNumber} is missing required label: ${REQUIRED_LABEL}`);
48+
return;
49+
}
50+
51+
core.setOutput('merge_ref', `refs/pull/${prNumber}/merge`);
52+
core.setOutput('pr_number', String(prNumber));
53+
- uses: actions/checkout@v6
54+
with:
55+
ref: ${{ steps.pr.outputs.merge_ref }}
56+
persist-credentials: false
57+
58+
- uses: ./.github/actions/deploy-vercel-preview
59+
with:
60+
github-token: ${{ secrets.GITHUB_TOKEN }}
61+
vercel-token: ${{ secrets.VERCEL_TOKEN }}
62+
vercel-org-id: ${{ secrets.MISTICA_WEB_VERCEL_ORG_ID }}
63+
vercel-project-id: ${{ secrets.MISTICA_WEB_VERCEL_PROJECT_ID }}
64+
vercel-project-name: mistica-web
65+
working-directory: ${{ github.workspace }}

.github/workflows/deploy-master.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
with:
2020
persist-credentials: false
2121

22-
- uses: amondnet/vercel-action@master
22+
- uses: amondnet/vercel-action@v42.3.0
2323
with:
2424
github-token: ${{ secrets.GITHUB_TOKEN }} # needed to allow comments on prs
2525
vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required

.github/workflows/deploy-pull-requests.yml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,7 @@ jobs:
1919
- uses: actions/checkout@v6
2020
with:
2121
persist-credentials: false
22-
23-
# we need to run a different buildCommand for PR previews (see package.json scripts)
24-
- run: sed -i 's/yarn vercel-build/yarn vercel-preview-build/' vercel.json
25-
shell: bash
26-
27-
- uses: amondnet/vercel-action@master
28-
id: vercel-deploy # identifier to reference this step
22+
- uses: ./.github/actions/deploy-vercel-preview
2923
with:
3024
github-token: ${{ secrets.GITHUB_TOKEN }} # needed to allow comments on prs
3125
vercel-token: ${{ secrets.VERCEL_TOKEN }} # required
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Auto-dispatch deploy on label
2+
3+
on:
4+
pull_request_target:
5+
types: [labeled]
6+
7+
permissions:
8+
actions: write
9+
contents: read
10+
issues: write
11+
pull-requests: read
12+
13+
jobs:
14+
dispatch:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Dispatch deploy workflow when `safe-to-deploy` label is added
18+
uses: actions/github-script@v7
19+
with:
20+
script: |
21+
const TARGET_LABEL = 'safe-to-deploy';
22+
const label = context.payload.label && context.payload.label.name;
23+
if (label !== TARGET_LABEL) {
24+
core.info(`Label '${label}' is not '${TARGET_LABEL}', skipping dispatch.`);
25+
return;
26+
}
27+
28+
const prNumber = context.payload.pull_request && context.payload.pull_request.number;
29+
if (!prNumber) {
30+
core.setFailed('Could not find pull request number in event payload.');
31+
return;
32+
}
33+
34+
const { owner, repo } = context.repo;
35+
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
36+
37+
// Only dispatch the fork-preview workflow for PRs coming from forks
38+
if (!pr.head || !pr.head.repo || !pr.head.repo.fork) {
39+
core.info('PR is not from a fork; skipping fork preview dispatch.');
40+
return;
41+
}
42+
43+
await github.rest.actions.createWorkflowDispatch({
44+
owner,
45+
repo,
46+
workflow_id: 'deploy-fork-pr-preview.yml',
47+
ref: 'master',
48+
inputs: { prNumber: String(prNumber) },
49+
});
50+
51+
core.info(`Dispatched deploy-fork-pr-preview for PR #${prNumber}`);
52+
53+
// post an audit comment on the PR
54+
const commentBody = `Label '${TARGET_LABEL}' added — dispatching fork preview workflow. Awaiting environment approval to expose deploy secrets.`;
55+
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body: commentBody });
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Size stats comment
2+
3+
on:
4+
workflow_run:
5+
workflows: ['Size stats']
6+
types: [completed]
7+
8+
permissions:
9+
actions: read
10+
contents: read
11+
pull-requests: write
12+
13+
jobs:
14+
comment:
15+
runs-on: ubuntu-latest
16+
if: github.event.workflow_run.conclusion == 'success'
17+
steps:
18+
- name: Download size stats artifact
19+
uses: actions/download-artifact@v4
20+
with:
21+
github-token: ${{ secrets.GITHUB_TOKEN }}
22+
run-id: ${{ github.event.workflow_run.id }}
23+
name: size-stats-results
24+
path: size-stats-results
25+
26+
- name: Upsert PR comment
27+
uses: actions/github-script@v7
28+
with:
29+
script: |
30+
const fs = require('fs');
31+
32+
const marker = '<!-- size-stats-comment -->';
33+
const title = '**Size stats**';
34+
const workflowRunPrs = context.payload.workflow_run?.pull_requests || [];
35+
const prNumber = workflowRunPrs.length === 1 ? workflowRunPrs[0].number : NaN;
36+
const message = fs.readFileSync('size-stats-results/message.md', 'utf8').trim();
37+
const body = `${marker}\n${title}\n\n${message}`;
38+
39+
if (!Number.isInteger(prNumber) || prNumber <= 0) {
40+
core.setFailed('Could not determine a unique PR number from workflow_run payload');
41+
return;
42+
}
43+
44+
const {owner, repo} = context.repo;
45+
const {data: comments} = await github.rest.issues.listComments({
46+
owner,
47+
repo,
48+
issue_number: prNumber,
49+
per_page: 100,
50+
});
51+
52+
const previous = comments.find((comment) =>
53+
comment.user?.type === 'Bot' && comment.body?.includes(marker),
54+
);
55+
56+
if (previous) {
57+
await github.rest.issues.updateComment({
58+
owner,
59+
repo,
60+
comment_id: previous.id,
61+
body,
62+
});
63+
} else {
64+
await github.rest.issues.createComment({
65+
owner,
66+
repo,
67+
issue_number: prNumber,
68+
body,
69+
});
70+
}

.github/workflows/size-stats.yml

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66

77
permissions:
88
contents: read
9-
pull-requests: write
9+
pull-requests: read
1010

1111
concurrency:
1212
group: size-stats-${{ github.ref }}
@@ -56,20 +56,12 @@ jobs:
5656
- id: stats
5757
uses: './.github/actions/size-stats'
5858

59-
show-results:
59+
store-results:
6060
runs-on: ubuntu-latest
6161
needs: [master-size-stats, branch-size-stats]
6262
steps:
6363
- uses: actions/checkout@v6
6464
with:
65-
ref: master
66-
persist-credentials: false
67-
68-
- uses: actions/checkout@v6
69-
with:
70-
repository: Telefonica/github-actions
71-
token: '${{ secrets.GH_TOKEN_ACTIONS }}'
72-
path: .github/shared-actions
7365
persist-credentials: false
7466

7567
- run: yarn install --immutable --immutable-cache
@@ -87,10 +79,16 @@ jobs:
8779
pr-lib-overhead: ${{ needs.branch-size-stats.outputs.lib-overhead }}
8880
pr-lib-overhead-gzip: ${{ needs.branch-size-stats.outputs.lib-overhead-gzip }}
8981

90-
- name: Comment on PR
91-
uses: ./.github/shared-actions/novum/comment-pr
82+
- name: Prepare artifact payload
83+
run: |
84+
mkdir -p size-stats-results
85+
cat > size-stats-results/message.md <<'EOF'
86+
${{ steps.message.outputs.message }}
87+
EOF
88+
89+
- name: Upload size stats results
90+
uses: actions/upload-artifact@v4
9291
with:
93-
github-token: ${{ secrets.GITHUB_TOKEN }}
94-
title: '**Size stats**'
95-
message: ${{ steps.message.outputs.message }}
96-
update-if-present: 'true'
92+
name: size-stats-results
93+
path: size-stats-results
94+
if-no-files-found: error

0 commit comments

Comments
 (0)