Skip to content

Commit a2b80d2

Browse files
simllllclaude
andauthored
ci: use npm Trusted Publishing and add changeset check (#7)
* ci: use npm Trusted Publishing and add changeset check - Remove NPM_TOKEN dependency, use OIDC-based Trusted Publishing - Add NPM_CONFIG_PROVENANCE for provenance attestation - Add changeset-check.yml to enforce changesets in PRs * ci: add changeset bot for PR comments - Add /changeset command to create changesets via PR comments - Update changeset-check to post helpful instructions - Supports: /changeset patch|minor|major [packages] * ci: add emoji reactions to changeset bot --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent dd0dc27 commit a2b80d2

3 files changed

Lines changed: 285 additions & 4 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
name: Changeset Bot
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
jobs:
8+
changeset:
9+
name: Create Changeset
10+
if: |
11+
github.event.issue.pull_request &&
12+
startsWith(github.event.comment.body, '/changeset')
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: write
16+
pull-requests: write
17+
18+
steps:
19+
- name: React to comment
20+
uses: actions/github-script@v7
21+
with:
22+
script: |
23+
await github.rest.reactions.createForIssueComment({
24+
owner: context.repo.owner,
25+
repo: context.repo.repo,
26+
comment_id: context.payload.comment.id,
27+
content: 'eyes'
28+
});
29+
30+
- name: Get PR branch
31+
id: pr
32+
uses: actions/github-script@v7
33+
with:
34+
script: |
35+
const pr = await github.rest.pulls.get({
36+
owner: context.repo.owner,
37+
repo: context.repo.repo,
38+
pull_number: context.issue.number
39+
});
40+
core.setOutput('branch', pr.data.head.ref);
41+
core.setOutput('title', pr.data.title);
42+
43+
- name: Checkout PR branch
44+
uses: actions/checkout@v4
45+
with:
46+
ref: ${{ steps.pr.outputs.branch }}
47+
fetch-depth: 0
48+
49+
- name: Install pnpm
50+
uses: pnpm/action-setup@v4
51+
52+
- name: Setup Node.js
53+
uses: actions/setup-node@v4
54+
with:
55+
node-version: '20'
56+
cache: 'pnpm'
57+
58+
- name: Install dependencies
59+
run: pnpm install --frozen-lockfile
60+
61+
- name: Parse command
62+
id: parse
63+
uses: actions/github-script@v7
64+
with:
65+
script: |
66+
const comment = context.payload.comment.body.trim();
67+
const lines = comment.split('\n');
68+
const firstLine = lines[0].trim();
69+
70+
// Parse: /changeset [patch|minor|major] [package1,package2]
71+
const match = firstLine.match(/^\/changeset\s+(patch|minor|major)(?:\s+(.+))?$/i);
72+
73+
if (!match) {
74+
core.setFailed('Invalid format. Use: /changeset <patch|minor|major> [package1,package2]');
75+
return;
76+
}
77+
78+
const bump = match[1].toLowerCase();
79+
const packagesArg = match[2];
80+
81+
// Get summary from rest of comment or PR title
82+
let summary = lines.slice(1).join('\n').trim();
83+
if (!summary) {
84+
summary = '${{ steps.pr.outputs.title }}';
85+
}
86+
87+
core.setOutput('bump', bump);
88+
core.setOutput('packages', packagesArg || '');
89+
core.setOutput('summary', summary);
90+
91+
- name: Get changed packages
92+
id: packages
93+
run: |
94+
if [ -n "${{ steps.parse.outputs.packages }}" ]; then
95+
echo "packages=${{ steps.parse.outputs.packages }}" >> $GITHUB_OUTPUT
96+
else
97+
# Auto-detect from changed files
98+
CHANGED=$(git diff --name-only origin/main...HEAD | grep -E "^(ts-cache|storages)/" | cut -d'/' -f1-2 | sort -u | tr '\n' ',' | sed 's/,$//')
99+
if [ -z "$CHANGED" ]; then
100+
CHANGED="ts-cache"
101+
fi
102+
echo "packages=$CHANGED" >> $GITHUB_OUTPUT
103+
fi
104+
105+
- name: Create changeset
106+
run: |
107+
BUMP="${{ steps.parse.outputs.bump }}"
108+
PACKAGES="${{ steps.packages.outputs.packages }}"
109+
SUMMARY="${{ steps.parse.outputs.summary }}"
110+
111+
# Generate random filename
112+
FILENAME=".changeset/$(echo $RANDOM | md5sum | head -c 12).md"
113+
114+
# Map folder names to package names
115+
get_package_name() {
116+
case "$1" in
117+
ts-cache) echo "@node-ts-cache/core" ;;
118+
storages/lru) echo "@node-ts-cache/lru-storage" ;;
119+
storages/redis) echo "@node-ts-cache/redis-storage" ;;
120+
storages/redisio) echo "@node-ts-cache/ioredis-storage" ;;
121+
storages/node-cache) echo "@node-ts-cache/node-cache-storage" ;;
122+
storages/lru-redis) echo "@node-ts-cache/lru-redis-storage" ;;
123+
*) echo "$1" ;;
124+
esac
125+
}
126+
127+
# Create changeset content
128+
echo "---" > "$FILENAME"
129+
130+
IFS=',' read -ra PKGS <<< "$PACKAGES"
131+
for pkg in "${PKGS[@]}"; do
132+
pkg=$(echo "$pkg" | xargs) # trim whitespace
133+
pkg_name=$(get_package_name "$pkg")
134+
echo "\"$pkg_name\": $BUMP" >> "$FILENAME"
135+
done
136+
137+
echo "---" >> "$FILENAME"
138+
echo "" >> "$FILENAME"
139+
echo "$SUMMARY" >> "$FILENAME"
140+
141+
echo "Created changeset:"
142+
cat "$FILENAME"
143+
144+
- name: Commit and push
145+
run: |
146+
git config --global user.name "github-actions[bot]"
147+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
148+
git add .changeset/
149+
git commit -m "chore: add changeset"
150+
git push
151+
152+
- name: React success
153+
uses: actions/github-script@v7
154+
with:
155+
script: |
156+
await github.rest.reactions.createForIssueComment({
157+
owner: context.repo.owner,
158+
repo: context.repo.repo,
159+
comment_id: context.payload.comment.id,
160+
content: 'rocket'
161+
});
162+
163+
- name: Comment success
164+
uses: actions/github-script@v7
165+
with:
166+
script: |
167+
await github.rest.issues.createComment({
168+
owner: context.repo.owner,
169+
repo: context.repo.repo,
170+
issue_number: context.issue.number,
171+
body: `✅ Changeset created!\n\n**Type:** ${{ steps.parse.outputs.bump }}\n**Packages:** ${{ steps.packages.outputs.packages }}\n**Summary:** ${{ steps.parse.outputs.summary }}`
172+
});
173+
174+
- name: React failure
175+
if: failure()
176+
uses: actions/github-script@v7
177+
with:
178+
script: |
179+
await github.rest.reactions.createForIssueComment({
180+
owner: context.repo.owner,
181+
repo: context.repo.repo,
182+
comment_id: context.payload.comment.id,
183+
content: 'confused'
184+
});
185+
186+
- name: Comment failure
187+
if: failure()
188+
uses: actions/github-script@v7
189+
with:
190+
script: |
191+
await github.rest.issues.createComment({
192+
owner: context.repo.owner,
193+
repo: context.repo.repo,
194+
issue_number: context.issue.number,
195+
body: `❌ Failed to create changeset. Please check the [workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.\n\n**Usage:**\n\`\`\`\n/changeset patch\n/changeset minor\n/changeset major @node-ts-cache/core,@node-ts-cache/lru-storage\n\`\`\`\n\nAdd a summary on the next line:\n\`\`\`\n/changeset patch\nFixed a bug in cache expiration\n\`\`\``
196+
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: Changeset Check
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
8+
jobs:
9+
check:
10+
name: Check for changeset
11+
runs-on: ubuntu-latest
12+
permissions:
13+
pull-requests: write
14+
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
with:
19+
fetch-depth: 0
20+
21+
- name: Check for changeset
22+
id: check
23+
run: |
24+
# Skip check for release PRs
25+
if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then
26+
echo "skip=true" >> $GITHUB_OUTPUT
27+
exit 0
28+
fi
29+
30+
# Check if any changeset files exist (excluding config.json and README)
31+
CHANGESETS=$(find .changeset -name "*.md" -type f ! -name "README.md" 2>/dev/null | wc -l)
32+
33+
if [ "$CHANGESETS" -eq 0 ]; then
34+
echo "found=false" >> $GITHUB_OUTPUT
35+
exit 0
36+
fi
37+
38+
echo "found=true" >> $GITHUB_OUTPUT
39+
echo "count=$CHANGESETS" >> $GITHUB_OUTPUT
40+
41+
- name: Post or update comment
42+
if: steps.check.outputs.skip != 'true'
43+
uses: actions/github-script@v7
44+
with:
45+
script: |
46+
const found = '${{ steps.check.outputs.found }}' === 'true';
47+
const count = '${{ steps.check.outputs.count }}';
48+
49+
const marker = '<!-- changeset-check-comment -->';
50+
51+
let body;
52+
if (found) {
53+
body = `${marker}\n✅ **Changeset found** (${count} changeset file(s))\n\nThis PR will be included in the next release.`;
54+
} else {
55+
body = `${marker}\n⚠️ **No changeset found**\n\nThis PR doesn't have a changeset. If this PR should trigger a release, please add one:\n\n### Option 1: Comment command\nComment on this PR:\n\`\`\`\n/changeset patch\nYour change description here\n\`\`\`\n\nOr specify packages:\n\`\`\`\n/changeset minor @node-ts-cache/core\nAdded new caching feature\n\`\`\`\n\n### Option 2: Manual\n\`\`\`bash\npnpm changeset\ngit add .changeset && git commit -m "chore: add changeset" && git push\n\`\`\`\n\n---\n*If this PR doesn't need a release (e.g., docs, CI changes), you can ignore this message.*`;
56+
}
57+
58+
// Find existing comment
59+
const comments = await github.rest.issues.listComments({
60+
owner: context.repo.owner,
61+
repo: context.repo.repo,
62+
issue_number: context.issue.number
63+
});
64+
65+
const existing = comments.data.find(c => c.body.includes(marker));
66+
67+
if (existing) {
68+
await github.rest.issues.updateComment({
69+
owner: context.repo.owner,
70+
repo: context.repo.repo,
71+
comment_id: existing.id,
72+
body
73+
});
74+
} else {
75+
await github.rest.issues.createComment({
76+
owner: context.repo.owner,
77+
repo: context.repo.repo,
78+
issue_number: context.issue.number,
79+
body
80+
});
81+
}
82+
83+
// Fail the check if no changeset found
84+
if (!found) {
85+
core.setFailed('No changeset found. See comment for instructions.');
86+
}

.github/workflows/publish.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,10 @@ jobs:
4646
id: changesets
4747
uses: changesets/action@v1
4848
with:
49-
publish: pnpm release
50-
version: pnpm version
49+
publish: pnpm run release
50+
version: pnpm run version
5151
title: 'chore: release packages'
5252
commit: 'chore: release packages'
5353
env:
5454
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
56-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
55+
NPM_CONFIG_PROVENANCE: true

0 commit comments

Comments
 (0)