From d07582654acda260e169014a20dcde02f2313d44 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 18 May 2026 11:45:24 -0400 Subject: [PATCH 1/2] Move change file workflow to 2-step run (#15069) --- .github/workflows/change-file-comment.yml | 52 +++++++++++++++ .../{changes-file.yml => change-file.yml} | 17 +++-- scripts/changes/check-pr.ts | 63 ++++++++++++++----- 3 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/change-file-comment.yml rename .github/workflows/{changes-file.yml => change-file.yml} (65%) diff --git a/.github/workflows/change-file-comment.yml b/.github/workflows/change-file-comment.yml new file mode 100644 index 0000000000..ff51cbfeef --- /dev/null +++ b/.github/workflows/change-file-comment.yml @@ -0,0 +1,52 @@ +name: 📝 Change File (Comment) + +# Runs after the "Change File (Check)" workflow completes. That workflow runs +# under the `pull_request` trigger with a read-only token (safe for PRs from +# forks); this workflow runs under `workflow_run` with write permissions to +# post the sticky comment, but never executes any PR code — it only reads +# the artifact produced by the upstream workflow. + +on: + workflow_run: + workflows: ["📝 Change File (Check)"] + types: [completed] + +jobs: + comment: + name: 📝 Post Comment + if: > + github.event.workflow_run.conclusion == 'success' && + github.repository == 'remix-run/react-router' + runs-on: ubuntu-latest + permissions: + actions: read # Read artifact + issues: write + pull-requests: write + + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v6 + + - name: 📦 Setup pnpm + uses: pnpm/action-setup@v6 + + - name: ⎔ Setup node + uses: actions/setup-node@v6 + with: + node-version-file: ".nvmrc" + cache: pnpm + + - name: 📥 Install deps + run: pnpm install --frozen-lockfile + + - name: 📥 Download result from upstream workflow + uses: actions/download-artifact@v4 + with: + name: pr-check-result + github-token: ${{ github.token }} + run-id: ${{ github.event.workflow_run.id }} + + - name: 💬 Post comment + env: + GITHUB_TOKEN: ${{ github.token }} + run: node scripts/changes/check-pr.ts comment pr-check-result.json diff --git a/.github/workflows/changes-file.yml b/.github/workflows/change-file.yml similarity index 65% rename from .github/workflows/changes-file.yml rename to .github/workflows/change-file.yml index 76098443a1..5c57a09c9b 100644 --- a/.github/workflows/changes-file.yml +++ b/.github/workflows/change-file.yml @@ -1,4 +1,4 @@ -name: 📝 Change File Check +name: 📝 Change File (Check) on: pull_request: @@ -16,8 +16,8 @@ jobs: if: github.repository == 'remix-run/react-router' runs-on: ubuntu-latest permissions: - issues: write - pull-requests: write + contents: read + pull-requests: read steps: - name: ⬇️ Checkout repo @@ -35,7 +35,14 @@ jobs: - name: 📥 Install deps run: pnpm install --frozen-lockfile - - name: 📝 Check change file and update PR comment + - name: 📝 Check for change file env: GITHUB_TOKEN: ${{ github.token }} - run: node scripts/changes/check-pr.ts ${{ github.event.pull_request.number }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: node scripts/changes/check-pr.ts check "$PR_NUMBER" + + - name: 📤 Upload result + uses: actions/upload-artifact@v4 + with: + name: pr-check-result + path: pr-check-result.json diff --git a/scripts/changes/check-pr.ts b/scripts/changes/check-pr.ts index 62852d2942..d1eaf6a8d3 100644 --- a/scripts/changes/check-pr.ts +++ b/scripts/changes/check-pr.ts @@ -2,15 +2,25 @@ * Checks whether the current PR contains a change file and posts (or * updates) a sticky comment on the PR with the result. * - * Usage (called by the changes-file GitHub Actions workflow): - * node scripts/changes/check-pr.ts + * Two-phase to avoid running with write permissions in PRs from forks + * See https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ + * + * check Reads the PR file list and writes pr-check-result.json. + * Safe to run with a read-only token (Workflow A). + * + * comment Reads pr-check-result.json and posts/updates the sticky + * comment. Runs in Workflow B (workflow_run) with write + * permissions; never sees PR contents. * * Usage: - * node scripts/changes/check-pr.ts + * node scripts/changes/check-pr.ts check + * node scripts/changes/check-pr.ts comment * * Environment: - * GITHUB_TOKEN - Required. GitHub token with pull-requests:write permission. + * GITHUB_TOKEN - Required. GitHub token with appropriate permissions + * for the selected mode. */ +import * as fs from "node:fs"; import { createPrComment, getPrComments, @@ -43,21 +53,36 @@ pnpm run changes:add // Matches packages/*/.changes/*.md but not .gitkeep const CHANGE_FILE_RE = /^packages\/[^/]+\/\.changes\/[^/]+\.md$/; -async function main() { - let arg = process.argv[2]; - let prNumber = arg ? parseInt(arg, 10) : NaN; - if (!arg || isNaN(prNumber)) { - console.error("Usage: node scripts/changes/check-pr.ts "); - process.exit(1); - } +// Needs to match change-file.yml/change-file-comment.yml +const ARTIFACT_FILE = "pr-check-result.json"; // + +let [mode, arg] = process.argv.slice(2); + +if (mode === "check") { + let prNumber = parseInt(arg ?? "", 10); + if (isNaN(prNumber)) usage(); + await check(prNumber); +} else if (mode === "comment") { + if (!arg) usage(); + await comment(arg); +} else { + usage(); +} - // Check for change files via the GitHub API — no git fetch needed +async function check(prNumber: number) { let files = await getPrFiles(prNumber); let found = files.some((f) => CHANGE_FILE_RE.test(f.filename)); + console.log( + `Writing artifact to ${ARTIFACT_FILE}:`, + JSON.stringify({ prNumber, found }), + ); + fs.writeFileSync(ARTIFACT_FILE, JSON.stringify({ prNumber, found })); +} + +async function comment(resultPath: string) { + let { prNumber, found } = JSON.parse(fs.readFileSync(resultPath, "utf8")); let body = found ? COMMENT_FOUND : COMMENT_MISSING; - console.log(`Change files found: ${found}`); - // Find existing sticky comment let comments = await getPrComments(prNumber); let existing = comments.find( (c) => @@ -74,7 +99,11 @@ async function main() { } } -main().catch((err) => { - console.error("Error:", err.message); +function usage(): never { + console.error( + "Usage:\n" + + " node scripts/changes/check-pr.ts check \n" + + " node scripts/changes/check-pr.ts comment ", + ); process.exit(1); -}); +} From f95ee7ccfc61828b7f93b7f189dc5ef9469648f0 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 18 May 2026 12:33:17 -0400 Subject: [PATCH 2/2] Consolidate pr workflows (#15070) * Consolidate pr workflows * Update --- .github/workflows/change-file.yml | 48 ---- .github/workflows/close-feature-pr.yml | 28 --- ...change-file-comment.yml => pr-actions.yml} | 26 +- .github/workflows/pr-checks.yml | 53 ++++ scripts/changes/check-pr.ts | 109 -------- scripts/pr.ts | 232 ++++++++++++++++++ scripts/utils/github.ts | 14 ++ 7 files changed, 312 insertions(+), 198 deletions(-) delete mode 100644 .github/workflows/change-file.yml delete mode 100644 .github/workflows/close-feature-pr.yml rename .github/workflows/{change-file-comment.yml => pr-actions.yml} (58%) create mode 100644 .github/workflows/pr-checks.yml delete mode 100644 scripts/changes/check-pr.ts create mode 100644 scripts/pr.ts diff --git a/.github/workflows/change-file.yml b/.github/workflows/change-file.yml deleted file mode 100644 index 5c57a09c9b..0000000000 --- a/.github/workflows/change-file.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: 📝 Change File (Check) - -on: - pull_request: - branches: - - dev - types: [opened, synchronize, reopened] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - check: - name: 📝 Change File Check - if: github.repository == 'remix-run/react-router' - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - - steps: - - name: ⬇️ Checkout repo - uses: actions/checkout@v6 - - - name: 📦 Setup pnpm - uses: pnpm/action-setup@v6 - - - name: ⎔ Setup node - uses: actions/setup-node@v6 - with: - node-version-file: ".nvmrc" - cache: pnpm - - - name: 📥 Install deps - run: pnpm install --frozen-lockfile - - - name: 📝 Check for change file - env: - GITHUB_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: node scripts/changes/check-pr.ts check "$PR_NUMBER" - - - name: 📤 Upload result - uses: actions/upload-artifact@v4 - with: - name: pr-check-result - path: pr-check-result.json diff --git a/.github/workflows/close-feature-pr.yml b/.github/workflows/close-feature-pr.yml deleted file mode 100644 index 9937a8c448..0000000000 --- a/.github/workflows/close-feature-pr.yml +++ /dev/null @@ -1,28 +0,0 @@ -# Close a singular pull request that implements a feature that has not -# gone through the Proposal process -# Triggered by adding the `feature-request` label to an issue - -name: 🚪 Check Feature PR - -on: - pull_request_target: - types: [labeled] - -jobs: - close-feature-pr: - name: 🚪 Check Feature PR - if: github.repository == 'remix-run/react-router' && github.event.label.name == 'feature-request' - runs-on: ubuntu-latest - permissions: - pull-requests: write - steps: - - name: ⬇️ Checkout repo - uses: actions/checkout@v6 - - - name: 🚪 Close PR - env: - GH_TOKEN: ${{ github.token }} - run: | - gh pr comment ${{ github.event.pull_request.number }} -F ./scripts/close-feature-pr.md - gh pr edit ${{ github.event.pull_request.number }} --remove-label ${{ github.event.label.name }} - gh pr close ${{ github.event.pull_request.number }} diff --git a/.github/workflows/change-file-comment.yml b/.github/workflows/pr-actions.yml similarity index 58% rename from .github/workflows/change-file-comment.yml rename to .github/workflows/pr-actions.yml index ff51cbfeef..68d24d1e92 100644 --- a/.github/workflows/change-file-comment.yml +++ b/.github/workflows/pr-actions.yml @@ -1,29 +1,29 @@ -name: 📝 Change File (Comment) +name: PR (Actions) -# Runs after the "Change File (Check)" workflow completes. That workflow runs -# under the `pull_request` trigger with a read-only token (safe for PRs from -# forks); this workflow runs under `workflow_run` with write permissions to -# post the sticky comment, but never executes any PR code — it only reads -# the artifact produced by the upstream workflow. +# Triggered when "PR (Check)" completes. Downloads the artifact produced by +# the upstream workflow and applies the recorded actions (sticky comments, +# label changes, close, etc.) with write permissions. Never executes PR +# source code — only reads the JSON artifact. on: workflow_run: - workflows: ["📝 Change File (Check)"] + workflows: ["PR (Check)"] types: [completed] jobs: - comment: - name: 📝 Post Comment + actions: + name: PR (Actions) if: > github.event.workflow_run.conclusion == 'success' && github.repository == 'remix-run/react-router' runs-on: ubuntu-latest permissions: - actions: read # Read artifact issues: write pull-requests: write + actions: read steps: + # Check's out the base (main) branch - not the PR branch - name: ⬇️ Checkout repo uses: actions/checkout@v6 @@ -42,11 +42,11 @@ jobs: - name: 📥 Download result from upstream workflow uses: actions/download-artifact@v4 with: - name: pr-check-result + name: pr-checks-result github-token: ${{ github.token }} run-id: ${{ github.event.workflow_run.id }} - - name: 💬 Post comment + - name: 💬 Apply actions env: GITHUB_TOKEN: ${{ github.token }} - run: node scripts/changes/check-pr.ts comment pr-check-result.json + run: node scripts/pr.ts actions pr-checks-result.json diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000000..ef7785e294 --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,53 @@ +name: PR (Check) + +# Read-only PR inspection. Computes a list of "actions" (sticky comments, +# label changes, close, etc.) and uploads them as an artifact. The PR (Actions) +# workflow consumes the artifact and applies the actions with write +# permissions — keeping that step out of the PR's untrusted code path. + +on: + pull_request: + types: [opened, synchronize, reopened, labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.action }}-${{ github.event.label.name }} + cancel-in-progress: true + +jobs: + check: + name: 🔍 Check PR + if: github.repository == 'remix-run/react-router' + runs-on: ubuntu-latest + permissions: + pull-requests: read + + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v6 + + - name: 📦 Setup pnpm + uses: pnpm/action-setup@v6 + + - name: ⎔ Setup node + uses: actions/setup-node@v6 + with: + node-version-file: ".nvmrc" + cache: pnpm + + - name: 📥 Install deps + run: pnpm install --frozen-lockfile + + - name: 🔍 Run checks + env: + GITHUB_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_BASE: ${{ github.event.pull_request.base.ref }} + EVENT_ACTION: ${{ github.event.action }} + LABEL_NAME: ${{ github.event.label.name }} + run: node scripts/pr.ts check + + - name: 📤 Upload result + uses: actions/upload-artifact@v4 + with: + name: pr-checks-result + path: pr-checks-result.json diff --git a/scripts/changes/check-pr.ts b/scripts/changes/check-pr.ts deleted file mode 100644 index d1eaf6a8d3..0000000000 --- a/scripts/changes/check-pr.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Checks whether the current PR contains a change file and posts (or - * updates) a sticky comment on the PR with the result. - * - * Two-phase to avoid running with write permissions in PRs from forks - * See https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ - * - * check Reads the PR file list and writes pr-check-result.json. - * Safe to run with a read-only token (Workflow A). - * - * comment Reads pr-check-result.json and posts/updates the sticky - * comment. Runs in Workflow B (workflow_run) with write - * permissions; never sees PR contents. - * - * Usage: - * node scripts/changes/check-pr.ts check - * node scripts/changes/check-pr.ts comment - * - * Environment: - * GITHUB_TOKEN - Required. GitHub token with appropriate permissions - * for the selected mode. - */ -import * as fs from "node:fs"; -import { - createPrComment, - getPrComments, - getPrFiles, - updatePrComment, -} from "../utils/github.ts"; - -const COMMENT_MARKER = ""; - -const COMMENT_FOUND = `${COMMENT_MARKER} -### ✅ Change File Found - -A [change file](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files) file exists in this PR. Thanks!`; - -const COMMENT_MISSING = `${COMMENT_MARKER} -### ⚠️ No Change File Found - -This PR doesn't include a [change file](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files) which is used for automated release notes. -If your change affects users, please add one (or more) change files and commit the generated file(s). - -\`\`\`sh -pnpm run changes:add -\`\`\` - -> This script requires Node 24+. If you are on a lower version, please [add a file manually](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files) - -> Not every PR needs a change file — you can skip this step if the change is internal-only -> (tests, tooling, docs)`; - -// Matches packages/*/.changes/*.md but not .gitkeep -const CHANGE_FILE_RE = /^packages\/[^/]+\/\.changes\/[^/]+\.md$/; - -// Needs to match change-file.yml/change-file-comment.yml -const ARTIFACT_FILE = "pr-check-result.json"; // - -let [mode, arg] = process.argv.slice(2); - -if (mode === "check") { - let prNumber = parseInt(arg ?? "", 10); - if (isNaN(prNumber)) usage(); - await check(prNumber); -} else if (mode === "comment") { - if (!arg) usage(); - await comment(arg); -} else { - usage(); -} - -async function check(prNumber: number) { - let files = await getPrFiles(prNumber); - let found = files.some((f) => CHANGE_FILE_RE.test(f.filename)); - console.log( - `Writing artifact to ${ARTIFACT_FILE}:`, - JSON.stringify({ prNumber, found }), - ); - fs.writeFileSync(ARTIFACT_FILE, JSON.stringify({ prNumber, found })); -} - -async function comment(resultPath: string) { - let { prNumber, found } = JSON.parse(fs.readFileSync(resultPath, "utf8")); - let body = found ? COMMENT_FOUND : COMMENT_MISSING; - - let comments = await getPrComments(prNumber); - let existing = comments.find( - (c) => - c.user?.login === "github-actions[bot]" && - c.body?.includes(COMMENT_MARKER), - ); - - if (existing) { - console.log(`Updating existing comment #${existing.id}`); - await updatePrComment(existing.id, body); - } else { - console.log("Creating new comment"); - await createPrComment(prNumber, body); - } -} - -function usage(): never { - console.error( - "Usage:\n" + - " node scripts/changes/check-pr.ts check \n" + - " node scripts/changes/check-pr.ts comment ", - ); - process.exit(1); -} diff --git a/scripts/pr.ts b/scripts/pr.ts new file mode 100644 index 0000000000..42b5339630 --- /dev/null +++ b/scripts/pr.ts @@ -0,0 +1,232 @@ +/** + * Runs a set of checks against a PR and applies the resulting actions. + * + * Two-phase to avoid running with write permissions on PRs from forks. + * See https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ + * + * check Inspects the PR via the GitHub API and writes a list of + * "actions" to pr-checks-result.json. Safe to run with a + * read-only token (Workflow A: 🔍 Check PR). + * + * actions Reads pr-checks-result.json and applies each action. Runs + * in Workflow B (PR (Actions)) under `workflow_run` with + * write permissions but never executes any PR code. + * + * Usage: + * node scripts/pr.ts check + * node scripts/pr.ts actions + * + * Environment (check): + * GITHUB_TOKEN - Required (read-only PR scope is enough). + * PR_NUMBER - Required. github.event.pull_request.number + * PR_BASE - Required. github.event.pull_request.base.ref + * EVENT_ACTION - Required. github.event.action (opened|synchronize|reopened|labeled) + * LABEL_NAME - Optional. github.event.label.name (set when EVENT_ACTION=labeled) + * + * Environment (actions): + * GITHUB_TOKEN - Required (issues:write + pull-requests:write). + */ +import * as fs from "node:fs"; +import * as util from "node:util"; + +import { + closePr, + createPrComment, + getPrComments, + getPrFiles, + removePrLabel, + updatePrComment, +} from "./utils/github.ts"; + +type Action = + | { type: "upsert-sticky-comment"; marker: string; body: string } + | { type: "create-comment"; body: string } + | { type: "remove-label"; label: string } + | { type: "close-pr" }; + +type CheckContext = { + prNumber: number; + baseBranch: string; + eventAction: string; + labelName: string; +}; + +type Check = (ctx: CheckContext) => Promise; + +const CHANGE_FILE_MARKER = ""; + +const CHANGE_FILE_FOUND_COMMENT = `${CHANGE_FILE_MARKER} +### ✅ Change File Found + +A [change file](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files) file exists in this PR. Thanks!`; + +const CHANGE_FILE_MISSING_COMMENT = `${CHANGE_FILE_MARKER} +### ⚠️ No Change File Found + +This PR doesn't include a [change file](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files) which is used for automated release notes. +If your change affects users, please add one (or more) change files and commit the generated file(s). + +\`\`\`sh +pnpm run changes:add +\`\`\` + +> This script requires Node 24+. If you are on a lower version, please [add a file manually](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files) + +> Not every PR needs a change file — you can skip this step if the change is internal-only +> (tests, tooling, docs)`; + +const CLOSE_FEATURE_PR_COMMENT = `\ +To align with our new [Open Governance](https://remix.run/blog/rr-governance) model, we are now asking that all new features go through the [Proposal/RFC process](https://github.com/remix-run/react-router/blob/main/GOVERNANCE.md#new-feature-process) and that we don't open PRs until a proposal has been accepted and advanced to Stage 1. + +If this feature doesn't have a Proposal, please [open one](https://github.com/remix-run/react-router/discussions/new?category=proposals) so we can evaluate/discuss the proposed feature. You can link to this PR as an example of a potential implementation and we can re-open it if the proposal advances. + +If this PR already has a Proposal but it has not yet been accepted, let's continue the discussion in the Proposal until it gets accepted and then we can look to open a PR. Feel free to link to this PR or to a branch in a forked repo to show what a potential implementation might look like. + +If you have any questions, you can always reach out on [Discord](https://rmx.as/discord). Thanks again for providing feedback and helping us make React Router even better! +`; + +let { positionals } = util.parseArgs({ allowPositionals: true }); +let [mode, arg] = positionals; + +if (mode === "check") { + await runChecks(); +} else if (mode === "actions") { + if (!arg) usage(); + await runActions(arg); +} else { + usage(); +} + +// ---------- Checks ---------- + +async function runChecks() { + let prNumber = parseInt(requireEnv("PR_NUMBER"), 10); + if (isNaN(prNumber)) { + console.error("PR_NUMBER must be numeric"); + process.exit(1); + } + + let checks: Check[] = [changeFileCheck, featurePrCheck]; + + let ctx: CheckContext = { + prNumber, + baseBranch: requireEnv("PR_BASE"), + eventAction: requireEnv("EVENT_ACTION"), + labelName: process.env.LABEL_NAME ?? "", + }; + console.log("ctx:", ctx); + + let result: { prNumber: number; actions: Action[] } = { + prNumber, + actions: [], + }; + + for (let check of checks) { + result.actions.push(...(await check(ctx))); + } + + // Matches pr-checks.yml/pr-actions.yml workflow artifact name + let filename = "pr-checks-result.json"; + console.log(`Writing ${filename}:`, JSON.stringify(result)); + fs.writeFileSync(filename, JSON.stringify(result)); +} + +async function changeFileCheck(ctx: CheckContext): Promise { + if (ctx.baseBranch !== "dev") return []; + if (!["opened", "synchronize", "reopened"].includes(ctx.eventAction)) { + return []; + } + + let files = await getPrFiles(ctx.prNumber); + let regex = /^packages\/[^/]+\/\.changes\/[^/]+\.md$/; + let found = files.some((f) => regex.test(f.filename)); + console.log(`changeFileCheck: found=${found}`); + return [ + { + type: "upsert-sticky-comment", + marker: CHANGE_FILE_MARKER, + body: found ? CHANGE_FILE_FOUND_COMMENT : CHANGE_FILE_MISSING_COMMENT, + }, + ]; +} + +async function featurePrCheck(ctx: CheckContext): Promise { + if (ctx.eventAction !== "labeled") return []; + if (ctx.labelName !== "feature-request") return []; + + console.log(`featurePrCheck: closing PR ${ctx.prNumber}`); + return [ + { type: "create-comment", body: CLOSE_FEATURE_PR_COMMENT }, + { type: "remove-label", label: ctx.labelName }, + { type: "close-pr" }, + ]; +} + +// ---------- Action dispatch ---------- + +async function runActions(resultPath: string) { + let { prNumber, actions } = JSON.parse( + fs.readFileSync(resultPath, "utf8"), + ) as { prNumber: number; actions: Action[] }; + + if (actions.length === 0) { + console.log("No actions to apply"); + return; + } + + for (let action of actions) { + switch (action.type) { + case "upsert-sticky-comment": { + let comments = await getPrComments(prNumber); + let existing = comments.find( + (c) => + c.user?.login === "github-actions[bot]" && + c.body?.includes(action.marker), + ); + if (existing) { + console.log(`Updating sticky comment #${existing.id}`); + await updatePrComment(existing.id, action.body); + } else { + console.log("Creating sticky comment"); + await createPrComment(prNumber, action.body); + } + return; + } + case "create-comment": { + console.log("Creating comment"); + await createPrComment(prNumber, action.body); + return; + } + case "remove-label": { + console.log(`Removing label '${action.label}'`); + await removePrLabel(prNumber, action.label); + return; + } + case "close-pr": { + console.log(`Closing PR ${prNumber}`); + await closePr(prNumber); + return; + } + } + } +} + +// Utils + +function usage(): never { + console.error( + "Usage:\n" + + " node scripts/pr.ts check\n" + + " node scripts/pr.ts actions ", + ); + process.exit(1); +} + +function requireEnv(name: string): string { + let value = process.env[name]; + if (!value) { + console.error(`Missing required env var: ${name}`); + process.exit(1); + } + return value; +} diff --git a/scripts/utils/github.ts b/scripts/utils/github.ts index b100b0eeb3..4338937465 100644 --- a/scripts/utils/github.ts +++ b/scripts/utils/github.ts @@ -237,3 +237,17 @@ export async function deletePrComment(commentId: number) { comment_id: commentId, }); } + +/** + * Remove a label from a PR (or issue) + */ +export async function removePrLabel(prNumber: number, label: string) { + await request( + "DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}", + { + ...requestOptions(), + issue_number: prNumber, + name: label, + }, + ); +}