1313 - release/v4
1414 - release/core-2
1515 paths :
16+ - ' packages/astro/**'
1617 - ' packages/backend/**'
18+ - ' packages/chrome-extension/**'
1719 - ' packages/clerk-js/**'
20+ - ' packages/expo/**'
21+ - ' packages/expo-passkeys/**'
22+ - ' packages/express/**'
23+ - ' packages/fastify/**'
24+ - ' packages/hono/**'
25+ - ' packages/localizations/**'
1826 - ' packages/nextjs/**'
27+ - ' packages/nuxt/**'
1928 - ' packages/react/**'
29+ - ' packages/react-router/**'
2030 - ' packages/shared/**'
31+ - ' packages/tanstack-react-start/**'
32+ - ' packages/testing/**'
2133 - ' packages/ui/**'
22- - ' snapi.config.json'
34+ - ' packages/vue/**'
35+ - ' break-check.config.json'
2336 - ' .github/workflows/api-changes.yml'
2437
2538permissions :
@@ -30,14 +43,30 @@ concurrency:
3043 cancel-in-progress : ${{ github.event_name == 'pull_request' }}
3144
3245env :
33- SNAPI_PACKAGE : https://pkg.pr.new/clerk/snapi/@clerk/snapi@2479ffce1adb8ff5f1caf95509268774224c121d
34- SNAPI_FILTERS : >-
46+ # The tool was renamed snapi -> break-check (package @clerk/break-check, binary
47+ # break-check, repo clerk/break-check). Pinned to a pkg.pr.new build of a
48+ # specific commit on main.
49+ BREAK_CHECK_PACKAGE : https://pkg.pr.new/clerk/break-check/@clerk/break-check@a4db93a68e43e45a71cc958cc4a722d91c5f62b2
50+ BREAK_CHECK_FILTERS : >-
51+ --filter=@clerk/astro
3552 --filter=@clerk/backend
53+ --filter=@clerk/chrome-extension
3654 --filter=@clerk/clerk-js
55+ --filter=@clerk/expo
56+ --filter=@clerk/expo-passkeys
57+ --filter=@clerk/express
58+ --filter=@clerk/fastify
59+ --filter=@clerk/hono
60+ --filter=@clerk/localizations
3761 --filter=@clerk/nextjs
62+ --filter=@clerk/nuxt
3863 --filter=@clerk/react
64+ --filter=@clerk/react-router
3965 --filter=@clerk/shared
66+ --filter=@clerk/tanstack-react-start
67+ --filter=@clerk/testing
4068 --filter=@clerk/ui
69+ --filter=@clerk/vue
4170
4271jobs :
4372 publish-baseline :
@@ -66,18 +95,27 @@ jobs:
6695 turbo-token : ${{ secrets.TURBO_TOKEN }}
6796
6897 - name : Build declarations
69- run : pnpm turbo build:declarations $TURBO_ARGS $SNAPI_FILTERS
98+ run : pnpm turbo build:declarations $TURBO_ARGS $BREAK_CHECK_FILTERS
7099
71100 - name : Generate API snapshot
72101 run : |
73- pnpm dlx --package "$SNAPI_PACKAGE" snapi snapshot \
102+ pnpm dlx --package "$BREAK_CHECK_PACKAGE" break-check snapshot \
74103 --output "$GITHUB_WORKSPACE/.api-snapshots-baseline"
75104
105+ - name : Resolve break-check cache key
106+ id : break-check-key
107+ run : echo "ref=${BREAK_CHECK_PACKAGE##*@}" >> "$GITHUB_OUTPUT"
108+
76109 - name : Save baseline to cache
77110 uses : actions/cache/save@v4
78111 with :
79112 path : .api-snapshots-baseline
80- key : snapi-baseline-${{ github.sha }}
113+ # Fold the break-check commit into the key: a snapshot produced by one
114+ # break-check version must not be reused by another, since discovery
115+ # changes (e.g. wildcard subpath expansion) make the surfaces
116+ # incomparable and the diff degenerates into thousands of phantom
117+ # additions.
118+ key : break-check-baseline-${{ steps.break-check-key.outputs.ref }}-${{ github.sha }}
81119
82120 check-api :
83121 if : ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }}
@@ -96,6 +134,13 @@ jobs:
96134 - name : Checkout Repo
97135 uses : actions/checkout@v4
98136 with :
137+ # Pin the "current" side of the diff to the PR head, not the
138+ # refs/pull/N/merge ref checkout resolves by default. The merge ref is
139+ # the head merged into the moving tip of the base branch, so once main
140+ # advances it absorbs unrelated changes and break-check reports them as
141+ # this PR's own (clerk/break-check#32). The baseline is already pinned
142+ # to base.sha (cache key + worktree fallback below).
143+ ref : ${{ github.event.pull_request.head.sha }}
99144 fetch-depth : 100
100145 fetch-tags : false
101146 filter : ' blob:none'
@@ -109,15 +154,22 @@ jobs:
109154 turbo-team : ${{ vars.TURBO_TEAM }}
110155 turbo-token : ${{ secrets.TURBO_TOKEN }}
111156
157+ - name : Resolve break-check cache key
158+ id : break-check-key
159+ run : echo "ref=${BREAK_CHECK_PACKAGE##*@}" >> "$GITHUB_OUTPUT"
160+
112161 - name : Restore baseline from cache
113162 id : baseline-cache
114163 uses : actions/cache/restore@v4
115164 with :
116165 path : .api-snapshots-baseline
117- key : snapi-baseline-${{ github.event.pull_request.base.sha }}
166+ # Keyed on the break-check commit too, so bumping break-check misses the
167+ # stale baseline and the worktree fallback below rebuilds it with the
168+ # same version the PR runs (see publish-baseline for the rationale).
169+ key : break-check-baseline-${{ steps.break-check-key.outputs.ref }}-${{ github.event.pull_request.base.sha }}
118170
119171 - name : Build current declarations
120- run : pnpm turbo build:declarations $TURBO_ARGS $SNAPI_FILTERS
172+ run : pnpm turbo build:declarations $TURBO_ARGS $BREAK_CHECK_FILTERS
121173
122174 - name : Fetch base commit
123175 if : steps.baseline-cache.outputs.cache-matched-key == ''
@@ -127,31 +179,48 @@ jobs:
127179 if : steps.baseline-cache.outputs.cache-matched-key == ''
128180 run : |
129181 mkdir -p .worktrees
130- git worktree add --detach .worktrees/snapi-baseline "${{ github.event.pull_request.base.sha }}"
131- cp snapi.config.json .worktrees/snapi-baseline/snapi.config.json
182+ git worktree add --detach .worktrees/break-check-baseline "${{ github.event.pull_request.base.sha }}"
183+ # Snapshot the base ref with the coverage it actually had. Only seed the
184+ # config when the base tracks no coverage at all; otherwise packages
185+ # newly added to coverage in this PR get diffed against a baseline that
186+ # never tracked them (every export reads as a phantom change against the
187+ # base's bundled .d.ts), and the base ref may not even build their
188+ # declarations yet. A base from before this rename still names its config
189+ # snapi.config.json, so check both names. This reads the base's real
190+ # coverage; it is not rename-compat, and goes no-op once main carries
191+ # break-check.config.json.
192+ if [ ! -f .worktrees/break-check-baseline/break-check.config.json ] && [ ! -f .worktrees/break-check-baseline/snapi.config.json ]; then
193+ cp break-check.config.json .worktrees/break-check-baseline/break-check.config.json
194+ fi
132195
133196 - name : Install baseline dependencies
134197 if : steps.baseline-cache.outputs.cache-matched-key == ''
135- working-directory : .worktrees/snapi -baseline
198+ working-directory : .worktrees/break-check -baseline
136199 run : pnpm install --frozen-lockfile
137200
138201 - name : Build baseline declarations
139202 if : steps.baseline-cache.outputs.cache-matched-key == ''
140- working-directory : .worktrees/snapi-baseline
141- run : pnpm turbo build:declarations $TURBO_ARGS $SNAPI_FILTERS
203+ working-directory : .worktrees/break-check-baseline
204+ # --continue past per-package failures and don't fail the step: the base
205+ # ref may not build declarations for packages that only gained
206+ # build:declarations support in this PR. break-check snapshots only the
207+ # base's own coverage below, so a partial build is fine; a needed package
208+ # that fails to build still surfaces when `break-check snapshot` finds no
209+ # .d.ts.
210+ run : pnpm turbo build:declarations $TURBO_ARGS $BREAK_CHECK_FILTERS --continue || true
142211
143212 - name : Generate baseline API snapshots
144213 if : steps.baseline-cache.outputs.cache-matched-key == ''
145- working-directory : .worktrees/snapi -baseline
214+ working-directory : .worktrees/break-check -baseline
146215 run : |
147- pnpm dlx --package "$SNAPI_PACKAGE" snapi snapshot \
216+ pnpm dlx --package "$BREAK_CHECK_PACKAGE" break-check snapshot \
148217 --output "$GITHUB_WORKSPACE/.api-snapshots-baseline"
149218
150219 - name : Detect API changes
151220 env :
152- SNAPI_ANTHROPIC_API_KEY : ${{ secrets.SNAPI_ANTHROPIC_API_KEY }}
221+ BREAK_CHECK_ANTHROPIC_API_KEY : ${{ secrets.BREAK_CHECK_ANTHROPIC_API_KEY }}
153222 run : |
154- pnpm dlx --package "$SNAPI_PACKAGE" snapi detect \
223+ pnpm dlx --package "$BREAK_CHECK_PACKAGE" break-check detect \
155224 --baseline .api-snapshots-baseline \
156225 --output api-changes-report.md \
157226 --fail-on-breaking
@@ -165,38 +234,67 @@ jobs:
165234 if-no-files-found : ignore
166235 retention-days : 5
167236
168- - name : Build snapi comment body
237+ - name : Post break-check report
169238 if : always()
170- id : report
171- run : |
172- if [ ! -f api-changes-report.md ]; then
173- exit 0
174- fi
175- {
176- echo 'body<<SNAPI_REPORT_EOF'
177- echo '<!-- snapi-report -->'
178- if grep -q '## No API Changes Detected' api-changes-report.md; then
179- echo '**Snapi**: no API changes detected in `@clerk/backend`, `@clerk/clerk-js`, `@clerk/nextjs`, `@clerk/react`, `@clerk/shared`, `@clerk/ui`.'
180- else
181- cat api-changes-report.md
182- fi
183- echo 'SNAPI_REPORT_EOF'
184- } >> "$GITHUB_OUTPUT"
185-
186- - name : Find existing snapi comment
187- if : always() && steps.report.outputs.body != ''
188- id : find-comment
189- uses : peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
239+ uses : actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
240+ env :
241+ RUN_URL : ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
190242 with :
191- issue-number : ${{ github.event.pull_request.number }}
192- comment-author : ' github-actions[bot]'
193- body-includes : ' <!-- snapi-report -->'
243+ script : |
244+ const fs = require('fs');
245+ const reportPath = 'api-changes-report.md';
246+ if (!fs.existsSync(reportPath)) {
247+ core.info('No break-check report found; skipping comment.');
248+ return;
249+ }
194250
195- - name : Post snapi report
196- if : always() && steps.report.outputs.body != ''
197- uses : peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
198- with :
199- comment-id : ${{ steps.find-comment.outputs.comment-id }}
200- issue-number : ${{ github.event.pull_request.number }}
201- body : ${{ steps.report.outputs.body }}
202- edit-mode : replace
251+ const marker = '<!-- break-check-report -->';
252+ const report = fs.readFileSync(reportPath, 'utf-8');
253+
254+ let body;
255+ if (report.includes('## No API Changes Detected')) {
256+ body = `${marker}\n**Break Check**: no API changes detected across the tracked packages.`;
257+ } else {
258+ // GitHub rejects comment bodies over 65536 chars. Read the report
259+ // from disk and post via the API so we never hit an arg-length
260+ // limit, and truncate with a pointer to the full artifact when a
261+ // genuinely large diff would overflow the comment.
262+ const LIMIT = 64000;
263+ const head = `${marker}\n`;
264+ if (head.length + report.length <= LIMIT) {
265+ body = head + report;
266+ } else {
267+ const notice =
268+ `\n\n> **Note**\n> Report truncated to fit GitHub's comment limit. ` +
269+ `The full report is attached as the \`api-changes-report\` artifact on ` +
270+ `[this run](${process.env.RUN_URL}).\n`;
271+ const budget = LIMIT - head.length - notice.length;
272+ body = head + report.slice(0, budget) + notice;
273+ }
274+ }
275+
276+ const comments = await github.paginate(github.rest.issues.listComments, {
277+ owner: context.repo.owner,
278+ repo: context.repo.repo,
279+ issue_number: context.issue.number,
280+ per_page: 100,
281+ });
282+ const existing = comments.find(
283+ (c) => c.user?.type === 'Bot' && c.body && c.body.includes(marker),
284+ );
285+
286+ if (existing) {
287+ await github.rest.issues.updateComment({
288+ owner: context.repo.owner,
289+ repo: context.repo.repo,
290+ comment_id: existing.id,
291+ body,
292+ });
293+ } else {
294+ await github.rest.issues.createComment({
295+ owner: context.repo.owner,
296+ repo: context.repo.repo,
297+ issue_number: context.issue.number,
298+ body,
299+ });
300+ }
0 commit comments