diff --git a/.agents/skills/finish-line/SKILL.md b/.agents/skills/finish-line/SKILL.md new file mode 100644 index 0000000000..a1fa454002 --- /dev/null +++ b/.agents/skills/finish-line/SKILL.md @@ -0,0 +1,140 @@ +--- +name: finish-line +description: Bring a blocked React Router community pull request across the finish line. Use when the user invokes `/finish-line` or `$finish-line`, provides a PR number or URL, and asks Codex to resolve merge blockers such as an unsigned CLA, missing change file, missing documentation, or stale contributor follow-up. Handles deciding whether to push small maintainer fixes onto the contributor PR branch or recreate the PR from main under a maintainer branch when the contributor's CLA is not signed. +--- + +# Finish Line + +## Overview + +Finish blocked community PRs in `remix-run/react-router` while respecting contributor ownership, CLA constraints, and the repo's PR packaging conventions. + +Treat the PR number or URL in `$ARGUMENTS` as the target PR. If no target is provided, ask for it before doing anything. + +## Triage + +1. Inspect local state with `git status --short --branch`. If unrelated dirty files exist, do not overwrite or stage them. +2. Fetch current main before making branch decisions: + +```sh +git fetch origin main +``` + +3. Gather PR context: + +```sh +gh pr view --repo remix-run/react-router --json number,title,body,state,isDraft,author,baseRefName,headRefName,headRepository,headRepositoryOwner,maintainerCanModify,mergeStateStatus,reviewDecision,labels,files,commits,statusCheckRollup,url +gh pr checks --repo remix-run/react-router +gh pr diff --repo remix-run/react-router --stat +gh pr view --repo remix-run/react-router --comments +``` + +4. Identify merge blockers. In particular: + +- If a CLA check or comment shows the author has not signed the CLA, use the unsigned-CLA replacement workflow. +- If the PR only needs repo-maintainer additions such as a change file or docs, use the contributor-branch workflow. +- If the blocker is unclear, summarize the evidence and ask the user which path to take. + +5. Evaluate test coverage before deciding the finish-line changes: + - Inspect the PR diff, changed files, existing nearby tests, review comments, and failed checks for test expectations. + - If the PR changes runtime behavior, build/plugin behavior, routing semantics, generated types, RSC behavior, docs rendering, or any bug/feature surface that can regress, add or preserve a focused test unless equivalent coverage already exists. + - If tests are already included, verify they exercise the changed behavior and cover the affected React Router mode(s): Declarative, Data, Framework, RSC Data, and/or RSC Framework. + - If tests are not needed because the change is documentation-only, packaging-only, a change file, or otherwise not executable behavior, note that rationale in the final report. + - If a useful test is required but too large or risky for the finish-line scope, stop and ask the user before broadening the PR. + +## Unsigned CLA Replacement + +Use this path when the PR author's CLA is not signed. Do not merge, cherry-pick, rebase, or push the contributor's commits. Use the PR diff as the behavior/content reference and recreate the final file changes in maintainer-authored commits from current `origin/main`. + +1. Save the original PR title, body, labels, changed-file list, and diff for reference. +2. Create a fresh branch from current main: + +```sh +git checkout -B brophdawg11/finish-line-pr- origin/main +``` + +3. Recreate the same resulting changes on the fresh branch. Keep the implementation as close as possible to the original PR unless main has moved and a tiny adaptation is required. +4. Add any missing finish-line work, such as tests, a change file, or docs, if those are also required. +5. Run focused validation that matches the touched area. Prefer the narrowest meaningful test/build command. +6. Commit the recreated changes with a concise imperative subject. +7. Before pushing/opening the replacement PR, read `.agents/skills/create-pr/SKILL.md` and follow its current branch, PR body, and label guidance unless this skill gives a more specific instruction for replacement PRs. +8. Push the maintainer branch and open a replacement PR against `main`. + - Reuse the original title unless it is misleading. + - Use a similar description, but make it clear this is a agent/maintainer-authored replacement. + - Include the old PR number in the description (`#`). + - Default to a ready PR when validation passed and the original PR was otherwise mergeable; use a draft PR if validation is incomplete or the original PR was draft. + - Apply the relevant labels from the original PR plus any package/feature labels required by `.agents/skills/create-pr/SKILL.md`. +9. Comment on the original PR and close it after the replacement PR exists: + +```markdown +Thanks for the PR! We can't merge this without the CLA being signed, so we're going to re-implement this work in # to keep this moving. +``` + +Then run: + +```sh +gh pr comment --repo remix-run/react-router --body-file +gh pr close --repo remix-run/react-router +``` + +## Contributor-Branch Workflow + +Use this path when the contributor's CLA is signed and the missing work is small maintainer follow-up, such as a change file or docs. + +1. Check out the PR branch: + +```sh +gh pr checkout --repo remix-run/react-router +``` + +2. Confirm the branch and local state: + +```sh +git status --short --branch +git branch --show-current +``` + +3. Make only the missing finish-line changes, including focused tests when the coverage evaluation requires them. Do not refactor the contributor's work unless it is necessary to unblock mergeability and the user agrees. +4. Validate the exact content with the user before pushing: + - For a change file, show the package path, file name, change type, and full markdown contents. + - For docs, show the affected files and the relevant prose/API snippets. + - For tests, show the test file path, the behavior covered, and the mode(s) covered. + - Ask explicitly for approval to commit and push back to the PR branch. +5. After approval, run focused validation when appropriate, commit the maintainer follow-up, and push to the PR branch. If `git push` fails because the contributor branch cannot be modified, stop and report the failure instead of opening a replacement PR unless the user approves that pivot. + +## Change Files + +Create change files under the affected package: + +```text +packages//.changes/..md +``` + +Use `patch`, `minor`, `major`, or `unstable` for ``. For bug fixes and narrow behavior fixes, default to `patch`. Keep the content concise: + +```markdown +Brief description of the user-facing change +``` + +If the PR spans multiple packages, prefer the package with the direct user-facing API or runtime behavior. Ask the user when the package or change type is not obvious. + +## Documentation + +Do not add documentation by default for ordinary bug fixes. Add docs when the PR changes a documented API, introduces new behavior users need to discover, changes examples, or the user/reviewer explicitly requested docs. + +Follow repo docs rules: + +- Edit source docs or JSDoc, not generated `docs/api/` output. +- Include mode context when adding docs for React Router behavior: Declarative, Data, Framework, RSC Data, or RSC Framework. +- For API docs generated from JSDoc, edit `packages/react-router/lib/` comments and note that `pnpm run docs` may be required. + +## Final Report + +Report the path taken and the current PR state: + +- Original PR number and blocker. +- Whether changes were pushed to the contributor branch or a replacement PR was opened. +- Branch, commit hash, and PR URL when applicable. +- Any old-PR comment/close action taken. +- Validation performed or skipped. +- Test coverage decision: added, already present, or intentionally omitted with rationale. diff --git a/.github/workflows/pr-actions.yml b/.github/workflows/pr-actions.yml index ec44f0f392..b23ab38d79 100644 --- a/.github/workflows/pr-actions.yml +++ b/.github/workflows/pr-actions.yml @@ -12,7 +12,7 @@ on: jobs: actions: - name: Actions + name: PR Actions if: > contains(fromJSON('["success","failure"]'), github.event.workflow_run.conclusion) && github.repository == 'remix-run/react-router' diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index ae7f383b70..6fc9ea504f 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -10,13 +10,18 @@ on: types: [opened, synchronize, reopened, labeled] concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.action }}-${{ github.event.label.name }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: check: - name: Checks - if: github.repository == 'remix-run/react-router' + name: PR Checks + if: > + github.repository == 'remix-run/react-router' && + ( + github.event.action != 'labeled' || + github.event.label.name == 'feature-request' + ) runs-on: ubuntu-latest permissions: pull-requests: read @@ -80,7 +85,6 @@ jobs: PR_HEAD_OWNER: ${{ github.event.pull_request.head.repo.owner.login }} PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.name }} PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} - EVENT_ACTION: ${{ github.event.action }} LABEL_NAME: ${{ github.event.label.name }} run: node scripts/pr.ts check pr-checks-result.json diff --git a/AGENTS.md b/AGENTS.md index e8ae5037a2..94f705c638 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -121,6 +121,10 @@ export default [ Test both states (on/off) for future flags. Don't break existing behavior without a flag. +## Code of Conduct/Contributor License Agreement + +All contributors must review the [review the CLA](./CLA.md) and sign it by [adding their github username to `contributors.yml`](./contributors.yml). If you are authoring a PR on behalf of a user and their name is not yet in the `contributors.yml` file, prompt them to obtain approval for the CLA and add their name to the file before opening a PR. + ## Change Files When making changes that affect users, create a change file at `packages//.changes/..md`. `` should be either `patch`, `minor`, `major` or `unstable` to indicate the type of API change being made. If iterating on a change that hasn't shipped yet, update the existing change file instead of creating a new one. @@ -136,6 +140,7 @@ Brief description of the change ## Branching - **`main`**: Active Development +- **`v7`**: v7.x maintenance - **`v6`**: v6.x maintenance - Branch from `main` for code and docs changes diff --git a/docs/community/contributing.md b/docs/community/contributing.md index 474ee6a108..30ae6d1f8f 100644 --- a/docs/community/contributing.md +++ b/docs/community/contributing.md @@ -76,6 +76,10 @@ major.require-node-24.md unstable.update-unstable-api.md ``` +## Code of Conduct/Contributor License Agreement + +All contributors must review the [review the CLA](https://github.com/remix-run/react-router/blob/main/CLA.md) and sign it by [adding their github username to `contributors.yml`](https://github.com/remix-run/react-router/blob/main/contributors.yml). + ### Docs + Examples All commits that change or add to the API must be done in a pull request that also updates all relevant examples and docs. diff --git a/packages/create-react-router/.changes/minor.detect-nub-package-manager.md b/packages/create-react-router/.changes/minor.detect-nub-package-manager.md new file mode 100644 index 0000000000..57fc984e5d --- /dev/null +++ b/packages/create-react-router/.changes/minor.detect-nub-package-manager.md @@ -0,0 +1 @@ +Detect nub as a supported package manager when creating new projects diff --git a/packages/create-react-router/__tests__/create-react-router-test.ts b/packages/create-react-router/__tests__/create-react-router-test.ts index 525d16b786..2aa4873922 100644 --- a/packages/create-react-router/__tests__/create-react-router-test.ts +++ b/packages/create-react-router/__tests__/create-react-router-test.ts @@ -818,6 +818,39 @@ describe("create-react-router CLI", () => { process.env.npm_config_user_agent = originalUserAgent; }); + it("recognizes when nub was used to run the command", async () => { + let originalUserAgent = process.env.npm_config_user_agent; + process.env.npm_config_user_agent = + "nub/0.1.0 npm/? node/v24.0.0 linux x64"; + + let projectDir = getProjectDir("nub-create-from-user-agent"); + + mockSpawnSuccess(); + + // Suppress terminal output + let stdoutMock = jest + .spyOn(process.stdout, "write") + .mockImplementation(() => true); + + await createReactRouter([ + projectDir, + "--template", + path.join(__dirname, "fixtures", "blank"), + "--no-git-init", + "--yes", + "--no-agent-skills", + ]); + + stdoutMock.mockReset(); + + expect(mockedSpawn).toHaveBeenCalledWith( + "nub", + expect.arrayContaining(["install"]), + expect.anything(), + ); + process.env.npm_config_user_agent = originalUserAgent; + }); + it("supports specifying the package manager, regardless of user agent", async () => { let originalUserAgent = process.env.npm_config_user_agent; process.env.npm_config_user_agent = diff --git a/packages/create-react-router/index.ts b/packages/create-react-router/index.ts index 5e334caef7..ed4c6d0392 100644 --- a/packages/create-react-router/index.ts +++ b/packages/create-react-router/index.ts @@ -159,9 +159,8 @@ async function getContext(argv: string[]): Promise { noMotion: getBooleanArg(values["no-motion"]), pkgManager: validatePackageManager( getStringArg(values["package-manager"]) ?? - // npm, pnpm, Yarn, Bun and Deno (v2.0.5+) set the user agent environment variable that can be used - // to determine which package manager ran the command. - (process.env.npm_config_user_agent ?? "npm").split("/")[0], + detectPackageManager() ?? + "npm", ), projectName, prompt, @@ -574,13 +573,43 @@ async function doneStep(ctx: Context) { await sleep(200); } -const validPackageManagers = ["npm", "yarn", "pnpm", "bun", "deno"] as const; +const validPackageManagers = [ + "npm", + "yarn", + "pnpm", + "bun", + "deno", + "nub", +] as const; type PackageManager = (typeof validPackageManagers)[number]; function validatePackageManager(pkgManager: string): PackageManager { return validPackageManagers.find((name) => pkgManager === name) ?? "npm"; } +/** + * Determine which package manager the user prefers. + * + * npm, pnpm, Yarn, Bun, Deno, and nub set the user agent environment variable + * that can be used to determine which package manager ran the command. + */ +function detectPackageManager(): PackageManager | undefined { + let { npm_config_user_agent } = process.env; + if (!npm_config_user_agent) return undefined; + try { + let pkgManager = npm_config_user_agent.split("/")[0]; + if (pkgManager === "npm") return "npm"; + if (pkgManager === "pnpm") return "pnpm"; + if (pkgManager === "yarn") return "yarn"; + if (pkgManager === "bun") return "bun"; + if (pkgManager === "deno") return "deno"; + if (pkgManager === "nub") return "nub"; + return undefined; + } catch { + return undefined; + } +} + async function installDependencies({ pkgManager, cwd, diff --git a/packages/react-router-dev/.changes/minor.detect-nub-package-manager.md b/packages/react-router-dev/.changes/minor.detect-nub-package-manager.md new file mode 100644 index 0000000000..ad0907e34b --- /dev/null +++ b/packages/react-router-dev/.changes/minor.detect-nub-package-manager.md @@ -0,0 +1 @@ +Detect nub as a supported package manager when installing framework dependencies diff --git a/packages/react-router-dev/.changes/patch.properly-detect-user-rolldownoptions-config-vite.md b/packages/react-router-dev/.changes/patch.properly-detect-user-rolldownoptions-config-vite.md new file mode 100644 index 0000000000..9c429f9aa2 --- /dev/null +++ b/packages/react-router-dev/.changes/patch.properly-detect-user-rolldownoptions-config-vite.md @@ -0,0 +1 @@ +Properly detect user `rolldownOptions` config in Vite 8+ diff --git a/packages/react-router-dev/__tests__/detect-package-manager-test.ts b/packages/react-router-dev/__tests__/detect-package-manager-test.ts new file mode 100644 index 0000000000..dd434a07f8 --- /dev/null +++ b/packages/react-router-dev/__tests__/detect-package-manager-test.ts @@ -0,0 +1,31 @@ +import { detectPackageManager } from "../cli/detectPackageManager"; + +describe("detectPackageManager", () => { + let originalUserAgent = process.env.npm_config_user_agent; + + afterEach(() => { + process.env.npm_config_user_agent = originalUserAgent; + }); + + it.each(["npm", "pnpm", "yarn", "bun", "nub"] as const)( + "detects %s from the user agent", + (packageManager) => { + process.env.npm_config_user_agent = `${packageManager}/1.0.0 npm/? node/v24.0.0 linux x64`; + + expect(detectPackageManager()).toBe(packageManager); + }, + ); + + it("returns undefined for unknown package managers", () => { + process.env.npm_config_user_agent = + "unknown/1.0.0 npm/? node/v24.0.0 linux x64"; + + expect(detectPackageManager()).toBeUndefined(); + }); + + it("returns undefined without a user agent", () => { + process.env.npm_config_user_agent = undefined; + + expect(detectPackageManager()).toBeUndefined(); + }); +}); diff --git a/packages/react-router-dev/cli/detectPackageManager.ts b/packages/react-router-dev/cli/detectPackageManager.ts index 79b7c8277e..c271d1ac48 100644 --- a/packages/react-router-dev/cli/detectPackageManager.ts +++ b/packages/react-router-dev/cli/detectPackageManager.ts @@ -1,9 +1,9 @@ -type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; +type PackageManager = "npm" | "pnpm" | "yarn" | "bun" | "nub"; /** * Determine which package manager the user prefers. * - * npm, pnpm and Yarn set the user agent environment variable + * npm, pnpm, Yarn, Bun, and nub set the user agent environment variable * that can be used to determine which package manager ran * the command. */ @@ -16,6 +16,7 @@ export const detectPackageManager = (): PackageManager | undefined => { if (pkgManager === "pnpm") return "pnpm"; if (pkgManager === "yarn") return "yarn"; if (pkgManager === "bun") return "bun"; + if (pkgManager === "nub") return "nub"; return undefined; } catch { return undefined; diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index 682dcc4420..4c785aee35 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -73,7 +73,12 @@ import { getRouteChunkModuleId, getRouteChunkNameFromModuleId, } from "./route-chunks"; -import { preloadVite, getVite, defineCompilerOptions } from "./vite"; +import { + preloadVite, + getVite, + defineCompilerOptions, + getUserBuildRollupOptions, +} from "./vite"; import { type ResolvedReactRouterConfig, type BuildManifest, @@ -3609,8 +3614,9 @@ export async function getEnvironmentOptionsResolvers( ssrEmitAssets: true, copyPublicDir: false, // The client only uses assets in the public directory rollupOptions: { + // prettier-ignore input: - viteUserConfig.environments?.ssr?.build?.rollupOptions?.input ?? + getUserBuildRollupOptions(viteUserConfig.environments?.ssr)?.input ?? virtual.serverBuild.id, output: { entryFileNames: serverBuildFile, @@ -3653,29 +3659,31 @@ export async function getEnvironmentOptionsResolvers( }, ), ], - output: viteUserConfig?.environments?.client?.build?.rollupOptions - ?.output ?? { - entryFileNames: ({ moduleIds }) => { - let routeChunkModuleId = moduleIds.find(isRouteChunkModuleId); - let routeChunkName = routeChunkModuleId - ? getRouteChunkNameFromModuleId(routeChunkModuleId)?.replace( - "unstable_", - "", - ) - : null; - let routeChunkSuffix = routeChunkName - ? `-${kebabCase(routeChunkName)}` - : ""; - let assetsDir = - viteUserConfig?.environments?.client?.build?.assetsDir ?? - viteUserConfig?.build?.assetsDir ?? - "assets"; - return path.posix.join( - assetsDir, - `[name]${routeChunkSuffix}-[hash].js`, - ); + // prettier-ignore + output: + getUserBuildRollupOptions(viteUserConfig?.environments?.client)?.output ?? + { + entryFileNames: ({ moduleIds }) => { + let routeChunkModuleId = moduleIds.find(isRouteChunkModuleId); + let routeChunkName = routeChunkModuleId + ? getRouteChunkNameFromModuleId(routeChunkModuleId)?.replace( + "unstable_", + "", + ) + : null; + let routeChunkSuffix = routeChunkName + ? `-${kebabCase(routeChunkName)}` + : ""; + let assetsDir = + viteUserConfig?.environments?.client?.build?.assetsDir ?? + viteUserConfig?.build?.assetsDir ?? + "assets"; + return path.posix.join( + assetsDir, + `[name]${routeChunkSuffix}-[hash].js`, + ); + }, }, - }, }, outDir: getClientBuildDirectory(ctx.reactRouterConfig), }, diff --git a/packages/react-router-dev/vite/vite.ts b/packages/react-router-dev/vite/vite.ts index cdcbd8b0e5..2c40bb11d5 100644 --- a/packages/react-router-dev/vite/vite.ts +++ b/packages/react-router-dev/vite/vite.ts @@ -1,6 +1,11 @@ import { createRequire } from "node:module"; import path from "pathe"; -import type { DepOptimizationConfig, ESBuildOptions } from "vite"; +import type { + DepOptimizationConfig, + ESBuildOptions, + EnvironmentOptions, + BuildEnvironmentOptions, +} from "vite"; import invariant from "../invariant"; import { isReactRouterRepo } from "../config/is-react-router-repo"; @@ -73,3 +78,22 @@ export function defineOptimizeDepsCompilerOptions(options: { ? { rolldownOptions: options.rolldown } : { esbuildOptions: options.esbuild }; } + +/** + * Read the user-supplied build options from either `rollupOptions` (Vite <=7) + * or `rolldownOptions` (Vite >=8). + */ +export function getUserBuildRollupOptions( + environment: EnvironmentOptions | undefined, +): BuildEnvironmentOptions["rollupOptions"] | undefined { + if (environment?.build) { + if ("rollupOptions" in environment.build) { + return environment.build.rollupOptions; + } + if ("rolldownOptions" in environment.build) { + return environment.build + .rolldownOptions as BuildEnvironmentOptions["rollupOptions"]; + } + } + return undefined; +} diff --git a/packages/react-router/.changes/patch.encode-href-params.md b/packages/react-router/.changes/patch.encode-href-params.md new file mode 100644 index 0000000000..c810872df0 --- /dev/null +++ b/packages/react-router/.changes/patch.encode-href-params.md @@ -0,0 +1,3 @@ +Fix `href()` to properly stringify and URL-encode param values, matching `generatePath()` + +- splat params preserve path separators while encoding each segment individually diff --git a/packages/react-router/__tests__/href-test.ts b/packages/react-router/__tests__/href-test.ts index e674abbaa7..50d631ec93 100644 --- a/packages/react-router/__tests__/href-test.ts +++ b/packages/react-router/__tests__/href-test.ts @@ -1,4 +1,5 @@ import { href } from "../lib/href"; +import { matchPath } from "../lib/router/utils"; describe("href", () => { it("works with param-less paths", () => { @@ -40,4 +41,40 @@ describe("href", () => { it("works with periods", () => { expect(href("/a/:b.zip", { b: "hello" })).toBe("/a/hello.zip"); }); + + it("encodes param values that contain a /", () => { + expect(href("/products/:id", { id: "shoes/2026-summer" })).toBe( + "/products/shoes%2F2026-summer", + ); + }); + + it("encodes param values that contain # or ?", () => { + expect(href("/products/:id", { id: "abc#frag" })).toBe( + "/products/abc%23frag", + ); + expect(href("/products/:id", { id: "abc?x=1" })).toBe( + "/products/abc%3Fx%3D1", + ); + }); + + it("encodes splat param values while preserving segments", () => { + expect(href("/:param/*", { param: "a?b/c#d", "*": "e?f/g#h" })).toBe( + "/a%3Fb%2Fc%23d/e%3Ff/g%23h", + ); + }); + + it("round-trips through matchPath for param values with special characters", () => { + let pattern = "/products/:id"; + for (let id of ["shoes/2026-summer", "abc#frag", "abc?x=1", "a b"]) { + let result = href(pattern, { id }); + // before the fix, href()'s own output didn't match its own pattern + let match = matchPath(pattern, result); + expect(match).not.toBeNull(); + expect(decodeURIComponent(match!.params.id!)).toBe(id); + } + }); + + it("coerces values to strings", () => { + expect(href("/:a/:b", { a: 1, b: true })).toBe("/1/true"); + }); }); diff --git a/packages/react-router/lib/href.ts b/packages/react-router/lib/href.ts index 92ecdc0ee0..ee82c6d743 100644 --- a/packages/react-router/lib/href.ts +++ b/packages/react-router/lib/href.ts @@ -12,6 +12,10 @@ type ToArgs> = // otherwise, require `params` arg [Params]; +function stringify(p: any) { + return p == null ? "" : typeof p === "string" ? p : String(p); +} + /** Returns a resolved URL path for the specified route. @@ -38,16 +42,18 @@ export function href( `Path '${path}' requires param '${param}' but it was not provided`, ); } - return value === undefined ? "" : "/" + value; + return value == null ? "" : "/" + encodeURIComponent(stringify(value)); }, ); if (path.endsWith("*")) { // treat trailing splat the same way as compilePath, and force it to be as if it were `/*`. - // `react-router typegen` will not generate the params for a malformed splat, causing a type error, but we can still do the correct thing here. + // `react-router typegen` will not generate the params for a malformed splat, + // causing a type error, but we can still do the correct thing here. const value = params?.["*"]; if (value !== undefined) { - result += "/" + value; + result += + "/" + stringify(value).split("/").map(encodeURIComponent).join("/"); } } diff --git a/scripts/pr.ts b/scripts/pr.ts index baf7cb934b..c23cd79352 100644 --- a/scripts/pr.ts +++ b/scripts/pr.ts @@ -24,8 +24,7 @@ * PR_HEAD_OWNER - Required. github.event.pull_request.head.repo.owner.login * PR_HEAD_REPO - Required. github.event.pull_request.head.repo.name * PR_HEAD_REF - Required. github.event.pull_request.head.ref - * EVENT_ACTION - Required. github.event.action (opened|synchronize|reopened|labeled) - * LABEL_NAME - Optional. github.event.label.name (set when EVENT_ACTION=labeled) + * LABEL_NAME - Optional. github.event.label.name (set when github.event.action == "labeled") * * Environment (actions): * GITHUB_TOKEN - Required (issues:write + pull-requests:write). @@ -57,7 +56,6 @@ type CheckContext = { headOwner: string; headRepo: string; headRef: string; - eventAction: string; labelName: string; }; @@ -164,7 +162,6 @@ async function runChecks() { headOwner: requireEnv("PR_HEAD_OWNER"), headRepo: requireEnv("PR_HEAD_REPO"), headRef: requireEnv("PR_HEAD_REF"), - eventAction: requireEnv("EVENT_ACTION"), labelName: process.env.LABEL_NAME ?? "", }; console.log("ctx:", ctx); @@ -205,10 +202,6 @@ async function runChecks() { } async function claCheck(ctx: CheckContext): Promise { - if (!["opened", "synchronize", "reopened"].includes(ctx.eventAction)) { - return { actions: [] }; - } - let author = ctx.author.toLowerCase(); if (author === "dependabot[bot]") { console.log(`claCheck: ignoring ${ctx.author}`); @@ -223,21 +216,6 @@ async function claCheck(ctx: CheckContext): Promise { let signedCla = contributors.includes(author); console.log(`claCheck: ${ctx.author} signed CLA: ${signedCla}`); - // Dry runs to start so we can monitor the flow alongside the bot - let DRY_RUN = true; - if (DRY_RUN) { - if (signedCla) { - console.log( - `claCheck: dry run; would add '${CLA_SIGNED_LABEL}' label and comment that the CLA is signed`, - ); - } else { - console.log( - `claCheck: dry run; would request CLA signature and fail PR checks`, - ); - } - return { actions: [] }; - } - if (signedCla) { return { actions: [ @@ -265,9 +243,6 @@ async function claCheck(ctx: CheckContext): Promise { async function changeFileCheck(ctx: CheckContext): Promise { if (ctx.baseBranch !== "main") return { actions: [] }; - if (!["opened", "synchronize", "reopened"].includes(ctx.eventAction)) { - return { actions: [] }; - } let files = await getPrFiles(ctx.prNumber); let touchesPackageFiles = files.some((f) => @@ -321,17 +296,18 @@ async function changeFileCheck(ctx: CheckContext): Promise { } async function featurePrCheck(ctx: CheckContext): Promise { - if (ctx.eventAction !== "labeled") return { actions: [] }; - if (ctx.labelName !== "feature-request") return { actions: [] }; + if (ctx.labelName === "feature-request") { + console.log(`featurePrCheck: closing PR ${ctx.prNumber}`); + return { + actions: [ + { type: "create-comment", body: CLOSE_FEATURE_PR_COMMENT }, + { type: "remove-label", label: ctx.labelName }, + { type: "close-pr" }, + ], + }; + } - console.log(`featurePrCheck: closing PR ${ctx.prNumber}`); - return { - actions: [ - { type: "create-comment", body: CLOSE_FEATURE_PR_COMMENT }, - { type: "remove-label", label: ctx.labelName }, - { type: "close-pr" }, - ], - }; + return { actions: [] }; } // ---------- Action dispatch ----------