Skip to content

Commit f334fd5

Browse files
authored
Harden CI workflows with best practices (#15413)
Assisted-By: devx/597728f0-1b3f-4153-b929-18dbff6cd683
1 parent 5e98140 commit f334fd5

6 files changed

Lines changed: 72 additions & 5 deletions

File tree

.github/workflows/pr-actions.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,8 @@ jobs:
5050
- name: Apply actions
5151
env:
5252
GITHUB_TOKEN: ${{ github.token }}
53+
WORKFLOW_RUN_HEAD_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }}
54+
WORKFLOW_RUN_HEAD_REPO_ID: ${{ github.event.workflow_run.head_repository.id }}
55+
WORKFLOW_RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
56+
WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
5357
run: node scripts/pr.ts actions pr-checks-result.json

.github/workflows/preview.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,12 @@ jobs:
9595
# pushes over remote branches in case of a branch name collision
9696
- name: Build/push branch (workflow_dispatch)
9797
if: github.event_name == 'workflow_dispatch'
98+
env:
99+
INSTALLABLE_BRANCH: ${{ inputs.installableBranch }}
98100
run: |
99-
node ./scripts/previews/branch.ts ${{ inputs.installableBranch }}
100-
git push --set-upstream origin ${{ inputs.installableBranch }}
101+
git check-ref-format --branch "$INSTALLABLE_BRANCH"
102+
node ./scripts/previews/branch.ts "$INSTALLABLE_BRANCH"
103+
git push --set-upstream origin "$INSTALLABLE_BRANCH"
101104
echo "💿 pushed installable branch: https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)"
102105
103106
# Cleanup PR preview/pr-{number} branches when the PR is closed

.github/workflows/release-comments-manual.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ jobs:
3939
- name: Comment on released issues and pull requests
4040
env:
4141
GH_TOKEN: ${{ github.token }}
42-
run: pnpm run release-comments --release=${{ github.event.inputs.release }}
42+
RELEASE: ${{ inputs.release }}
43+
run: pnpm run release-comments --release="$RELEASE"

scripts/pr.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
*
2929
* Environment (actions):
3030
* GITHUB_TOKEN - Required (issues:write + pull-requests:write).
31+
* WORKFLOW_RUN_HEAD_OWNER - Required. workflow_run.head_repository.owner.login
32+
* WORKFLOW_RUN_HEAD_REPO_ID - Required. workflow_run.head_repository.id
33+
* WORKFLOW_RUN_HEAD_BRANCH - Required. workflow_run.head_branch
34+
* WORKFLOW_RUN_HEAD_SHA - Required. workflow_run.head_sha
3135
*/
3236
import * as fs from "node:fs";
3337
import * as util from "node:util";
@@ -39,6 +43,7 @@ import {
3943
createPrComment,
4044
getPrComments,
4145
getPrFiles,
46+
getWorkflowRunPrNumber,
4247
removePrLabel,
4348
updatePrComment,
4449
} from "./utils/github.ts";
@@ -344,7 +349,9 @@ async function runActions() {
344349
return;
345350
}
346351

347-
let { prNumber, actions } = JSON.parse(fs.readFileSync(filename, "utf8")) as {
352+
let { prNumber: artifactPrNumber, actions } = JSON.parse(
353+
fs.readFileSync(filename, "utf8"),
354+
) as {
348355
prNumber: number;
349356
actions: Action[];
350357
};
@@ -354,6 +361,25 @@ async function runActions() {
354361
return;
355362
}
356363

364+
let headRepositoryId = Number(requireEnv("WORKFLOW_RUN_HEAD_REPO_ID"));
365+
if (!Number.isSafeInteger(headRepositoryId) || headRepositoryId <= 0) {
366+
throw new Error("WORKFLOW_RUN_HEAD_REPO_ID must be a positive integer");
367+
}
368+
369+
// The artifact is PR-controlled. Resolve its only permitted target from the
370+
// trusted workflow_run event and reject stale or cross-PR instructions.
371+
let prNumber = await getWorkflowRunPrNumber({
372+
headOwner: requireEnv("WORKFLOW_RUN_HEAD_OWNER"),
373+
headRepositoryId,
374+
headBranch: requireEnv("WORKFLOW_RUN_HEAD_BRANCH"),
375+
headSha: requireEnv("WORKFLOW_RUN_HEAD_SHA"),
376+
});
377+
if (artifactPrNumber !== prNumber) {
378+
throw new Error(
379+
`Artifact targets PR #${String(artifactPrNumber)}, but workflow run belongs to PR #${prNumber}`,
380+
);
381+
}
382+
357383
console.log(actions);
358384

359385
for (let action of actions) {

scripts/previews/branch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ async function main() {
5555
);
5656

5757
// Switch to new branch and reset to current commit on base branch
58-
logAndExec(`git checkout -B ${installableBranch}`);
58+
logAndExec(["git", "checkout", "-B", installableBranch]);
5959

6060
// Build dist/ folders
6161
logAndExec("pnpm build");

scripts/utils/github.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import { getGitTag } from "./packages.ts";
55
const OWNER = "remix-run";
66
const REPO = "react-router";
77

8+
type WorkflowRunHead = {
9+
headOwner: string;
10+
headRepositoryId: number;
11+
headBranch: string;
12+
headSha: string;
13+
};
14+
815
function getToken(): string {
916
let token = process.env.GITHUB_TOKEN;
1017
if (!token) {
@@ -119,6 +126,32 @@ export async function findOpenPr(head: string, base: string) {
119126
return response.data.length > 0 ? response.data[0] : null;
120127
}
121128

129+
/**
130+
* Resolve the open PR whose current head produced a workflow_run event.
131+
*/
132+
export async function getWorkflowRunPrNumber(workflowRun: WorkflowRunHead) {
133+
let response = await request("GET /repos/{owner}/{repo}/pulls", {
134+
...requestOptions(),
135+
state: "open",
136+
head: `${workflowRun.headOwner}:${workflowRun.headBranch}`,
137+
per_page: 100,
138+
});
139+
140+
let matches = response.data.filter(
141+
(pr) =>
142+
pr.head.repo?.id === workflowRun.headRepositoryId &&
143+
pr.head.ref === workflowRun.headBranch &&
144+
pr.head.sha === workflowRun.headSha,
145+
);
146+
if (matches.length !== 1) {
147+
throw new Error(
148+
`Expected exactly one open PR for workflow run, found ${matches.length}`,
149+
);
150+
}
151+
152+
return matches[0].number;
153+
}
154+
122155
/**
123156
* Create a new PR
124157
*/

0 commit comments

Comments
 (0)