Skip to content

Commit 8795eee

Browse files
bluwyAndarist
andauthored
Comment changeset status in PRs (#625)
* Initial implementation * Fix * fix * fix * Make it look nicer * Update docs * Rename to pr-status-comment * Remove outputs * Update path * update name * Checkout PR in a detached worktree (#626) Co-authored-by: bluwy <bjornlu.dev@gmail.com> * Move comment to separate job * Loosen event requirement * Update * Apply suggestions from code review Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com> * Use TLA * use using * Remove test log --------- Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
1 parent 34f64f6 commit 8795eee

12 files changed

Lines changed: 599 additions & 2 deletions

File tree

.changeset/wacky-taxis-sink.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@changesets/action": minor
3+
---
4+
5+
Add a new `@changesets/action/pr-status` sub-action to generate the changeset status comment for PRs as an alternative to the [Changesets Bot](https://github.com/apps/changeset-bot).

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
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.
44

5+
There are also sub-actions hosted in this repository. Check out their respective READMEs for more details:
6+
7+
- [pr-status](./pr-status/README.md): Generate changeset status in PRs.
8+
59
## Usage
610

711
### Inputs

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@
2020
"@actions/core": "^3.0.1",
2121
"@actions/exec": "^3.0.0",
2222
"@actions/github": "^9.1.1",
23+
"@changesets/get-release-plan": "^4.0.16",
2324
"@changesets/ghcommit": "^2.0.1",
25+
"@changesets/git": "^3.0.4",
2426
"@changesets/pre": "^2.0.2",
2527
"@changesets/read": "^0.6.7",
2628
"@manypkg/get-packages": "^1.1.3",
2729
"@octokit/core": "^7.0.6",
2830
"@octokit/plugin-throttling": "^11.0.3",
29-
"semver": "^7.8.0"
31+
"human-id": "^4.1.3",
32+
"markdown-table": "^3.0.4",
33+
"semver": "^7.8.0",
34+
"tinyexec": "^1.1.2"
3035
},
3136
"devDependencies": {
3237
"@changesets/changelog-github": "^0.7.0",

pnpm-lock.yaml

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pr-status/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# changesets/action/pr-status
2+
3+
This action generates the changesets status in PRs, e.g. whether it has changeset files and which packages will be released if the PR is merged.
4+
5+
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.
6+
7+
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:
8+
9+
```yaml
10+
jobs:
11+
pr-status:
12+
if: github.event.pull_request.head.repo.full_name == github.repository
13+
# ...
14+
```
15+
16+
See the [action metadata](action.yml) for details on the inputs and outputs.
17+
18+
> [!WARNING]
19+
> **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).
20+
21+
## Example setup
22+
23+
```yaml
24+
# .github/workflows/comment-changesets-pr-status.yml
25+
name: Comment Changesets status in PRs
26+
27+
on:
28+
pull_request_target:
29+
30+
concurrency:
31+
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
32+
cancel-in-progress: true
33+
34+
jobs:
35+
pr-status:
36+
runs-on: ubuntu-slim
37+
permissions:
38+
contents: read # to check out files in the repo
39+
outputs:
40+
comment-body: ${{ steps.pr-status.outputs.comment-body }}
41+
steps:
42+
- name: Check out repo
43+
uses: actions/checkout@v6
44+
45+
- name: Generate status
46+
id: pr-status
47+
uses: changesets/action/pr-status@v1
48+
49+
pr-comment:
50+
needs: pr-status
51+
runs-on: ubuntu-slim
52+
permissions:
53+
pull-requests: write # to create and update comments on PRs
54+
steps:
55+
- name: Comment on PR
56+
uses: changesets/action/pr-comment@v1
57+
with:
58+
body: ${{ needs.pr-status.outputs.comment-body }}
59+
update-id: changesets-pr-status
60+
```
61+
62+
The workflow uses [`@changesets/action/pr-comment`](../pr-comment/README.md), which is a simple GitHub Action to comment on PRs.

pr-status/action.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: Changesets - PR Status
2+
description: Generate the changesets status in PRs
3+
runs:
4+
using: node24
5+
main: ../dist/pr-status.js
6+
inputs: {}
7+
outputs:
8+
comment-body:
9+
description: The generated comment body to present the changesets status in PRs.
10+
branding:
11+
icon: package
12+
color: blue

rolldown.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { defineConfig } from "rolldown";
22

33
export default defineConfig({
4-
input: "src/index.ts",
4+
input: {
5+
index: "src/index.ts",
6+
["pr-status"]: "src/pr-status/index.ts",
7+
},
58
output: {
69
dir: "dist",
710
format: "esm",

src/pr-status/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as core from "@actions/core";
2+
import * as github from "@actions/github";
3+
import { getCommentMessage } from "./message.ts";
4+
5+
try {
6+
await main();
7+
} catch (err) {
8+
core.setFailed((err as Error).message);
9+
}
10+
11+
async function main() {
12+
const context = github.context.payload.pull_request;
13+
if (!context) {
14+
throw new Error(
15+
"This action should only be run on `pull_request_target` or `pull_request` events",
16+
);
17+
}
18+
19+
core.info("Creating comment message...");
20+
const commentBody = await getCommentMessage(context);
21+
core.setOutput("comment-body", commentBody);
22+
core.info("Done!");
23+
}

src/pr-status/message.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import * as github from "@actions/github";
2+
import getReleasePlan from "@changesets/get-release-plan";
3+
import type {
4+
ComprehensiveRelease,
5+
ReleasePlan,
6+
VersionType,
7+
} from "@changesets/types";
8+
import { markdownTable } from "markdown-table";
9+
import {
10+
getNewChangesetTemplateContent,
11+
getNewChangesetUrl,
12+
} from "./template.ts";
13+
import { getPullRequestWorktree } from "./worktree.ts";
14+
15+
type PullRequestContext = NonNullable<
16+
typeof github.context.payload.pull_request
17+
>;
18+
19+
export async function getCommentMessage(context: PullRequestContext) {
20+
await using worktree = await getPullRequestWorktree(context);
21+
22+
const releasePlan = await getReleasePlan(worktree.cwd, worktree.baseRef);
23+
const templateContent = await getNewChangesetTemplateContent(
24+
worktree.cwd,
25+
worktree.baseRef,
26+
context.title,
27+
);
28+
29+
const newChangesetUrl = getNewChangesetUrl(
30+
context.head.repo.html_url,
31+
context.head.ref,
32+
templateContent,
33+
);
34+
35+
if (releasePlan.changesets.length > 0) {
36+
return getApproveMessage(context.head.sha, newChangesetUrl, releasePlan);
37+
} else {
38+
return getAbsentMessage(context.head.sha, newChangesetUrl, releasePlan);
39+
}
40+
}
41+
42+
function getApproveMessage(
43+
commitSha: string,
44+
newChangesetUrl: string,
45+
releasePlan: ReleasePlan,
46+
) {
47+
return `\
48+
### 🦋 Changeset detected
49+
50+
Latest commit: ${commitSha}
51+
52+
**The changes in this PR will be included in the next version bump.**
53+
54+
${getReleasePlanMessage(releasePlan)}
55+
56+
Not sure what this means? [Click here to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).
57+
58+
[Click here if you're a maintainer who wants to add another changeset to this PR](${newChangesetUrl})`;
59+
}
60+
61+
function getAbsentMessage(
62+
commitSha: string,
63+
newChangesetUrl: string,
64+
releasePlan: ReleasePlan,
65+
) {
66+
return `\
67+
### ⚠️ No Changeset found
68+
69+
Latest commit: ${commitSha}
70+
71+
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.**
72+
73+
${getReleasePlanMessage(releasePlan)}
74+
75+
[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).
76+
77+
[Click here if you're a maintainer who wants to add a changeset to this PR](${newChangesetUrl})`;
78+
}
79+
80+
function getReleasePlanMessage(releasePlan: ReleasePlan) {
81+
const publishableReleases = releasePlan.releases.filter(
82+
(r) => r.type !== "none",
83+
) as (ComprehensiveRelease & { type: Exclude<VersionType, "none"> })[];
84+
85+
const table = markdownTable([
86+
["Name", "Type"],
87+
...publishableReleases.map((release) => {
88+
return [
89+
release.name,
90+
{
91+
major: "Major",
92+
minor: "Minor",
93+
patch: "Patch",
94+
}[release.type],
95+
];
96+
}),
97+
]);
98+
99+
let summary = "This PR includes ";
100+
if (releasePlan.changesets.length === 0) {
101+
summary += "no changesets";
102+
} else {
103+
summary += `changesets to release ${publishableReleases.length} package`;
104+
if (publishableReleases.length !== 1) {
105+
summary += "s";
106+
}
107+
}
108+
109+
return `\
110+
<details>
111+
<summary>${summary}</summary>
112+
113+
${
114+
publishableReleases.length > 0
115+
? table
116+
: "When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types"
117+
}
118+
119+
</details>`;
120+
}

src/pr-status/template.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { getChangedPackagesSinceRef } from "@changesets/git";
2+
import { humanId } from "human-id";
3+
4+
export function getNewChangesetUrl(
5+
headRepoUrl: string,
6+
headRef: string,
7+
templateContent: string,
8+
) {
9+
const fileName = humanId({ separator: "-", capitalize: false });
10+
return `${headRepoUrl}/new/${headRef}?filename=.changeset/${fileName}.md&value=${encodeURIComponent(templateContent)}`;
11+
}
12+
13+
export async function getNewChangesetTemplateContent(
14+
cwd: string,
15+
baseRef: string,
16+
prTitle: string,
17+
) {
18+
const changedPackages = await getChangedPackagesSinceRef({
19+
cwd,
20+
ref: baseRef,
21+
});
22+
23+
return `\
24+
---
25+
${changedPackages.map((p) => `"${p.packageJson.name}": patch`).join("\n")}
26+
---
27+
28+
${prTitle}
29+
`;
30+
}

0 commit comments

Comments
 (0)