Skip to content

Commit cbbe37d

Browse files
authored
chore: Analyse Next.js releases for app router changes (#502)
* chore: Analyse Next.js releases for app router changes Having an email on every canary release can be too verbose, I'm only interested in being notified of releases that change the app router core code, which may impact `nuqs`. * chore: No need for ZX (just minimist) * chore: Notify on GA releases too * chore: Formatting
1 parent a95d587 commit cbbe37d

File tree

5 files changed

+322
-5
lines changed

5 files changed

+322
-5
lines changed

.github/workflows/test-against-nextjs-release.yml

+22
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,25 @@ jobs:
6262
steps:
6363
- name: Invalidate ISR cache for GitHub Actions status on landing page
6464
run: curl -s "https://nuqs.47ng.com/api/isr?tag=github-actions-status&token=${{ secrets.ISR_TOKEN }}"
65+
66+
analyse-release:
67+
runs-on: ubuntu-latest
68+
name: Check for app router changes
69+
steps:
70+
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
71+
- uses: pnpm/action-setup@d882d12c64e032187b2edb46d3a0d003b7a43598
72+
with:
73+
version: 8
74+
- uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8
75+
with:
76+
node-version: lts/*
77+
cache: pnpm
78+
- name: Install dependencies
79+
run: pnpm install
80+
- name: Check for changes in app router
81+
run: ./next-release-analyser.mjs --version ${{ inputs.version }}
82+
working-directory: packages/scripts
83+
env:
84+
MAILPACE_API_TOKEN: ${{ secrets.MAILPACE_API_TOKEN }}
85+
EMAIL_ADDRESS_TO: ${{ secrets.EMAIL_ADDRESS_TO }}
86+
EMAIL_ADDRESS_FROM: ${{ secrets.EMAIL_ADDRESS_FROM }}
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env node
2+
// @ts-check
3+
4+
import MailPace from '@mailpace/mailpace.js'
5+
import { createEnv } from '@t3-oss/env-core'
6+
import minimist from 'minimist'
7+
import { z } from 'zod'
8+
9+
const gaRegexp = /^\d+\.\d+\.\d+$/
10+
const canaryRegexp = /^(\d+)\.(\d+)\.(\d+)-canary\.(\d+)$/
11+
12+
const env = createEnv({
13+
server: {
14+
CI: z
15+
.string()
16+
.optional()
17+
.transform(v => v === 'true'),
18+
MAILPACE_API_TOKEN: z.string(),
19+
EMAIL_ADDRESS_FROM: z.string().email(),
20+
EMAIL_ADDRESS_TO: z.string().email()
21+
},
22+
isServer: true,
23+
runtimeEnv: process.env
24+
})
25+
26+
main()
27+
28+
const fileSchema = z.object({
29+
filename: z.string(),
30+
patch: z.string().optional()
31+
})
32+
33+
async function main() {
34+
const argv = minimist(process.argv.slice(2))
35+
const thisVersion = argv.version
36+
if (gaRegexp.test(thisVersion)) {
37+
await sendGAEmail(thisVersion)
38+
return
39+
}
40+
41+
const previousVersion = getPreviousVersion(argv.version)
42+
43+
if (!previousVersion) {
44+
console.log('No previous version to compare with')
45+
process.exit(0)
46+
}
47+
const compareURL = `https://api.github.com/repos/vercel/next.js/compare/v${previousVersion}...v${thisVersion}`
48+
const compare = await fetch(compareURL).then(res => res.json())
49+
const files = z.array(fileSchema).parse(compare.files)
50+
const appRouterFile = files.find(
51+
file =>
52+
file.filename === 'packages/next/src/client/components/app-router.tsx'
53+
)
54+
if (!appRouterFile) {
55+
console.log('No changes in app-router.tsx')
56+
process.exit(0)
57+
}
58+
await sendNotificationEmail(thisVersion, appRouterFile)
59+
}
60+
61+
// --
62+
63+
/**
64+
* @param {string} version
65+
*/
66+
function getPreviousVersion(version) {
67+
const match = canaryRegexp.exec(version)
68+
if (!match || !match[4]) {
69+
return null
70+
}
71+
const canary = parseInt(match[4])
72+
if (canary === 0) {
73+
return null
74+
}
75+
return `${match[1]}.${match[2]}.${match[3]}-canary.${canary - 1}`
76+
}
77+
78+
/**
79+
*
80+
* @param {string} thisVersion
81+
* @param {z.infer<typeof fileSchema>} appRouterFile
82+
* @returns
83+
*/
84+
async function sendNotificationEmail(thisVersion, appRouterFile) {
85+
const client = new MailPace.DomainClient(env.MAILPACE_API_TOKEN)
86+
const release = await fetch(
87+
`https://api.github.com/repos/vercel/next.js/releases/tags/v${thisVersion}`
88+
).then(res => res.json())
89+
const subject = `[nuqs] Next.js ${thisVersion} has app router changes`
90+
const body = `Release: ${release.html_url}
91+
92+
${release.body}
93+
---
94+
95+
${
96+
appRouterFile.patch
97+
? `App router changes:
98+
\`\`\`diff
99+
${appRouterFile.patch}
100+
\`\`\``
101+
: 'No patch available'
102+
}
103+
`
104+
console.info('Sending email:', subject)
105+
console.info(body)
106+
if (!env.CI) {
107+
return
108+
}
109+
return client.sendEmail({
110+
from: env.EMAIL_ADDRESS_FROM,
111+
to: env.EMAIL_ADDRESS_TO,
112+
subject,
113+
textbody: body,
114+
tags: ['nuqs']
115+
})
116+
}
117+
118+
/**
119+
* @param {string} thisVersion
120+
*/
121+
function sendGAEmail(thisVersion) {
122+
const client = new MailPace.DomainClient(env.MAILPACE_API_TOKEN)
123+
const subject = `[nuqs] Next.js ${thisVersion} was published to GA`
124+
const body = `https://github.com/vercel/next.js/releases/tag/v${thisVersion}`
125+
console.info('Sending email:', subject)
126+
console.info(body)
127+
if (!env.CI) {
128+
return
129+
}
130+
return client.sendEmail({
131+
from: env.EMAIL_ADDRESS_FROM,
132+
to: env.EMAIL_ADDRESS_TO,
133+
subject,
134+
textbody: body,
135+
tags: ['nuqs']
136+
})
137+
}

