From 0f2b7f6a932f0d936f8cccb384889d7477db7552 Mon Sep 17 00:00:00 2001 From: bluwy Date: Mon, 18 May 2026 22:22:30 +0800 Subject: [PATCH 01/13] Initial implementation --- comment-pr-changeset/README.md | 38 ++++++++ comment-pr-changeset/action.yml | 18 ++++ package.json | 4 + pnpm-lock.yaml | 17 ++++ pnpm-workspace.yaml | 8 +- rolldown.config.js | 15 ++-- src/comment-pr-changeset/constants.ts | 1 + src/comment-pr-changeset/index.ts | 59 ++++++++++++ src/comment-pr-changeset/message.ts | 124 ++++++++++++++++++++++++++ src/comment-pr-changeset/template.ts | 30 +++++++ 10 files changed, 304 insertions(+), 10 deletions(-) create mode 100644 comment-pr-changeset/README.md create mode 100644 comment-pr-changeset/action.yml create mode 100644 src/comment-pr-changeset/constants.ts create mode 100644 src/comment-pr-changeset/index.ts create mode 100644 src/comment-pr-changeset/message.ts create mode 100644 src/comment-pr-changeset/template.ts diff --git a/comment-pr-changeset/README.md b/comment-pr-changeset/README.md new file mode 100644 index 00000000..17a54284 --- /dev/null +++ b/comment-pr-changeset/README.md @@ -0,0 +1,38 @@ +# changesets/action/comment-pr-changeset + +This action comments on PRs of its changeset status, e.g. whether it has changeset files and which packages will be released if the PR is merged. + +The action requires the base ref (of the repo) and head ref (of the PR) to be checked out in order to infer the changed files and packages. It also requires the [`pull_request_target`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target) event to be triggered in order to have permissions to comment on the PR and to work in PRs from forks. + +Note: It is important to not run untrusted code when using the `pull_request_target` event. The example below only checks out code and does not run any code from the PR. Read more about the `pull_request_target` event in the [GitHub documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target). + +```yaml +name: Comment PR Changeset Status + +on: + pull_request_target: + type: [opened, edited, synchronize] + +jobs: + comment-pr-changeset: + runs-on: ubuntu-slim + permissions: + issues: write # to create comments on PRs + steps: + - name: Check out base ref + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Check out head ref + run: | + git remote add head $REPO + git fetch head $REF + git checkout FETCH_HEAD + env: + REPO: ${{ github.event.pull_request.head.repo.clone_url }} + REF: ${{ github.event.pull_request.head.ref }} + + - name: Comment changeset status + uses: changesets/action/comment-pr-changeset@v2 +``` diff --git a/comment-pr-changeset/action.yml b/comment-pr-changeset/action.yml new file mode 100644 index 00000000..0ab79ef9 --- /dev/null +++ b/comment-pr-changeset/action.yml @@ -0,0 +1,18 @@ +name: Changesets - Comment PR Changeset +description: Comment the changeset status in PRs +runs: + using: node24 + main: dist/comment-pr-changeset.js +inputs: + github-token: + description: The GitHub token to use for authentication. Defaults to the GitHub-provided token. + required: false + default: ${{ github.token }} +outputs: + commentId: + description: The comment id created or updated by this action. + commentBody: + description: The comment body created or updated by this action. +branding: + icon: package + color: blue diff --git a/package.json b/package.json index 51a42aff..99a61362 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,17 @@ "@actions/core": "^3.0.1", "@actions/exec": "^3.0.0", "@actions/github": "^9.1.1", + "@changesets/get-release-plan": "^4.0.16", "@changesets/ghcommit": "^2.0.1", + "@changesets/git": "^3.0.4", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.5", "@manypkg/get-packages": "^1.1.3", "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@types/mdast": "^3.0.0", + "human-id": "^4.1.3", + "markdown-table": "^3.0.4", "mdast-util-to-string": "^1.0.6", "remark-parse": "^7.0.1", "remark-stringify": "^7.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c11cd8d3..66245fd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,9 +22,15 @@ importers: '@actions/github': specifier: ^9.1.1 version: 9.1.1 + '@changesets/get-release-plan': + specifier: ^4.0.16 + version: 4.0.16 '@changesets/ghcommit': specifier: ^2.0.1 version: 2.0.1 + '@changesets/git': + specifier: ^3.0.4 + version: 3.0.4 '@changesets/pre': specifier: ^2.0.2 version: 2.0.2 @@ -43,6 +49,12 @@ importers: '@types/mdast': specifier: ^3.0.0 version: 3.0.15 + human-id: + specifier: ^4.1.3 + version: 4.1.3 + markdown-table: + specifier: ^3.0.4 + version: 3.0.4 mdast-util-to-string: specifier: ^1.0.6 version: 1.1.0 @@ -948,6 +960,9 @@ packages: markdown-table@1.1.3: resolution: {integrity: sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2250,6 +2265,8 @@ snapshots: markdown-table@1.1.3: {} + markdown-table@3.0.4: {} + math-intrinsics@1.1.0: {} mdast-util-compact@1.0.4: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3c05cc47..f6ecbded 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,15 +3,15 @@ dedupePeers: true dedupePeerDependents: true minimumReleaseAge: 10080 minimumReleaseAgeExclude: - - '@oxc-project/*' - - '@rolldown/*' + - "@oxc-project/*" + - "@rolldown/*" - rolldown@1.0.1 - vite@8.0.13 - vitest@4.1.6 overrides: - lightningcss: '-' # we do not bundle any css - postcss: '-' # we do not bundle any css + lightningcss: "-" # we do not bundle any css + postcss: "-" # we do not bundle any css shellEmulator: true trustPolicy: no-downgrade diff --git a/rolldown.config.js b/rolldown.config.js index 73ce840d..26699eb9 100644 --- a/rolldown.config.js +++ b/rolldown.config.js @@ -1,13 +1,16 @@ -import { defineConfig } from 'rolldown' +import { defineConfig } from "rolldown"; export default defineConfig({ - input: 'src/index.ts', + input: { + index: "src/index.ts", + ["comment-pr-changeset"]: "src/comment-pr-changeset/index.ts", + }, output: { - dir: 'dist', - format: 'esm', + dir: "dist", + format: "esm", cleanDir: true, minify: true, comments: false, }, - platform: 'node', -}) + platform: "node", +}); diff --git a/src/comment-pr-changeset/constants.ts b/src/comment-pr-changeset/constants.ts new file mode 100644 index 00000000..3c6632d5 --- /dev/null +++ b/src/comment-pr-changeset/constants.ts @@ -0,0 +1 @@ +export const commentMarker = ""; diff --git a/src/comment-pr-changeset/index.ts b/src/comment-pr-changeset/index.ts new file mode 100644 index 00000000..2e4673bc --- /dev/null +++ b/src/comment-pr-changeset/index.ts @@ -0,0 +1,59 @@ +import * as core from "@actions/core"; +import * as github from "@actions/github"; +import { setupOctokit } from "../octokit.ts"; +import { commentMarker } from "./constants.ts"; +import { getCommentMessage } from "./message.ts"; + +type Octokit = ReturnType; +type CreateCommentParams = NonNullable< + Parameters[0] +>; +type UpdateCommentParams = NonNullable< + Parameters[0] +>; + +(async () => { + const context = github.context.payload.pull_request; + if (!context) { + core.error("This action should only be run on pull_request_target events"); + return; + } + + // Construct the comment message first + const commentBody = await getCommentMessage(context); + const commentParam: CreateCommentParams | UpdateCommentParams = { + repo: context.base.repo.name, + owner: context.base.repo.owner, + issue_number: context.number, + body: commentBody, + }; + core.setOutput("commentBody", commentBody); + + const githubToken = core.getInput("github-token", { required: true }); + const octokit = setupOctokit(githubToken); + + const existingCommentId = await octokit.rest.issues + .listComments({ + repo: context.base.repo.name, + owner: context.base.repo.owner, + issue_number: context.number, + }) + .then((res) => { + const comment = res.data.find((c) => c.body?.includes(commentMarker)); + return comment?.id; + }); + + if (existingCommentId) { + core.setOutput("commentId", existingCommentId.toString()); + await octokit.rest.issues.updateComment({ + ...commentParam, + comment_id: existingCommentId, + }); + } else { + const result = await octokit.rest.issues.createComment(commentParam); + core.setOutput("commentId", result.data.id.toString()); + } +})().catch((err) => { + core.error(err); + core.setFailed(err.message); +}); diff --git a/src/comment-pr-changeset/message.ts b/src/comment-pr-changeset/message.ts new file mode 100644 index 00000000..b43ab29c --- /dev/null +++ b/src/comment-pr-changeset/message.ts @@ -0,0 +1,124 @@ +import * as github from "@actions/github"; +import getReleasePlan from "@changesets/get-release-plan"; +import type { + ComprehensiveRelease, + ReleasePlan, + VersionType, +} from "@changesets/types"; +import { markdownTable } from "markdown-table"; +import { commentMarker } from "./constants.ts"; +import { + getNewChangesetTemplateContent, + getNewChangesetTemplateUrl, +} from "./template.ts"; + +type PullRequestContext = NonNullable< + typeof github.context.payload.pull_request +>; + +export async function getCommentMessage(context: PullRequestContext) { + const cwd = process.cwd(); + const releasePlan = await getReleasePlan(cwd, context.base.ref); + + const templateContent = await getNewChangesetTemplateContent( + cwd, + context.base.ref, + context.head.title, + ); + + const addChangesetUrl = getNewChangesetTemplateUrl( + context.head.repo.html_url, + context.head.ref, + templateContent, + ); + + if (releasePlan.changesets.length > 0) { + return getApproveMessage(context.head.sha, addChangesetUrl, releasePlan); + } else { + return getAbsentMessage(context.head.sha, addChangesetUrl, releasePlan); + } +} + +function getApproveMessage( + commitSha: string, + addChangesetUrl: string, + releasePlan: ReleasePlan, +) { + return `\ +${commentMarker} + +### 🦋 Changeset detected + +Latest commit: ${commitSha} + +**The changes in this PR will be included in the next version bump.** + +${getReleasePlanMessage(releasePlan)} + +Not sure what this means? [Click here to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). + +[Click here if you're a maintainer who wants to add another changeset to this PR](${addChangesetUrl})`; +} + +function getAbsentMessage( + commitSha: string, + addChangesetUrl: string, + releasePlan: ReleasePlan, +) { + return `\ +${commentMarker} + +### ⚠️ No Changeset found + +Latest commit: ${commitSha} + +Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.** + +${getReleasePlanMessage(releasePlan)} + +[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). + +[Click here if you're a maintainer who wants to add a changeset to this PR](${addChangesetUrl})`; +} + +function getReleasePlanMessage(releasePlan: ReleasePlan) { + const publishableReleases = releasePlan.releases.filter( + (r) => r.type !== "none", + ) as (ComprehensiveRelease & { type: Exclude })[]; + + const table = markdownTable([ + ["Name", "Type"], + ...publishableReleases.map((release) => { + return [ + release.name, + { + major: "Major", + minor: "Minor", + patch: "Patch", + }[release.type], + ]; + }), + ]); + + let summary = "This PR includes "; + if (releasePlan.changesets.length === 0) { + summary += "no changesets"; + } else { + summary += `changesets to release ${publishableReleases.length} package`; + if (publishableReleases.length !== 1) { + summary += "s"; + } + } + + return `\ +
+${summary} + +${ + publishableReleases.length > 0 + ? table + : "When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types" +} + +
`; +} diff --git a/src/comment-pr-changeset/template.ts b/src/comment-pr-changeset/template.ts new file mode 100644 index 00000000..078e8737 --- /dev/null +++ b/src/comment-pr-changeset/template.ts @@ -0,0 +1,30 @@ +import { getChangedPackagesSinceRef } from "@changesets/git"; +import { humanId } from "human-id"; + +export function getNewChangesetTemplateUrl( + headRepoUrl: string, + headRef: string, + templateContent: string, +) { + const fileName = humanId({ separator: "-", capitalize: false }); + return `${headRepoUrl}/new/${headRef}?filename=.changeset/${fileName}.md&value=${encodeURIComponent(templateContent)}`; +} + +export async function getNewChangesetTemplateContent( + cwd: string, + baseRef: string, + prTitle: string, +) { + const changedPackages = await getChangedPackagesSinceRef({ + cwd, + ref: baseRef, + }); + + return `\ +--- +${changedPackages.map((p) => `"${p.packageJson.name}": patch`).join("\n")} +--- + +${prTitle} +`; +} From 0c33f1762ad938212e755f720d8f2ffbdc875117 Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 19 May 2026 00:37:46 +0800 Subject: [PATCH 02/13] Fix --- comment-pr-changeset/README.md | 2 +- comment-pr-changeset/action.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comment-pr-changeset/README.md b/comment-pr-changeset/README.md index 17a54284..a0c5752d 100644 --- a/comment-pr-changeset/README.md +++ b/comment-pr-changeset/README.md @@ -11,7 +11,7 @@ name: Comment PR Changeset Status on: pull_request_target: - type: [opened, edited, synchronize] + types: [opened, edited, synchronize] jobs: comment-pr-changeset: diff --git a/comment-pr-changeset/action.yml b/comment-pr-changeset/action.yml index 0ab79ef9..e3384bdd 100644 --- a/comment-pr-changeset/action.yml +++ b/comment-pr-changeset/action.yml @@ -2,7 +2,7 @@ name: Changesets - Comment PR Changeset description: Comment the changeset status in PRs runs: using: node24 - main: dist/comment-pr-changeset.js + main: ../dist/comment-pr-changeset.js inputs: github-token: description: The GitHub token to use for authentication. Defaults to the GitHub-provided token. From 513d3b1da6c549af3bc38d8eeaac5b374f079ccc Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 19 May 2026 01:44:40 +0800 Subject: [PATCH 03/13] fix --- comment-pr-changeset/README.md | 19 +++++++++++-------- src/comment-pr-changeset/index.ts | 11 ++++++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/comment-pr-changeset/README.md b/comment-pr-changeset/README.md index a0c5752d..3bae6747 100644 --- a/comment-pr-changeset/README.md +++ b/comment-pr-changeset/README.md @@ -11,28 +11,31 @@ name: Comment PR Changeset Status on: pull_request_target: - types: [opened, edited, synchronize] + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true jobs: comment-pr-changeset: runs-on: ubuntu-slim permissions: - issues: write # to create comments on PRs + contents: read # to check out files in the repo + pull-requests: write # to create and update comments on PRs steps: - name: Check out base ref uses: actions/checkout@v6 - with: - persist-credentials: false - name: Check out head ref run: | - git remote add head $REPO - git fetch head $REF - git checkout FETCH_HEAD + git fetch "$REPO" "$REF" + git switch -c pr "$SHA" env: REPO: ${{ github.event.pull_request.head.repo.clone_url }} REF: ${{ github.event.pull_request.head.ref }} + SHA: ${{ github.event.pull_request.head.sha }} - name: Comment changeset status - uses: changesets/action/comment-pr-changeset@v2 + uses: changesets/action/comment-pr-changeset@comment-pr-changeset-dist ``` diff --git a/src/comment-pr-changeset/index.ts b/src/comment-pr-changeset/index.ts index 2e4673bc..6e95139e 100644 --- a/src/comment-pr-changeset/index.ts +++ b/src/comment-pr-changeset/index.ts @@ -19,11 +19,11 @@ type UpdateCommentParams = NonNullable< return; } - // Construct the comment message first + core.info("Creating comment message..."); const commentBody = await getCommentMessage(context); const commentParam: CreateCommentParams | UpdateCommentParams = { repo: context.base.repo.name, - owner: context.base.repo.owner, + owner: context.base.repo.owner.login, issue_number: context.number, body: commentBody, }; @@ -32,10 +32,11 @@ type UpdateCommentParams = NonNullable< const githubToken = core.getInput("github-token", { required: true }); const octokit = setupOctokit(githubToken); + core.info("Checking for existing comment..."); const existingCommentId = await octokit.rest.issues .listComments({ repo: context.base.repo.name, - owner: context.base.repo.owner, + owner: context.base.repo.owner.login, issue_number: context.number, }) .then((res) => { @@ -44,15 +45,19 @@ type UpdateCommentParams = NonNullable< }); if (existingCommentId) { + core.info(`Updating existing comment (id: ${existingCommentId})...`); core.setOutput("commentId", existingCommentId.toString()); await octokit.rest.issues.updateComment({ ...commentParam, comment_id: existingCommentId, }); } else { + core.info("Creating new comment..."); const result = await octokit.rest.issues.createComment(commentParam); core.setOutput("commentId", result.data.id.toString()); } + + core.info("Done!"); })().catch((err) => { core.error(err); core.setFailed(err.message); From bdfd7f004ef3a94da53801cab436c78d5e4f23fb Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 19 May 2026 01:58:34 +0800 Subject: [PATCH 04/13] fix --- src/comment-pr-changeset/message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/comment-pr-changeset/message.ts b/src/comment-pr-changeset/message.ts index b43ab29c..e72e5ea6 100644 --- a/src/comment-pr-changeset/message.ts +++ b/src/comment-pr-changeset/message.ts @@ -23,7 +23,7 @@ export async function getCommentMessage(context: PullRequestContext) { const templateContent = await getNewChangesetTemplateContent( cwd, context.base.ref, - context.head.title, + context.title, ); const addChangesetUrl = getNewChangesetTemplateUrl( From 3f05264f4cdd12a6ee8d01daa6cbfe1bc068be3f Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 19 May 2026 10:14:16 +0800 Subject: [PATCH 05/13] Make it look nicer --- src/comment-pr-changeset/message.ts | 16 ++++++++-------- src/comment-pr-changeset/template.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/comment-pr-changeset/message.ts b/src/comment-pr-changeset/message.ts index e72e5ea6..12f27807 100644 --- a/src/comment-pr-changeset/message.ts +++ b/src/comment-pr-changeset/message.ts @@ -9,7 +9,7 @@ import { markdownTable } from "markdown-table"; import { commentMarker } from "./constants.ts"; import { getNewChangesetTemplateContent, - getNewChangesetTemplateUrl, + getNewChangesetUrl, } from "./template.ts"; type PullRequestContext = NonNullable< @@ -26,22 +26,22 @@ export async function getCommentMessage(context: PullRequestContext) { context.title, ); - const addChangesetUrl = getNewChangesetTemplateUrl( + const newChangesetUrl = getNewChangesetUrl( context.head.repo.html_url, context.head.ref, templateContent, ); if (releasePlan.changesets.length > 0) { - return getApproveMessage(context.head.sha, addChangesetUrl, releasePlan); + return getApproveMessage(context.head.sha, newChangesetUrl, releasePlan); } else { - return getAbsentMessage(context.head.sha, addChangesetUrl, releasePlan); + return getAbsentMessage(context.head.sha, newChangesetUrl, releasePlan); } } function getApproveMessage( commitSha: string, - addChangesetUrl: string, + newChangesetUrl: string, releasePlan: ReleasePlan, ) { return `\ @@ -57,12 +57,12 @@ ${getReleasePlanMessage(releasePlan)} Not sure what this means? [Click here to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). -[Click here if you're a maintainer who wants to add another changeset to this PR](${addChangesetUrl})`; +[Click here if you're a maintainer who wants to add another changeset to this PR](${newChangesetUrl})`; } function getAbsentMessage( commitSha: string, - addChangesetUrl: string, + newChangesetUrl: string, releasePlan: ReleasePlan, ) { return `\ @@ -78,7 +78,7 @@ ${getReleasePlanMessage(releasePlan)} [Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). -[Click here if you're a maintainer who wants to add a changeset to this PR](${addChangesetUrl})`; +[Click here if you're a maintainer who wants to add a changeset to this PR](${newChangesetUrl})`; } function getReleasePlanMessage(releasePlan: ReleasePlan) { diff --git a/src/comment-pr-changeset/template.ts b/src/comment-pr-changeset/template.ts index 078e8737..03f9bc29 100644 --- a/src/comment-pr-changeset/template.ts +++ b/src/comment-pr-changeset/template.ts @@ -1,7 +1,7 @@ import { getChangedPackagesSinceRef } from "@changesets/git"; import { humanId } from "human-id"; -export function getNewChangesetTemplateUrl( +export function getNewChangesetUrl( headRepoUrl: string, headRef: string, templateContent: string, From b3bbf47e7d69ee2f2d1c3ade7722ebfcd4acacd8 Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 19 May 2026 10:34:06 +0800 Subject: [PATCH 06/13] Update docs --- README.md | 4 ++++ comment-pr-changeset/README.md | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b465bcc0..a19e7523 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ This action for [Changesets](https://github.com/changesets/changesets) creates a pull request with all of the package versions updated and changelogs updated and when there are new changesets on [your configured `baseBranch`](https://github.com/changesets/changesets/blob/main/docs/config-file-options.md#basebranch-git-branch-name), the PR will be updated. When you're ready, you can merge the pull request and you can either publish the packages to npm manually or setup the action to do it for you. +There are also sub-actions hosted in this repository. Check out their respective READMEs for more details: + +- [comment-pr-changeset](./comment-pr-changeset/README.md): Comment the changeset status in PRs. + ## Usage ### Inputs diff --git a/comment-pr-changeset/README.md b/comment-pr-changeset/README.md index 3bae6747..db24a765 100644 --- a/comment-pr-changeset/README.md +++ b/comment-pr-changeset/README.md @@ -4,9 +4,15 @@ This action comments on PRs of its changeset status, e.g. whether it has changes The action requires the base ref (of the repo) and head ref (of the PR) to be checked out in order to infer the changed files and packages. It also requires the [`pull_request_target`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target) event to be triggered in order to have permissions to comment on the PR and to work in PRs from forks. -Note: It is important to not run untrusted code when using the `pull_request_target` event. The example below only checks out code and does not run any code from the PR. Read more about the `pull_request_target` event in the [GitHub documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target). +See the [action metadata](action.yml) for details on the inputs and outputs. + +> [!WARNING] +> **Do not run untrusted code** when using the `pull_request_target` event. The example below only checks out code and does not run any code from the PR. Read more about the `pull_request_target` event in the [GitHub documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target). + +## Example setup ```yaml +# .github/workflows/comment-pr-changeset.yml name: Comment PR Changeset Status on: @@ -37,5 +43,5 @@ jobs: SHA: ${{ github.event.pull_request.head.sha }} - name: Comment changeset status - uses: changesets/action/comment-pr-changeset@comment-pr-changeset-dist + uses: changesets/action/comment-pr-changeset@v2 ``` From b4b967a859ba9952842e1c2e58ff734d07ed6768 Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 19 May 2026 15:34:20 +0800 Subject: [PATCH 07/13] Rename to pr-status-comment --- README.md | 2 +- comment-pr-changeset/README.md | 10 +++++----- comment-pr-changeset/action.yml | 2 +- rolldown.config.js | 2 +- src/comment-pr-changeset/constants.ts | 1 - src/pr-status-comment/constants.ts | 1 + .../index.ts | 0 .../message.ts | 0 .../template.ts | 0 9 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 src/comment-pr-changeset/constants.ts create mode 100644 src/pr-status-comment/constants.ts rename src/{comment-pr-changeset => pr-status-comment}/index.ts (100%) rename src/{comment-pr-changeset => pr-status-comment}/message.ts (100%) rename src/{comment-pr-changeset => pr-status-comment}/template.ts (100%) diff --git a/README.md b/README.md index a19e7523..df592ef0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This action for [Changesets](https://github.com/changesets/changesets) creates a There are also sub-actions hosted in this repository. Check out their respective READMEs for more details: -- [comment-pr-changeset](./comment-pr-changeset/README.md): Comment the changeset status in PRs. +- [pr-status-comment](./pr-status-comment/README.md): Comment the changeset status in PRs. ## Usage diff --git a/comment-pr-changeset/README.md b/comment-pr-changeset/README.md index db24a765..05b74e35 100644 --- a/comment-pr-changeset/README.md +++ b/comment-pr-changeset/README.md @@ -1,4 +1,4 @@ -# changesets/action/comment-pr-changeset +# changesets/action/pr-status-comment This action comments on PRs of its changeset status, e.g. whether it has changeset files and which packages will be released if the PR is merged. @@ -12,8 +12,8 @@ See the [action metadata](action.yml) for details on the inputs and outputs. ## Example setup ```yaml -# .github/workflows/comment-pr-changeset.yml -name: Comment PR Changeset Status +# .github/workflows/pr-status-comment.yml +name: Comment changeset status in PRs on: pull_request_target: @@ -24,7 +24,7 @@ concurrency: cancel-in-progress: true jobs: - comment-pr-changeset: + pr-status-comment: runs-on: ubuntu-slim permissions: contents: read # to check out files in the repo @@ -43,5 +43,5 @@ jobs: SHA: ${{ github.event.pull_request.head.sha }} - name: Comment changeset status - uses: changesets/action/comment-pr-changeset@v2 + uses: changesets/action/pr-status-comment@v1 ``` diff --git a/comment-pr-changeset/action.yml b/comment-pr-changeset/action.yml index e3384bdd..183b6437 100644 --- a/comment-pr-changeset/action.yml +++ b/comment-pr-changeset/action.yml @@ -2,7 +2,7 @@ name: Changesets - Comment PR Changeset description: Comment the changeset status in PRs runs: using: node24 - main: ../dist/comment-pr-changeset.js + main: ../dist/pr-status-comment.js inputs: github-token: description: The GitHub token to use for authentication. Defaults to the GitHub-provided token. diff --git a/rolldown.config.js b/rolldown.config.js index 26699eb9..d25d5954 100644 --- a/rolldown.config.js +++ b/rolldown.config.js @@ -3,7 +3,7 @@ import { defineConfig } from "rolldown"; export default defineConfig({ input: { index: "src/index.ts", - ["comment-pr-changeset"]: "src/comment-pr-changeset/index.ts", + ["pr-status-comment"]: "src/pr-status-comment/index.ts", }, output: { dir: "dist", diff --git a/src/comment-pr-changeset/constants.ts b/src/comment-pr-changeset/constants.ts deleted file mode 100644 index 3c6632d5..00000000 --- a/src/comment-pr-changeset/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const commentMarker = ""; diff --git a/src/pr-status-comment/constants.ts b/src/pr-status-comment/constants.ts new file mode 100644 index 00000000..6ff6e790 --- /dev/null +++ b/src/pr-status-comment/constants.ts @@ -0,0 +1 @@ +export const commentMarker = ""; diff --git a/src/comment-pr-changeset/index.ts b/src/pr-status-comment/index.ts similarity index 100% rename from src/comment-pr-changeset/index.ts rename to src/pr-status-comment/index.ts diff --git a/src/comment-pr-changeset/message.ts b/src/pr-status-comment/message.ts similarity index 100% rename from src/comment-pr-changeset/message.ts rename to src/pr-status-comment/message.ts diff --git a/src/comment-pr-changeset/template.ts b/src/pr-status-comment/template.ts similarity index 100% rename from src/comment-pr-changeset/template.ts rename to src/pr-status-comment/template.ts From 7457ddbe474e4776c875c97810f57eaaa66f1d83 Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 19 May 2026 15:35:08 +0800 Subject: [PATCH 08/13] Remove outputs --- comment-pr-changeset/action.yml | 6 +----- src/pr-status-comment/index.ts | 3 --- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/comment-pr-changeset/action.yml b/comment-pr-changeset/action.yml index 183b6437..af97b3ac 100644 --- a/comment-pr-changeset/action.yml +++ b/comment-pr-changeset/action.yml @@ -8,11 +8,7 @@ inputs: description: The GitHub token to use for authentication. Defaults to the GitHub-provided token. required: false default: ${{ github.token }} -outputs: - commentId: - description: The comment id created or updated by this action. - commentBody: - description: The comment body created or updated by this action. +outputs: {} branding: icon: package color: blue diff --git a/src/pr-status-comment/index.ts b/src/pr-status-comment/index.ts index 6e95139e..f0ae81b2 100644 --- a/src/pr-status-comment/index.ts +++ b/src/pr-status-comment/index.ts @@ -27,7 +27,6 @@ type UpdateCommentParams = NonNullable< issue_number: context.number, body: commentBody, }; - core.setOutput("commentBody", commentBody); const githubToken = core.getInput("github-token", { required: true }); const octokit = setupOctokit(githubToken); @@ -46,7 +45,6 @@ type UpdateCommentParams = NonNullable< if (existingCommentId) { core.info(`Updating existing comment (id: ${existingCommentId})...`); - core.setOutput("commentId", existingCommentId.toString()); await octokit.rest.issues.updateComment({ ...commentParam, comment_id: existingCommentId, @@ -54,7 +52,6 @@ type UpdateCommentParams = NonNullable< } else { core.info("Creating new comment..."); const result = await octokit.rest.issues.createComment(commentParam); - core.setOutput("commentId", result.data.id.toString()); } core.info("Done!"); From 6057580e86590e546d9743e8eb16a7f61e4762f4 Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 19 May 2026 15:38:47 +0800 Subject: [PATCH 09/13] Update path --- {comment-pr-changeset => pr-status-comment}/README.md | 0 {comment-pr-changeset => pr-status-comment}/action.yml | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {comment-pr-changeset => pr-status-comment}/README.md (100%) rename {comment-pr-changeset => pr-status-comment}/action.yml (100%) diff --git a/comment-pr-changeset/README.md b/pr-status-comment/README.md similarity index 100% rename from comment-pr-changeset/README.md rename to pr-status-comment/README.md diff --git a/comment-pr-changeset/action.yml b/pr-status-comment/action.yml similarity index 100% rename from comment-pr-changeset/action.yml rename to pr-status-comment/action.yml From 8188bb1ce47661370c14482ee1c084df2f50ffae Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 19 May 2026 15:40:04 +0800 Subject: [PATCH 10/13] update name --- pr-status-comment/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pr-status-comment/action.yml b/pr-status-comment/action.yml index af97b3ac..ee6acba5 100644 --- a/pr-status-comment/action.yml +++ b/pr-status-comment/action.yml @@ -1,4 +1,4 @@ -name: Changesets - Comment PR Changeset +name: Changesets - PR Status Comment description: Comment the changeset status in PRs runs: using: node24 From 65ef483d6bc8f6aae6d36437e882104ead450742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 25 May 2026 08:10:59 +0200 Subject: [PATCH 11/13] Checkout PR in a detached worktree (#626) Co-authored-by: bluwy --- package.json | 1 + pnpm-lock.yaml | 3 + pr-status-comment/README.md | 11 +- src/pr-status-comment/message.ts | 23 ++-- src/pr-status-comment/worktree.test.ts | 148 +++++++++++++++++++++++++ src/pr-status-comment/worktree.ts | 136 +++++++++++++++++++++++ 6 files changed, 305 insertions(+), 17 deletions(-) create mode 100644 src/pr-status-comment/worktree.test.ts create mode 100644 src/pr-status-comment/worktree.ts diff --git a/package.json b/package.json index 99a61362..754bad92 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "remark-parse": "^7.0.1", "remark-stringify": "^7.0.3", "semver": "^7.5.3", + "tinyexec": "^1.1.2", "unified": "^8.3.2" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66245fd3..61825dd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,6 +67,9 @@ importers: semver: specifier: ^7.5.3 version: 7.7.4 + tinyexec: + specifier: ^1.1.2 + version: 1.1.2 unified: specifier: ^8.3.2 version: 8.4.2 diff --git a/pr-status-comment/README.md b/pr-status-comment/README.md index 05b74e35..da42f82b 100644 --- a/pr-status-comment/README.md +++ b/pr-status-comment/README.md @@ -2,7 +2,7 @@ This action comments on PRs of its changeset status, e.g. whether it has changeset files and which packages will be released if the PR is merged. -The action requires the base ref (of the repo) and head ref (of the PR) to be checked out in order to infer the changed files and packages. It also requires the [`pull_request_target`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target) event to be triggered in order to have permissions to comment on the PR and to work in PRs from forks. +The action requires the base ref (of the repo) to be checked out. It fetches the PR head ref into a temporary detached worktree in order to infer the changed files and packages. It also requires the [`pull_request_target`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target) event to be triggered in order to have permissions to comment on the PR and to work in PRs from forks. See the [action metadata](action.yml) for details on the inputs and outputs. @@ -33,15 +33,6 @@ jobs: - name: Check out base ref uses: actions/checkout@v6 - - name: Check out head ref - run: | - git fetch "$REPO" "$REF" - git switch -c pr "$SHA" - env: - REPO: ${{ github.event.pull_request.head.repo.clone_url }} - REF: ${{ github.event.pull_request.head.ref }} - SHA: ${{ github.event.pull_request.head.sha }} - - name: Comment changeset status uses: changesets/action/pr-status-comment@v1 ``` diff --git a/src/pr-status-comment/message.ts b/src/pr-status-comment/message.ts index 12f27807..a661b0c3 100644 --- a/src/pr-status-comment/message.ts +++ b/src/pr-status-comment/message.ts @@ -11,19 +11,28 @@ import { getNewChangesetTemplateContent, getNewChangesetUrl, } from "./template.ts"; +import { withPullRequestWorktree } from "./worktree.ts"; type PullRequestContext = NonNullable< typeof github.context.payload.pull_request >; export async function getCommentMessage(context: PullRequestContext) { - const cwd = process.cwd(); - const releasePlan = await getReleasePlan(cwd, context.base.ref); - - const templateContent = await getNewChangesetTemplateContent( - cwd, - context.base.ref, - context.title, + const { releasePlan, templateContent } = await withPullRequestWorktree( + context, + async ({ cwd, baseRef }) => { + const releasePlan = await getReleasePlan(cwd, baseRef); + const templateContent = await getNewChangesetTemplateContent( + cwd, + baseRef, + context.title, + ); + + return { + releasePlan, + templateContent, + }; + }, ); const newChangesetUrl = getNewChangesetUrl( diff --git a/src/pr-status-comment/worktree.test.ts b/src/pr-status-comment/worktree.test.ts new file mode 100644 index 00000000..21fe6f78 --- /dev/null +++ b/src/pr-status-comment/worktree.test.ts @@ -0,0 +1,148 @@ +import { pathToFileURL } from "node:url"; +import getReleasePlan from "@changesets/get-release-plan"; +import { createFixture } from "fs-fixture"; +import { exec } from "tinyexec"; +import { describe, expect, it } from "vitest"; +import { withPullRequestWorktree } from "./worktree.ts"; + +async function git(cwd: string, args: string[]) { + const output = await exec("git", args, { + nodeOptions: { cwd }, + throwOnError: true, + }); + return output.stdout.trim(); +} + +describe("withPullRequestWorktree", () => { + it("fetches a PR branch into a detached worktree and keeps the main checkout untouched", async () => { + // Local source repo + await using sourceRepoFixture = await createFixture({ + ".changeset/config.json": JSON.stringify({}), + "package.json": JSON.stringify({ + name: "repo", + private: true, + workspaces: ["packages/*"], + }), + "packages/pkg-a/package.json": JSON.stringify({ + name: "pkg-a", + version: "1.0.0", + }), + }); + const sourceRepo = sourceRepoFixture.path; + await git(sourceRepo, ["init", "-b", "main"]); + await git(sourceRepo, ["config", "user.name", "Test User"]); + await git(sourceRepo, ["config", "user.email", "test@example.com"]); + await git(sourceRepo, ["add", "."]); + await git(sourceRepo, ["commit", "-m", "base"]); + + // Simulate remote bare git server + await using originBareFixture = await createFixture(); + const originBare = originBareFixture.path; + await git(originBare, ["clone", "--bare", sourceRepo, originBare]); + + // Simulate checkout PR in github action + await using checkoutRepoFixture = await createFixture(); + const checkoutRepo = checkoutRepoFixture.path; + await git(checkoutRepo, [ + "clone", + "--depth", + "1", + "--branch", + "main", + pathToFileURL(originBare).toString(), + checkoutRepo, + ]); + + // Simulate remote fork bare git server + await using forkBareFixture = await createFixture(); + const forkBare = forkBareFixture.path; + await git(forkBare, ["clone", "--bare", originBare, forkBare]); + + await using forkRepoFixture = await createFixture(); + const forkRepo = forkRepoFixture.path; + await git(forkRepo, [ + "clone", + "--depth", + "1", + "--branch", + "main", + pathToFileURL(forkBare).toString(), + forkRepo, + ]); + await git(forkRepo, ["config", "user.name", "Test User"]); + await git(forkRepo, ["config", "user.email", "test@example.com"]); + await git(forkRepo, ["checkout", "-b", "feature"]); + + // log all files in forkRepo with dot files + console.log( + "Files in forkRepo:\n", + await exec("ls", ["-Ra"], { nodeOptions: { cwd: forkRepo } }).then( + (res) => res.stdout.trim(), + ), + ); + + await forkRepoFixture.mkdir("packages/pkg-a/src"); + await forkRepoFixture.writeFile( + "packages/pkg-a/src/index.ts", + "export const value = 1;\n", + ); + await forkRepoFixture.writeFile( + ".changeset/add-pkg-a.md", + `\ +--- +"pkg-a": patch +--- + +Add pkg-a +`, + ); + + await git(forkRepo, ["add", "."]); + await git(forkRepo, ["commit", "-m", "feature"]); + await git(forkRepo, ["push", "origin", "feature"]); + + // Run tests + const originalHead = await git(checkoutRepo, ["rev-parse", "HEAD"]); + const context = { + number: 123, + base: { + ref: "main", + }, + head: { + ref: "feature", + repo: { + clone_url: pathToFileURL(forkBare).toString(), + }, + }, + } as any; + + const result = await withPullRequestWorktree( + context, + async ({ cwd, baseRef }) => { + const releasePlan = await getReleasePlan(cwd, baseRef); + return { + currentHead: await git(cwd, ["rev-parse", "HEAD"]), + currentBranch: await git(cwd, ["branch", "--show-current"]), + releases: releasePlan.releases.map((release) => ({ + name: release.name, + type: release.type, + newVersion: release.newVersion, + })), + }; + }, + checkoutRepo, + ); + + expect(result.currentHead).not.toBe(originalHead); + expect(result.currentBranch).toBe(""); + expect(result.releases).toEqual([ + { + name: "pkg-a", + type: "patch", + newVersion: "1.0.1", + }, + ]); + expect(await git(checkoutRepo, ["rev-parse", "HEAD"])).toBe(originalHead); + expect(await git(checkoutRepo, ["branch", "--show-current"])).toBe("main"); + }); +}); diff --git a/src/pr-status-comment/worktree.ts b/src/pr-status-comment/worktree.ts new file mode 100644 index 00000000..024a6b37 --- /dev/null +++ b/src/pr-status-comment/worktree.ts @@ -0,0 +1,136 @@ +import { randomUUID } from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import type * as github from "@actions/github"; +import { isRepoShallow } from "@changesets/git"; +import { exec } from "tinyexec"; + +type PullRequestContext = NonNullable< + typeof github.context.payload.pull_request +>; + +type WorktreeInfo = { + baseRef: string; + cwd: string; +}; + +type TinyexecOptions = Parameters[2]; + +function git(cwd: string, args: string[], opts: TinyexecOptions = {}) { + return exec("git", args, { + nodeOptions: { cwd, ...opts.nodeOptions }, + throwOnError: true, + ...opts, + }); +} + +async function deleteRef(cwd: string, ref: string) { + await git(cwd, ["update-ref", "-d", ref], { throwOnError: false }); +} + +function getRefNames(context: PullRequestContext) { + const suffix = `${context.number}-${randomUUID()}`; + return { + baseLocalRef: `refs/changesets-action-pr-status-comment/base/${suffix}`, + baseRemoteRef: `refs/heads/${context.base.ref}`, + headLocalRef: `refs/changesets-action-pr-status-comment/head/${suffix}`, + headRemoteRef: `refs/heads/${context.head.ref}`, + }; +} + +async function ensureMergeBase(args: { + cwd: string; + refs: ReturnType; + headRemoteUrl: string; + deepenBy?: number; +}) { + const { cwd, refs, headRemoteUrl, deepenBy = 50 } = args; + + while (true) { + const mergeBase = await git( + cwd, + ["merge-base", refs.baseLocalRef, "HEAD"], + { throwOnError: false }, + ); + + if (mergeBase.exitCode === 0) { + return mergeBase.stdout.trim(); + } + + if (!(await isRepoShallow({ cwd }))) { + throw new Error( + `Failed to find merge base between "${refs.baseLocalRef}" and HEAD, and the repository is no longer shallow.`, + ); + } + + await git(cwd, [ + "fetch", + "--no-tags", + `--deepen=${deepenBy}`, + "origin", + `${refs.baseRemoteRef}:${refs.baseLocalRef}`, + ]); + await git(cwd, [ + "fetch", + "--no-tags", + `--deepen=${deepenBy}`, + headRemoteUrl, + `${refs.headRemoteRef}:${refs.headLocalRef}`, + ]); + } +} + +export async function withPullRequestWorktree( + context: PullRequestContext, + fn: (worktree: WorktreeInfo) => Promise, + repoCwd: string = process.cwd(), +) { + const worktreeDir = await fs.mkdtemp( + path.join(os.tmpdir(), "changesets-action-pr-status-comment-"), + ); + const refs = getRefNames(context); + + try { + await git(repoCwd, [ + "fetch", + "--no-tags", + "--depth=1", + "origin", + `${refs.baseRemoteRef}:${refs.baseLocalRef}`, + ]); + await git(repoCwd, [ + "fetch", + "--no-tags", + "--depth=1", + context.head.repo.clone_url, + `${refs.headRemoteRef}:${refs.headLocalRef}`, + ]); + await git(repoCwd, [ + "worktree", + "add", + "--detach", + worktreeDir, + refs.headLocalRef, + ]); + await ensureMergeBase({ + cwd: worktreeDir, + refs, + headRemoteUrl: context.head.repo.clone_url, + }); + + return await fn({ + baseRef: refs.baseLocalRef, + cwd: worktreeDir, + }); + } finally { + await git(repoCwd, ["worktree", "remove", "--force", worktreeDir], { + throwOnError: false, + }); + await Promise.all([ + deleteRef(repoCwd, refs.baseLocalRef), + deleteRef(repoCwd, refs.headLocalRef), + ]); + await fs.rm(worktreeDir, { recursive: true, force: true }); + } +} From 7c7f15b39f21f64c221f0f07af48f07c26109062 Mon Sep 17 00:00:00 2001 From: bluwy Date: Mon, 25 May 2026 14:46:54 +0800 Subject: [PATCH 12/13] Move comment to separate job --- README.md | 2 +- pr-status-comment/README.md | 38 ------------ pr-status-comment/action.yml | 14 ----- pr-status/README.md | 52 ++++++++++++++++ pr-status/action.yml | 12 ++++ rolldown.config.js | 2 +- src/pr-status-comment/constants.ts | 1 - src/pr-status-comment/index.ts | 61 ------------------- src/pr-status/index.ts | 21 +++++++ .../message.ts | 5 -- .../template.ts | 0 .../worktree.test.ts | 0 .../worktree.ts | 6 +- 13 files changed, 90 insertions(+), 124 deletions(-) delete mode 100644 pr-status-comment/README.md delete mode 100644 pr-status-comment/action.yml create mode 100644 pr-status/README.md create mode 100644 pr-status/action.yml delete mode 100644 src/pr-status-comment/constants.ts delete mode 100644 src/pr-status-comment/index.ts create mode 100644 src/pr-status/index.ts rename src/{pr-status-comment => pr-status}/message.ts (97%) rename src/{pr-status-comment => pr-status}/template.ts (100%) rename src/{pr-status-comment => pr-status}/worktree.test.ts (100%) rename src/{pr-status-comment => pr-status}/worktree.ts (93%) diff --git a/README.md b/README.md index df592ef0..5f41559a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This action for [Changesets](https://github.com/changesets/changesets) creates a There are also sub-actions hosted in this repository. Check out their respective READMEs for more details: -- [pr-status-comment](./pr-status-comment/README.md): Comment the changeset status in PRs. +- [pr-status](./pr-status/README.md): Generate changeset status in PRs. ## Usage diff --git a/pr-status-comment/README.md b/pr-status-comment/README.md deleted file mode 100644 index da42f82b..00000000 --- a/pr-status-comment/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# changesets/action/pr-status-comment - -This action comments on PRs of its changeset status, e.g. whether it has changeset files and which packages will be released if the PR is merged. - -The action requires the base ref (of the repo) to be checked out. It fetches the PR head ref into a temporary detached worktree in order to infer the changed files and packages. It also requires the [`pull_request_target`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target) event to be triggered in order to have permissions to comment on the PR and to work in PRs from forks. - -See the [action metadata](action.yml) for details on the inputs and outputs. - -> [!WARNING] -> **Do not run untrusted code** when using the `pull_request_target` event. The example below only checks out code and does not run any code from the PR. Read more about the `pull_request_target` event in the [GitHub documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target). - -## Example setup - -```yaml -# .github/workflows/pr-status-comment.yml -name: Comment changeset status in PRs - -on: - pull_request_target: - types: [opened, synchronize, reopened] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - pr-status-comment: - runs-on: ubuntu-slim - permissions: - contents: read # to check out files in the repo - pull-requests: write # to create and update comments on PRs - steps: - - name: Check out base ref - uses: actions/checkout@v6 - - - name: Comment changeset status - uses: changesets/action/pr-status-comment@v1 -``` diff --git a/pr-status-comment/action.yml b/pr-status-comment/action.yml deleted file mode 100644 index ee6acba5..00000000 --- a/pr-status-comment/action.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Changesets - PR Status Comment -description: Comment the changeset status in PRs -runs: - using: node24 - main: ../dist/pr-status-comment.js -inputs: - github-token: - description: The GitHub token to use for authentication. Defaults to the GitHub-provided token. - required: false - default: ${{ github.token }} -outputs: {} -branding: - icon: package - color: blue diff --git a/pr-status/README.md b/pr-status/README.md new file mode 100644 index 00000000..0b254cc5 --- /dev/null +++ b/pr-status/README.md @@ -0,0 +1,52 @@ +# changesets/action/pr-status + +This action generates the changeset status in PRs, e.g. whether it has changeset files and which packages will be released if the PR is merged. + +It requires the repo to be checked out, and automatically fetches the PR head ref into a temporary detached worktree in order to infer the changed files and packages. It also requires the [`pull_request_target`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target) event to be triggered in order to have permissions to comment on the PR and to work in PRs from forks. You can also use the `pull_request` event if you prefer to lock permissions down and not run for PRs from forks. + +See the [action metadata](action.yml) for details on the inputs and outputs. + +> [!WARNING] +> **Do not run untrusted code** when using the `pull_request_target` event. The example below only checks out code and does not run any code from the PR. Read more about the `pull_request_target` event in the [GitHub documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target). + +## Example setup + +```yaml +# .github/workflows/comment-changeset-pr-status.yml +name: Comment changeset status in PRs + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + pr-status: + runs-on: ubuntu-slim + permissions: + contents: read # to check out files in the repo + outputs: + commentBody: ${{ steps.pr-status.outputs.commentBody }} + steps: + - name: Check out repo + uses: actions/checkout@v6 + + - name: Generate status + id: pr-status + uses: changesets/action/pr-status@v1 + + comment: + needs: pr-status + runs-on: ubuntu-slim + permissions: + pull-requests: write # to create and update comments on PRs + steps: + - name: Comment on PR + uses: mshick/add-pr-comment@v3 + with: + message-id: changeset-pr-status + message: ${{ needs.pr-status.outputs.commentBody }} +``` diff --git a/pr-status/action.yml b/pr-status/action.yml new file mode 100644 index 00000000..f58232b2 --- /dev/null +++ b/pr-status/action.yml @@ -0,0 +1,12 @@ +name: Changesets - PR Status +description: Generate the changeset status in PRs +runs: + using: node24 + main: ../dist/pr-status.js +inputs: {} +outputs: + commentBody: + description: The generated comment body to present the changeset status in PRs. +branding: + icon: package + color: blue diff --git a/rolldown.config.js b/rolldown.config.js index d25d5954..b3359c73 100644 --- a/rolldown.config.js +++ b/rolldown.config.js @@ -3,7 +3,7 @@ import { defineConfig } from "rolldown"; export default defineConfig({ input: { index: "src/index.ts", - ["pr-status-comment"]: "src/pr-status-comment/index.ts", + ["pr-status"]: "src/pr-status/index.ts", }, output: { dir: "dist", diff --git a/src/pr-status-comment/constants.ts b/src/pr-status-comment/constants.ts deleted file mode 100644 index 6ff6e790..00000000 --- a/src/pr-status-comment/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const commentMarker = ""; diff --git a/src/pr-status-comment/index.ts b/src/pr-status-comment/index.ts deleted file mode 100644 index f0ae81b2..00000000 --- a/src/pr-status-comment/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import * as core from "@actions/core"; -import * as github from "@actions/github"; -import { setupOctokit } from "../octokit.ts"; -import { commentMarker } from "./constants.ts"; -import { getCommentMessage } from "./message.ts"; - -type Octokit = ReturnType; -type CreateCommentParams = NonNullable< - Parameters[0] ->; -type UpdateCommentParams = NonNullable< - Parameters[0] ->; - -(async () => { - const context = github.context.payload.pull_request; - if (!context) { - core.error("This action should only be run on pull_request_target events"); - return; - } - - core.info("Creating comment message..."); - const commentBody = await getCommentMessage(context); - const commentParam: CreateCommentParams | UpdateCommentParams = { - repo: context.base.repo.name, - owner: context.base.repo.owner.login, - issue_number: context.number, - body: commentBody, - }; - - const githubToken = core.getInput("github-token", { required: true }); - const octokit = setupOctokit(githubToken); - - core.info("Checking for existing comment..."); - const existingCommentId = await octokit.rest.issues - .listComments({ - repo: context.base.repo.name, - owner: context.base.repo.owner.login, - issue_number: context.number, - }) - .then((res) => { - const comment = res.data.find((c) => c.body?.includes(commentMarker)); - return comment?.id; - }); - - if (existingCommentId) { - core.info(`Updating existing comment (id: ${existingCommentId})...`); - await octokit.rest.issues.updateComment({ - ...commentParam, - comment_id: existingCommentId, - }); - } else { - core.info("Creating new comment..."); - const result = await octokit.rest.issues.createComment(commentParam); - } - - core.info("Done!"); -})().catch((err) => { - core.error(err); - core.setFailed(err.message); -}); diff --git a/src/pr-status/index.ts b/src/pr-status/index.ts new file mode 100644 index 00000000..3ceedc9a --- /dev/null +++ b/src/pr-status/index.ts @@ -0,0 +1,21 @@ +import * as core from "@actions/core"; +import * as github from "@actions/github"; +import { getCommentMessage } from "./message.ts"; + +(async () => { + const context = github.context.payload.pull_request; + if (!context) { + core.error("This action should only be run on pull_request_target events"); + return; + } + + core.info("Creating comment message..."); + const commentBody = await getCommentMessage(context); + + core.setOutput("commentBody", commentBody); + + core.info("Done!"); +})().catch((err) => { + core.error(err); + core.setFailed(err.message); +}); diff --git a/src/pr-status-comment/message.ts b/src/pr-status/message.ts similarity index 97% rename from src/pr-status-comment/message.ts rename to src/pr-status/message.ts index a661b0c3..a446ec73 100644 --- a/src/pr-status-comment/message.ts +++ b/src/pr-status/message.ts @@ -6,7 +6,6 @@ import type { VersionType, } from "@changesets/types"; import { markdownTable } from "markdown-table"; -import { commentMarker } from "./constants.ts"; import { getNewChangesetTemplateContent, getNewChangesetUrl, @@ -54,8 +53,6 @@ function getApproveMessage( releasePlan: ReleasePlan, ) { return `\ -${commentMarker} - ### 🦋 Changeset detected Latest commit: ${commitSha} @@ -75,8 +72,6 @@ function getAbsentMessage( releasePlan: ReleasePlan, ) { return `\ -${commentMarker} - ### ⚠️ No Changeset found Latest commit: ${commitSha} diff --git a/src/pr-status-comment/template.ts b/src/pr-status/template.ts similarity index 100% rename from src/pr-status-comment/template.ts rename to src/pr-status/template.ts diff --git a/src/pr-status-comment/worktree.test.ts b/src/pr-status/worktree.test.ts similarity index 100% rename from src/pr-status-comment/worktree.test.ts rename to src/pr-status/worktree.test.ts diff --git a/src/pr-status-comment/worktree.ts b/src/pr-status/worktree.ts similarity index 93% rename from src/pr-status-comment/worktree.ts rename to src/pr-status/worktree.ts index 024a6b37..5c049cd7 100644 --- a/src/pr-status-comment/worktree.ts +++ b/src/pr-status/worktree.ts @@ -32,9 +32,9 @@ async function deleteRef(cwd: string, ref: string) { function getRefNames(context: PullRequestContext) { const suffix = `${context.number}-${randomUUID()}`; return { - baseLocalRef: `refs/changesets-action-pr-status-comment/base/${suffix}`, + baseLocalRef: `refs/changesets-action-pr-status/base/${suffix}`, baseRemoteRef: `refs/heads/${context.base.ref}`, - headLocalRef: `refs/changesets-action-pr-status-comment/head/${suffix}`, + headLocalRef: `refs/changesets-action-pr-status/head/${suffix}`, headRemoteRef: `refs/heads/${context.head.ref}`, }; } @@ -87,7 +87,7 @@ export async function withPullRequestWorktree( repoCwd: string = process.cwd(), ) { const worktreeDir = await fs.mkdtemp( - path.join(os.tmpdir(), "changesets-action-pr-status-comment-"), + path.join(os.tmpdir(), "changesets-action-pr-status-"), ); const refs = getRefNames(context); From d288cadd5af95eaa2fd98fdd330d9697f1874cf2 Mon Sep 17 00:00:00 2001 From: bluwy Date: Mon, 25 May 2026 15:00:51 +0800 Subject: [PATCH 13/13] Loosen event requirement --- pr-status/README.md | 11 ++++++++++- src/pr-status/index.ts | 6 +++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pr-status/README.md b/pr-status/README.md index 0b254cc5..c3055a23 100644 --- a/pr-status/README.md +++ b/pr-status/README.md @@ -2,7 +2,16 @@ This action generates the changeset status in PRs, e.g. whether it has changeset files and which packages will be released if the PR is merged. -It requires the repo to be checked out, and automatically fetches the PR head ref into a temporary detached worktree in order to infer the changed files and packages. It also requires the [`pull_request_target`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target) event to be triggered in order to have permissions to comment on the PR and to work in PRs from forks. You can also use the `pull_request` event if you prefer to lock permissions down and not run for PRs from forks. +It requires the repo to be checked out, and automatically fetches the PR head ref into a temporary detached worktree in order to infer the changed files and packages. It also requires the [`pull_request_target`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target) event to be triggered in order to have permissions to comment on the PR and to work in PRs from forks. + +You can also use the [`pull_request`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request) event if you prefer to lock permissions down and not run for PRs from forks. Make sure to add an if check to prevent the action from failing in fork PRs: + +```yaml +jobs: + pr-status: + if: github.event.pull_request.head.repo.full_name == github.repository + # ... +``` See the [action metadata](action.yml) for details on the inputs and outputs. diff --git a/src/pr-status/index.ts b/src/pr-status/index.ts index 3ceedc9a..816ae095 100644 --- a/src/pr-status/index.ts +++ b/src/pr-status/index.ts @@ -5,15 +5,15 @@ import { getCommentMessage } from "./message.ts"; (async () => { const context = github.context.payload.pull_request; if (!context) { - core.error("This action should only be run on pull_request_target events"); + core.error( + "This action should only be run on `pull_request_target` or `pull_request` events", + ); return; } core.info("Creating comment message..."); const commentBody = await getCommentMessage(context); - core.setOutput("commentBody", commentBody); - core.info("Done!"); })().catch((err) => { core.error(err);