Files
recurring-checklist/mobile/scripts/stamp-version.js
T
Waters ChurchandClaude Opus 5 469db5bc60 Bring the checklist app under version control
The project has lived only as a folder on the Mac Studio and, briefly, as a
transfer tarball. This is the first commit: the Express server and its web
front end, the Capacitor iOS and Android wrappers, and the release and backup
scripts.

The repo is the source, not a backup of the machine. Left out on purpose, by
the .gitignore files that were already in the tree: node_modules, build output,
mobile/www (regenerated from server/public on every build), and server/data,
which holds the live checklists and belongs on the NAS rather than in git.

Every credential is excluded too — server/.env, the Play upload keystore and
its properties file, the App Store Connect key and ios-signing.config. The one
secret that was not in an ignored file was the NAS SSH password, hard-coded in
build-deploy.sh and grepped back out of it by backup-to-drive.sh. Both now read
it from the login keychain (service checklist-nas, account Christian), falling
back to NAS_PASS in the environment, so rotating it is one keychain entry
rather than a history rewrite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:00:57 -04:00

63 lines
2.9 KiB
JavaScript

#!/usr/bin/env node
'use strict';
// stamp-version.js — push release.config.json's numbers into the Xcode project.
//
// Android reads release.config.json directly from build.gradle, but Xcode keeps
// MARKETING_VERSION / CURRENT_PROJECT_VERSION inside project.pbxproj, so they have to be
// written in. Both stores then get the same marketing version from one edited file, which
// is the whole point: a build number that silently disagrees between platforms is how you
// end up with "1.4 on Android, 1.2 on iPhone" bug reports that nobody can reproduce.
//
// Run with --check to verify without writing (used by release.sh as a pre-flight).
const fs = require('fs');
const path = require('path');
const MOBILE = path.resolve(__dirname, '..');
const PBXPROJ = path.join(MOBILE, 'ios', 'App', 'App.xcodeproj', 'project.pbxproj');
const release = JSON.parse(fs.readFileSync(path.join(MOBILE, 'release.config.json'), 'utf8'));
const version = String(release.version || '').trim();
const iosBuild = release.iosBuild;
// Apple rejects a CFBundleShortVersionString that isn't 1-3 dot-separated integers, and it
// rejects it at upload time — after the whole archive has already been built and signed.
if (!/^\d+(\.\d+){0,2}$/.test(version)) {
console.error(`✗ version must be 1-3 dot-separated numbers (e.g. 1.2.3) — got ${JSON.stringify(release.version)}`);
process.exit(1);
}
if (!Number.isInteger(iosBuild) || iosBuild < 1) {
console.error(`✗ iosBuild must be a positive integer — got ${JSON.stringify(iosBuild)}`);
process.exit(1);
}
if (!Number.isInteger(release.androidVersionCode) || release.androidVersionCode < 1) {
console.error(`✗ androidVersionCode must be a positive integer — got ${JSON.stringify(release.androidVersionCode)}`);
process.exit(1);
}
let pbx = fs.readFileSync(PBXPROJ, 'utf8');
const before = pbx;
// Both the Debug and Release configurations carry these, hence the global replace: an
// archive built from the wrong configuration must not ship a stale build number.
pbx = pbx.replace(/MARKETING_VERSION = [^;]+;/g, `MARKETING_VERSION = ${version};`);
pbx = pbx.replace(/CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${iosBuild};`);
const marketingHits = (pbx.match(/MARKETING_VERSION = /g) || []).length;
const buildHits = (pbx.match(/CURRENT_PROJECT_VERSION = /g) || []).length;
if (marketingHits < 2 || buildHits < 2) {
console.error(`✗ expected MARKETING_VERSION and CURRENT_PROJECT_VERSION in both build configs (found ${marketingHits}/${buildHits})`);
process.exit(1);
}
if (process.argv.includes('--check')) {
console.log(pbx === before
? `✓ Xcode already at ${version} (${iosBuild})`
: `• Xcode would change to ${version} (${iosBuild})`);
process.exit(0);
}
if (pbx !== before) fs.writeFileSync(PBXPROJ, pbx);
console.log(`✓ Xcode stamped ${version} (build ${iosBuild})`);
console.log(`✓ Gradle reads ${version} (code ${release.androidVersionCode}) from release.config.json`);