packages/scripts/package.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "scripts",
3+
"private": true,
4+
"version": "0.0.0-internal",
5+
"type": "module",
6+
"dependencies": {
7+
"@mailpace/mailpace.js": "^0.1.1",
8+
"@t3-oss/env-core": "^0.9.2",
9+
"minimist": "^1.2.8",
10+
"zod": "^3.22.4"
11+
},
12+
"devDependencies": {
13+
"@types/minimist": "^1.2.5",
14+
"@types/node": "^20.11.17",
15+
"typescript": "^5.3.3"
16+
}
17+
}

packages/scripts/tsconfig.json

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"$schema": "https://json.schemastore.org/tsconfig",
3+
"compilerOptions": {
4+
// Type checking
5+
"strict": true,
6+
"noUncheckedIndexedAccess": true,
7+
"alwaysStrict": false, // Don't emit "use strict" to avoid conflicts with "use client"
8+
// Modules
9+
"module": "ESNext",
10+
"moduleResolution": "Bundler",
11+
"resolveJsonModule": true,
12+
// Language & Environment
13+
"target": "ESNext",
14+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
15+
// Emit
16+
"noEmit": true,
17+
"declaration": true,
18+
"declarationMap": true,
19+
"verbatimModuleSyntax": true,
20+
"moduleDetection": "force",
21+
22+
"downlevelIteration": true,
23+
// Interop
24+
"allowJs": true,
25+
"isolatedModules": true,
26+
"esModuleInterop": true,
27+
"forceConsistentCasingInFileNames": true,
28+
// Misc
29+
"skipLibCheck": true,
30+
"skipDefaultLibCheck": true,
31+
"incremental": true,
32+
"tsBuildInfoFile": ".tsbuildinfo"
33+
},
34+
"include": ["next-release-analyser.mjs"],
35+
"exclude": ["node_modules"]
36+
}

0 commit comments

Comments
 (0)