Skip to content

Commit 1d4fd9a

Browse files
authored
feat(bot): add a custom changeset bot (#2)
* feat(bot): add a custom changeset bot * chore(bot): update human-id import * chore: add test changeset * chore(bot): update comment to only show info about 1 package * chore(bot): update wording in comment * chore: move bot into main pnpm workspace, add to tsconfig * chore(bot): update emoji * chore(bot): update comment links * chore(bot): use a bullet point instaed of a dash between links * docs(changesets): add a guide for changesets
1 parent 5a0f94d commit 1d4fd9a

File tree

11 files changed

+725
-1
lines changed

11 files changed

+725
-1
lines changed

.changeset/khaki-kids-juggle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gravitational/design-system': patch
3+
---
4+
5+
this is a test changeset to test publishing

.github/workflows/bot.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Changeset Bot
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: write
10+
issues: write
11+
12+
env:
13+
NODE_VERSION: 22.x
14+
15+
jobs:
16+
changeset-bot:
17+
name: Changeset Bot
18+
runs-on: ubuntu-latest
19+
if: ${{ !startsWith(github.event.pull_request.head.ref, 'changeset-release') }}
20+
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v5
24+
with:
25+
fetch-depth: 0
26+
27+
- uses: actions/setup-node@v4
28+
with:
29+
node-version: ${{ env.NODE_VERSION }}
30+
31+
- name: Enable Corepack
32+
run: corepack enable
33+
34+
- name: Install dependencies
35+
run: pnpm install --frozen-lockfile
36+
37+
- name: Install bot dependencies
38+
run: pnpm install --frozen-lockfile
39+
working-directory: bot
40+
41+
- name: Run changeset bot
42+
env:
43+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44+
run: |
45+
pnpm bot \
46+
${{ github.repository_owner }} \
47+
${{ github.event.repository.name }} \
48+
${{ github.event.pull_request.number }} \
49+
${{ github.event.action }}

bot/getChangedPackages.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import assembleReleasePlan from '@changesets/assemble-release-plan';
2+
import { parse as parseConfig } from '@changesets/config';
3+
import parseChangeset from '@changesets/parse';
4+
import type {
5+
NewChangeset,
6+
PackageJSON,
7+
PreState,
8+
WrittenConfig,
9+
} from '@changesets/types';
10+
import type { Package, Packages } from '@manypkg/get-packages';
11+
import type { Octokit } from '@octokit/rest';
12+
import fetch from 'node-fetch';
13+
14+
export async function getChangedPackages({
15+
owner,
16+
repo,
17+
ref,
18+
changedFiles: changedFilesPromise,
19+
octokit,
20+
}: {
21+
owner: string;
22+
repo: string;
23+
ref: string;
24+
changedFiles: string[] | Promise<string[]>;
25+
octokit: InstanceType<typeof Octokit>;
26+
}) {
27+
let hasErrored = false;
28+
29+
const githubToken = process.env.GITHUB_TOKEN;
30+
31+
function fetchFile(path: string) {
32+
return fetch(
33+
`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`,
34+
{
35+
headers: {
36+
Authorization: `Bearer ${githubToken}`,
37+
},
38+
}
39+
);
40+
}
41+
42+
async function fetchJsonFile<T>(path: string): Promise<T> {
43+
try {
44+
const x = await fetchFile(path);
45+
46+
return (await x.json()) as T;
47+
} catch (err) {
48+
hasErrored = true;
49+
50+
// eslint-disable-next-line no-console
51+
console.error(err);
52+
53+
return {} as T;
54+
}
55+
}
56+
57+
async function fetchTextFile(path: string) {
58+
try {
59+
const x = await fetchFile(path);
60+
61+
return await x.text();
62+
} catch (err) {
63+
hasErrored = true;
64+
65+
// eslint-disable-next-line no-console
66+
console.error(err);
67+
68+
return '';
69+
}
70+
}
71+
72+
const rootPackageJsonContentsPromise =
73+
fetchJsonFile<PackageJSON>('package.json');
74+
const configPromise: Promise<WrittenConfig> = fetchJsonFile(
75+
'.changeset/config.json'
76+
);
77+
78+
const tree = await octokit.rest.git.getTree({
79+
owner,
80+
repo,
81+
recursive: '1',
82+
tree_sha: ref,
83+
});
84+
85+
const changedFiles = await changedFilesPromise;
86+
87+
let preStatePromise: Promise<PreState> | undefined;
88+
let changesetPromises: Promise<NewChangeset>[] = [];
89+
90+
for (let item of tree.data.tree) {
91+
if (!item.path) {
92+
continue;
93+
}
94+
95+
if (item.path === '.changeset/pre.json') {
96+
preStatePromise = fetchJsonFile('.changeset/pre.json');
97+
98+
continue;
99+
}
100+
101+
if (
102+
item.path !== '.changeset/README.md' &&
103+
item.path.startsWith('.changeset') &&
104+
item.path.endsWith('.md') &&
105+
changedFiles.includes(item.path)
106+
) {
107+
const res = /\.changeset\/([^.]+)\.md/.exec(item.path);
108+
if (!res) {
109+
throw new Error('could not get name from changeset filename');
110+
}
111+
112+
const id = res[1];
113+
114+
changesetPromises.push(
115+
fetchTextFile(item.path).then(text => {
116+
return { ...parseChangeset(text), id };
117+
})
118+
);
119+
}
120+
}
121+
122+
let rootPackageJsonContent = await rootPackageJsonContentsPromise;
123+
124+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
125+
if (hasErrored) {
126+
throw new Error('an error occurred when fetching files');
127+
}
128+
129+
const root: Package = {
130+
dir: '.',
131+
packageJson: rootPackageJsonContent,
132+
};
133+
134+
const packages: Packages = {
135+
root,
136+
tool: 'pnpm',
137+
packages: [root],
138+
};
139+
140+
const releasePlan = assembleReleasePlan(
141+
await Promise.all(changesetPromises),
142+
packages,
143+
await configPromise.then(rawConfig => parseConfig(rawConfig, packages)),
144+
await preStatePromise
145+
);
146+
147+
return {
148+
changedPackages: packages.packages
149+
.filter(pkg =>
150+
changedFiles.some(changedFile => changedFile.startsWith(`${pkg.dir}/`))
151+
)
152+
.map(x => x.packageJson.name),
153+
releasePlan,
154+
};
155+
}

0 commit comments

Comments
 (0)