diff --git a/.changeset/curly-cameras-matter.md b/.changeset/curly-cameras-matter.md new file mode 100644 index 00000000..3a91556e --- /dev/null +++ b/.changeset/curly-cameras-matter.md @@ -0,0 +1,5 @@ +--- +"@changesets/action": minor +--- + +Add a new `@changesets/action/pr-comment` sub-action to comment on PRs diff --git a/README.md b/README.md index 5f41559a..938eac9b 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,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](./pr-status/README.md): Generate changeset status in PRs. +- [pr-comment](./pr-comment/README.md): Comment on PRs. ## Usage diff --git a/pr-comment/README.md b/pr-comment/README.md new file mode 100644 index 00000000..268ec2ef --- /dev/null +++ b/pr-comment/README.md @@ -0,0 +1,52 @@ +# @changesets/action/pr-comment + +A simple GitHub Action to comment on PRs aimed to complement [`@changesets/action/pr-status`](../pr-status/README.md). + +This action is intentionally simple without advanced features. Check out other actions if so, such as [mshick/add-pr-comment](https://github.com/marketplace/actions/add-pr-comment) and [peter-evans/create-or-update-comment](https://github.com/marketplace/actions/create-or-update-comment). + +See the [action metadata](action.yml) for details on the inputs and outputs. + +## Example setup + +```yaml +name: PR Comment + +on: + pull_request: + +jobs: + pr-comment: + runs-on: ubuntu-slim + permissions: + pull-requests: write # to create and update comments on PRs + steps: + - uses: changesets/action/pr-comment@v1 + with: + body: Hello world! +``` + +When called repeatedly, the action will update the comment it created by default. If you use this action to create different types of comments, pass an `update-id` value to differentiate them. + +```yaml +jobs: + pr-comment: + # ... + steps: + - uses: changesets/action/pr-comment@v1 + with: + body: Hello world! + update-id: my-tag +``` + +If you want to always create new comments, pass an empty value to `update-id`. + +```yaml +jobs: + pr-comment: + # ... + steps: + - uses: changesets/action/pr-comment@v1 + with: + body: Hello world! + update-id: "" +``` diff --git a/pr-comment/action.yml b/pr-comment/action.yml new file mode 100644 index 00000000..273da211 --- /dev/null +++ b/pr-comment/action.yml @@ -0,0 +1,25 @@ +name: Changesets - PR Comment +description: A simple GitHub Action to comment on PRs +runs: + using: node24 + main: ../dist/pr-comment.js +inputs: + github-token: + description: "The GitHub token to use for authentication. Defaults to the GitHub-provided token." + required: false + default: ${{ github.token }} + body: + description: "The comment body to post on the PR." + required: true + update-id: + description: > + By default, the action will create and update a comment with this id. Pass a different id to + create and update a new comment, or pass an empty string to disable updating comments. + required: false + default: "changesets-action-pr-comment" +outputs: + comment-id: + description: "The comment id of the comment that was created or updated." +branding: + icon: message-circle + color: blue diff --git a/pr-status/README.md b/pr-status/README.md index 6ae780a2..7750eb4d 100644 --- a/pr-status/README.md +++ b/pr-status/README.md @@ -56,7 +56,6 @@ jobs: uses: changesets/action/pr-comment@v1 with: body: ${{ needs.pr-status.outputs.comment-body }} - update-id: changesets-pr-status ``` The workflow uses [`@changesets/action/pr-comment`](../pr-comment/README.md), which is a simple GitHub Action to comment on PRs. diff --git a/rolldown.config.js b/rolldown.config.js index b3359c73..814f4f4b 100644 --- a/rolldown.config.js +++ b/rolldown.config.js @@ -4,6 +4,7 @@ export default defineConfig({ input: { index: "src/index.ts", ["pr-status"]: "src/pr-status/index.ts", + ["pr-comment"]: "src/pr-comment/index.ts", }, output: { dir: "dist", diff --git a/src/pr-comment/index.ts b/src/pr-comment/index.ts new file mode 100644 index 00000000..f2803322 --- /dev/null +++ b/src/pr-comment/index.ts @@ -0,0 +1,77 @@ +import * as core from "@actions/core"; +import * as github from "@actions/github"; + +type Octokit = ReturnType; +type CreateCommentParams = NonNullable< + Parameters[0] +>; +type UpdateCommentParams = NonNullable< + Parameters[0] +>; + +try { + await main(); +} catch (err) { + core.setFailed((err as Error).message); +} + +async function main() { + const context = github.context.payload.pull_request; + if (!context) { + throw new Error( + "This action should only be run on `pull_request_target` or `pull_request` events", + ); + } + + const githubToken = core.getInput("github-token", { required: true }); + const body = core.getInput("body", { required: true }); + const updateId = core.getInput("update-id"); + + const commentMarker = updateId ? getCommentMarker(updateId) : null; + const commentBody = commentMarker ? `${commentMarker}\n\n${body}` : body; + const commentParam: CreateCommentParams | UpdateCommentParams = { + repo: context.base.repo.name, + owner: context.base.repo.owner.login, + issue_number: context.number, + body: commentBody, + }; + + const octokit = github.getOctokit(githubToken); + + let existingCommentId: number | undefined; + if (commentMarker) { + core.info("Checking for existing comment..."); + 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, + }); + core.setOutput("comment-id", existingCommentId); + } else { + core.info("Creating new comment..."); + const result = await octokit.rest.issues.createComment(commentParam); + core.setOutput("comment-id", result.data.id); + } + + core.info("Done!"); +} + +function getCommentMarker(updateId: string) { + const prefix = "changesets-action-pr-comment"; + return prefix === updateId + ? `` + : ``; +}