Skip to content

Commit ae5dbf9

Browse files
authored
feat: automatic dependabot pr changesets (#897)
1 parent 766e5c1 commit ae5dbf9

2 files changed

Lines changed: 190 additions & 0 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Creates a changeset file for Dependabot PRs that update production
3+
* dependencies in published @knocklabs/* packages.
4+
*
5+
* Skips devDependency-only changes since those don't require a release.
6+
*
7+
* This script is only run from a workflow gated to dependabot[bot],
8+
* so we trust that the changes are dependency updates.
9+
*
10+
* Environment variables:
11+
* PR_TITLE - The pull request title (used as the changeset description)
12+
* PR_NUMBER - The pull request number (used in the changeset filename)
13+
* GITHUB_OUTPUT - Path to the GitHub Actions output file
14+
*/
15+
16+
const { execSync } = require("child_process");
17+
const fs = require("fs");
18+
const path = require("path");
19+
20+
const prTitle = process.env.PR_TITLE;
21+
const prNumber = process.env.PR_NUMBER;
22+
const githubOutput = process.env.GITHUB_OUTPUT;
23+
24+
if (!prTitle || !prNumber || !githubOutput) {
25+
console.error(
26+
"Missing required environment variables: PR_TITLE, PR_NUMBER, GITHUB_OUTPUT",
27+
);
28+
process.exit(1);
29+
}
30+
31+
const changesetFile = path.join(".changeset", `dependabot-pr-${prNumber}.md`);
32+
33+
function setOutput(key, value) {
34+
fs.appendFileSync(githubOutput, `${key}=${value}\n`);
35+
}
36+
37+
// If changeset already exists, skip
38+
if (fs.existsSync(changesetFile)) {
39+
console.log("Changeset already exists, skipping");
40+
setOutput("created", "false");
41+
process.exit(0);
42+
}
43+
44+
// Find all package.json files changed in the latest commit
45+
const diffOutput = execSync(
46+
"git diff --name-only HEAD~1 HEAD -- '**/package.json'",
47+
{ encoding: "utf-8" },
48+
).trim();
49+
50+
if (!diffOutput) {
51+
console.log("No package.json files changed");
52+
setOutput("created", "false");
53+
process.exit(0);
54+
}
55+
56+
const changedFiles = diffOutput.split("\n").filter(Boolean);
57+
const packages = [];
58+
59+
for (const pkgFile of changedFiles) {
60+
const content = JSON.parse(fs.readFileSync(pkgFile, "utf-8"));
61+
const pkgName = content.name || "";
62+
63+
if (content.private || !pkgName.startsWith("@knocklabs/")) {
64+
continue;
65+
}
66+
67+
// Only include packages where production dependencies changed,
68+
// not devDependency-only updates
69+
let oldDeps = {};
70+
try {
71+
const oldContent = execSync(`git show "HEAD~1:${pkgFile}"`, {
72+
encoding: "utf-8",
73+
});
74+
oldDeps = JSON.parse(oldContent).dependencies || {};
75+
} catch {
76+
// File may not exist in previous commit
77+
}
78+
79+
const newDeps = content.dependencies || {};
80+
const sortedOld = JSON.stringify(oldDeps, Object.keys(oldDeps).sort());
81+
const sortedNew = JSON.stringify(newDeps, Object.keys(newDeps).sort());
82+
83+
if (sortedOld === sortedNew) {
84+
console.log(`Only devDependency changes in ${pkgName}, skipping`);
85+
continue;
86+
}
87+
88+
packages.push(pkgName);
89+
}
90+
91+
const uniquePackages = [...new Set(packages)].sort();
92+
93+
if (uniquePackages.length === 0) {
94+
console.log("No published @knocklabs packages were affected");
95+
setOutput("created", "false");
96+
process.exit(0);
97+
}
98+
99+
// Generate changeset file
100+
const lines = [
101+
"---",
102+
...uniquePackages.map((pkg) => `"${pkg}": patch`),
103+
"---",
104+
"",
105+
prTitle,
106+
"",
107+
];
108+
109+
fs.writeFileSync(changesetFile, lines.join("\n"));
110+
111+
console.log(`Created changeset: ${changesetFile}`);
112+
console.log(fs.readFileSync(changesetFile, "utf-8"));
113+
setOutput("created", "true");
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Dependabot Changeset
2+
3+
on:
4+
pull_request_target:
5+
types: [opened]
6+
workflow_dispatch:
7+
inputs:
8+
pr-number:
9+
description: "Pull request number to add a changeset for"
10+
required: true
11+
type: string
12+
13+
jobs:
14+
changeset:
15+
name: Add changeset for dependency update
16+
runs-on: ubuntu-latest
17+
if: github.actor == 'dependabot[bot]' || github.event_name == 'workflow_dispatch'
18+
19+
steps:
20+
- name: Get PR metadata
21+
id: pr
22+
env:
23+
GH_TOKEN: ${{ secrets.KNOCK_ENG_BOT_GITHUB_TOKEN }}
24+
EVENT_NAME: ${{ github.event_name }}
25+
INPUT_PR_NUMBER: ${{ inputs.pr-number }}
26+
REPO: ${{ github.repository }}
27+
PR_NUMBER_FROM_EVENT: ${{ github.event.pull_request.number }}
28+
PR_TITLE_FROM_EVENT: ${{ github.event.pull_request.title }}
29+
PR_REF_FROM_EVENT: ${{ github.event.pull_request.head.ref }}
30+
run: |
31+
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
32+
PR_JSON=$(gh pr view "$INPUT_PR_NUMBER" --repo "$REPO" --json title,headRefName)
33+
echo "number=$INPUT_PR_NUMBER" >> "$GITHUB_OUTPUT"
34+
echo "title=$(echo "$PR_JSON" | jq -r '.title')" >> "$GITHUB_OUTPUT"
35+
echo "ref=$(echo "$PR_JSON" | jq -r '.headRefName')" >> "$GITHUB_OUTPUT"
36+
else
37+
echo "number=$PR_NUMBER_FROM_EVENT" >> "$GITHUB_OUTPUT"
38+
echo "title=$PR_TITLE_FROM_EVENT" >> "$GITHUB_OUTPUT"
39+
echo "ref=$PR_REF_FROM_EVENT" >> "$GITHUB_OUTPUT"
40+
fi
41+
42+
# Checkout the base branch to get the trusted version of the script,
43+
# then checkout the PR branch on top to get the package.json changes.
44+
- name: Checkout base branch
45+
uses: actions/checkout@v4
46+
with:
47+
ref: main
48+
token: ${{ secrets.KNOCK_ENG_BOT_GITHUB_TOKEN }}
49+
path: base
50+
51+
- name: Checkout PR branch
52+
uses: actions/checkout@v4
53+
with:
54+
ref: ${{ steps.pr.outputs.ref }}
55+
token: ${{ secrets.KNOCK_ENG_BOT_GITHUB_TOKEN }}
56+
fetch-depth: 2
57+
58+
- name: Setup Node.js
59+
uses: actions/setup-node@v4
60+
with:
61+
node-version-file: "package.json"
62+
63+
- name: Detect affected packages and create changeset
64+
id: changeset
65+
env:
66+
PR_TITLE: ${{ steps.pr.outputs.title }}
67+
PR_NUMBER: ${{ steps.pr.outputs.number }}
68+
run: node base/.github/scripts/dependabot-changeset.js
69+
70+
- name: Commit and push changeset
71+
if: steps.changeset.outputs.created == 'true'
72+
run: |
73+
git config user.name "knock-eng-bot"
74+
git config user.email "knock-eng-bot@users.noreply.github.com"
75+
git add .changeset/
76+
git commit -m "chore(deps): add changeset for dependency update"
77+
git push

0 commit comments

Comments
 (0)