#!/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`);