Refuse to release until the fastlane changelog for this version code matches CHANGELOG.md

This commit is contained in:
Jon Staab 2026-09-24 15:45:24 -07:00
parent 4e607dd5a2
commit e78359746b
6 changed files with 58 additions and 7 deletions

View file

@ -197,7 +197,8 @@ rolling out on Play and submitting for review.
```sh ```sh
pnpm bump minor # or patch, major, or an explicit x.y.z pnpm bump minor # or patch, major, or an explicit x.y.z
# write the CHANGELOG.md section for the new version # 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 git tag 1.12.0 && git push origin dev 1.12.0
pnpm release:local pnpm release:local
``` ```

View file

@ -14,6 +14,7 @@
"build:server": "vite build --config vite.config.server.ts", "build:server": "vite build --config vite.config.server.ts",
"start": "node server.js", "start": "node server.js",
"release:check": "node scripts/release/local.mjs --check && node scripts/release/ci.mjs --check", "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:local": "node scripts/release/local.mjs",
"release:ci": "node scripts/release/ci.mjs", "release:ci": "node scripts/release/ci.mjs",
"bump": "node scripts/release/bump.mjs", "bump": "node scripts/release/bump.mjs",

View file

@ -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)}`)

View file

@ -11,6 +11,7 @@ config({path: join(root, ".env.local")})
export const {name, version} = JSON.parse(await readFile(join(root, "package.json"), "utf-8")) 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 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 zapstore = await readFile(join(root, "zapstore.yaml"), "utf-8")
const zapstoreField = key => { const zapstoreField = key => {
@ -35,6 +36,22 @@ export const notes = (nextHeading < 0 ? remainder : remainder.slice(0, nextHeadi
.join("\n") .join("\n")
.trim() .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) => { export const git = (...gitArgs) => {
try { try {
return output("git", gitArgs, {cwd: root, stdio: ["ignore", "pipe", "ignore"]}) return output("git", gitArgs, {cwd: root, stdio: ["ignore", "pipe", "ignore"]})

View file

@ -1,7 +1,17 @@
import {existsSync, readFileSync} from "node:fs" import {existsSync, readFileSync} from "node:fs"
import {join} from "node:path" import {join, relative} from "node:path"
import {parseArgs} from "node:util" 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" import {ask, bold, dim, fail, green, red, yellow} from "./shell.mjs"
export const release = async (command, steps) => { export const release = async (command, steps) => {
@ -46,6 +56,16 @@ export const release = async (command, steps) => {
if (!notes) { if (!notes) {
problems.push({missing: [`CHANGELOG.md has no "# ${version}" section`]}) 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}`)) { if (git("rev-parse", `refs/tags/${version}`)) {

View file

@ -4,7 +4,7 @@ import {readFile} from "node:fs/promises"
import {join, resolve} from "node:path" import {join, resolve} from "node:path"
import {spec} from "@welshman/lib" import {spec} from "@welshman/lib"
import {gradle, keystoreEnv} from "../lib/android.mjs" 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" import {play} from "../lib/play.mjs"
const aab = join(root, "android/app/build/outputs/bundle/release/app-release.aab") const aab = join(root, "android/app/build/outputs/bundle/release/app-release.aab")
@ -30,7 +30,6 @@ export default {
], ],
run: async () => { run: async () => {
const gradleConfig = await readFile(join(root, "android/app/build.gradle"), "utf-8") 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 track = process.env.PLAY_TRACK ?? "production"
const status = process.env.PLAY_STATUS ?? "draft" const status = process.env.PLAY_STATUS ?? "draft"
const api = await play({ const api = await play({
@ -68,8 +67,7 @@ export default {
versionCode, versionCode,
track, track,
status, status,
// Play rejects release notes over 500 characters notes: shortNotes,
notes: notes.slice(0, 500),
}) })
followUps.push( followUps.push(