From e78359746b764e4ffaf58c53a357d201d34721a7 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Thu, 24 Sep 2026 15:45:24 -0700 Subject: [PATCH] Refuse to release until the fastlane changelog for this version code matches CHANGELOG.md --- README.md | 3 ++- package.json | 1 + scripts/release/changelog.mjs | 14 ++++++++++++++ scripts/release/lib/context.mjs | 17 +++++++++++++++++ scripts/release/lib/pipeline.mjs | 24 ++++++++++++++++++++++-- scripts/release/steps/play.mjs | 6 ++---- 6 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 scripts/release/changelog.mjs diff --git a/README.md b/README.md index cf041325..783faa2f 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,8 @@ rolling out on Play and submitting for review. ```sh pnpm bump minor # or patch, major, or an explicit x.y.z # write the CHANGELOG.md section for the new version -git commit -am "Bump version" +pnpm release:changelog # copies it to fastlane/ for F-Droid, which reads it from the tag +git add -A && git commit -m "Bump version" git tag 1.12.0 && git push origin dev 1.12.0 pnpm release:local ``` diff --git a/package.json b/package.json index 552de0e0..191eb717 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "build:server": "vite build --config vite.config.server.ts", "start": "node server.js", "release:check": "node scripts/release/local.mjs --check && node scripts/release/ci.mjs --check", + "release:changelog": "node scripts/release/changelog.mjs", "release:local": "node scripts/release/local.mjs", "release:ci": "node scripts/release/ci.mjs", "bump": "node scripts/release/bump.mjs", diff --git a/scripts/release/changelog.mjs b/scripts/release/changelog.mjs new file mode 100644 index 00000000..bc47e0d6 --- /dev/null +++ b/scripts/release/changelog.mjs @@ -0,0 +1,14 @@ +#!/usr/bin/env node +import {mkdir, writeFile} from "node:fs/promises" +import {dirname, relative} from "node:path" +import {fastlaneChangelog, notes, root, shortNotes, version} from "./lib/context.mjs" +import {fail} from "./lib/shell.mjs" + +if (!notes) { + fail(`CHANGELOG.md has no "# ${version}" section`) +} + +await mkdir(dirname(fastlaneChangelog), {recursive: true}) +await writeFile(fastlaneChangelog, `${shortNotes}\n`) + +console.log(`Wrote ${relative(root, fastlaneChangelog)}`) diff --git a/scripts/release/lib/context.mjs b/scripts/release/lib/context.mjs index 22760faf..95304f34 100644 --- a/scripts/release/lib/context.mjs +++ b/scripts/release/lib/context.mjs @@ -11,6 +11,7 @@ config({path: join(root, ".env.local")}) export const {name, version} = JSON.parse(await readFile(join(root, "package.json"), "utf-8")) const changelog = await readFile(join(root, "CHANGELOG.md"), "utf-8") +const gradleConfig = await readFile(join(root, "android/app/build.gradle"), "utf-8") const zapstore = await readFile(join(root, "zapstore.yaml"), "utf-8") const zapstoreField = key => { @@ -35,6 +36,22 @@ export const notes = (nextHeading < 0 ? remainder : remainder.slice(0, nextHeadi .join("\n") .trim() +export const versionCode = Number(gradleConfig.match(/versionCode (\d+)/)[1]) + +// Play and F-Droid both cap release notes at 500 characters, so cut at the last whole line under it +const noteLines = notes.split("\n") + +while (noteLines.join("\n").length > 500) { + noteLines.pop() +} + +export const shortNotes = noteLines.join("\n") + +export const fastlaneChangelog = join( + root, + `fastlane/metadata/android/en-US/changelogs/${versionCode}.txt`, +) + export const git = (...gitArgs) => { try { return output("git", gitArgs, {cwd: root, stdio: ["ignore", "pipe", "ignore"]}) diff --git a/scripts/release/lib/pipeline.mjs b/scripts/release/lib/pipeline.mjs index 1375f428..61d68e8d 100644 --- a/scripts/release/lib/pipeline.mjs +++ b/scripts/release/lib/pipeline.mjs @@ -1,7 +1,17 @@ import {existsSync, readFileSync} from "node:fs" -import {join} from "node:path" +import {join, relative} from "node:path" import {parseArgs} from "node:util" -import {followUps, git, name, notes, repository, root, version} from "./context.mjs" +import { + fastlaneChangelog, + followUps, + git, + name, + notes, + repository, + root, + shortNotes, + version, +} from "./context.mjs" import {ask, bold, dim, fail, green, red, yellow} from "./shell.mjs" export const release = async (command, steps) => { @@ -46,6 +56,16 @@ export const release = async (command, steps) => { if (!notes) { problems.push({missing: [`CHANGELOG.md has no "# ${version}" section`]}) + } else if ( + !existsSync(fastlaneChangelog) || + readFileSync(fastlaneChangelog, "utf-8") !== `${shortNotes}\n` + ) { + // F-Droid reads it from the tag, so it has to be committed before tagging + problems.push({ + missing: [ + `${relative(root, fastlaneChangelog)} doesn't match CHANGELOG.md: pnpm release:changelog`, + ], + }) } if (git("rev-parse", `refs/tags/${version}`)) { diff --git a/scripts/release/steps/play.mjs b/scripts/release/steps/play.mjs index 25cc35a5..b594ecb3 100644 --- a/scripts/release/steps/play.mjs +++ b/scripts/release/steps/play.mjs @@ -4,7 +4,7 @@ import {readFile} from "node:fs/promises" import {join, resolve} from "node:path" import {spec} from "@welshman/lib" import {gradle, keystoreEnv} from "../lib/android.mjs" -import {followUps, missingEnv, notes, root, version} from "../lib/context.mjs" +import {followUps, missingEnv, root, shortNotes, version, versionCode} from "../lib/context.mjs" import {play} from "../lib/play.mjs" const aab = join(root, "android/app/build/outputs/bundle/release/app-release.aab") @@ -30,7 +30,6 @@ export default { ], run: async () => { const gradleConfig = await readFile(join(root, "android/app/build.gradle"), "utf-8") - const versionCode = Number(gradleConfig.match(/versionCode (\d+)/)[1]) const track = process.env.PLAY_TRACK ?? "production" const status = process.env.PLAY_STATUS ?? "draft" const api = await play({ @@ -68,8 +67,7 @@ export default { versionCode, track, status, - // Play rejects release notes over 500 characters - notes: notes.slice(0, 500), + notes: shortNotes, }) followUps.push(