mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 19:50:28 +00:00
feat(ios): pin calver release versioning (#63001)
* feat(ios): decouple app versioning from gateway * feat(ios): pin calver release versioning * refactor(ios): drop prerelease version helper fields * docs(changelog): note pinned ios release versioning (#63001) (thanks @ngutman)
This commit is contained in:
@@ -8,7 +8,7 @@ Usage:
|
||||
scripts/ios-beta-prepare.sh --build-number 7 [--team-id TEAMID]
|
||||
|
||||
Prepares local beta-release inputs without touching local signing overrides:
|
||||
- reads package.json.version and writes apps/ios/build/Version.xcconfig
|
||||
- reads apps/ios/version.json and writes apps/ios/build/Version.xcconfig
|
||||
- writes apps/ios/build/BetaRelease.xcconfig with canonical bundle IDs
|
||||
- configures the beta build for relay-backed APNs registration
|
||||
- regenerates apps/ios/OpenClaw.xcodeproj via xcodegen
|
||||
@@ -21,12 +21,14 @@ BUILD_DIR="${IOS_DIR}/build"
|
||||
BETA_XCCONFIG="${IOS_DIR}/build/BetaRelease.xcconfig"
|
||||
TEAM_HELPER="${ROOT_DIR}/scripts/ios-team-id.sh"
|
||||
VERSION_HELPER="${ROOT_DIR}/scripts/ios-write-version-xcconfig.sh"
|
||||
IOS_VERSION_HELPER="${ROOT_DIR}/scripts/ios-version.ts"
|
||||
VERSION_SYNC_HELPER="${ROOT_DIR}/scripts/ios-sync-versioning.ts"
|
||||
|
||||
BUILD_NUMBER=""
|
||||
TEAM_ID="${IOS_DEVELOPMENT_TEAM:-}"
|
||||
PUSH_RELAY_BASE_URL="${OPENCLAW_PUSH_RELAY_BASE_URL:-${IOS_PUSH_RELAY_BASE_URL:-}}"
|
||||
PUSH_RELAY_BASE_URL_XCCONFIG=""
|
||||
PACKAGE_VERSION="$(cd "${ROOT_DIR}" && node -p "require('./package.json').version" 2>/dev/null || true)"
|
||||
IOS_VERSION=""
|
||||
|
||||
prepare_build_dir() {
|
||||
if [[ -L "${BUILD_DIR}" ]]; then
|
||||
@@ -132,6 +134,16 @@ PUSH_RELAY_BASE_URL_XCCONFIG="$(
|
||||
|
||||
prepare_build_dir
|
||||
|
||||
(
|
||||
cd "${ROOT_DIR}" && node --import tsx "${VERSION_SYNC_HELPER}" --check
|
||||
)
|
||||
|
||||
IOS_VERSION="$(cd "${ROOT_DIR}" && node --import tsx "${IOS_VERSION_HELPER}" --field canonicalVersion)"
|
||||
if [[ -z "${IOS_VERSION}" ]]; then
|
||||
echo "Unable to resolve iOS version from ${ROOT_DIR}/apps/ios/version.json." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(
|
||||
bash "${VERSION_HELPER}" --build-number "${BUILD_NUMBER}"
|
||||
)
|
||||
@@ -161,5 +173,5 @@ EOF
|
||||
xcodegen generate
|
||||
)
|
||||
|
||||
echo "Prepared iOS beta release: version=${PACKAGE_VERSION} build=${BUILD_NUMBER} team=${TEAM_ID}"
|
||||
echo "Prepared iOS beta release: version=${IOS_VERSION} build=${BUILD_NUMBER} team=${TEAM_ID}"
|
||||
echo "XCODE_XCCONFIG_FILE=${BETA_XCCONFIG}"
|
||||
|
||||
148
scripts/ios-pin-version.ts
Normal file
148
scripts/ios-pin-version.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import path from "node:path";
|
||||
import {
|
||||
normalizePinnedIosVersion,
|
||||
resolveGatewayVersionForIosRelease,
|
||||
resolveIosVersion,
|
||||
syncIosVersioning,
|
||||
writeIosVersionManifest,
|
||||
} from "./lib/ios-version.ts";
|
||||
|
||||
type CliOptions = {
|
||||
explicitVersion: string | null;
|
||||
fromGateway: boolean;
|
||||
rootDir: string;
|
||||
sync: boolean;
|
||||
};
|
||||
|
||||
export type PinIosVersionResult = {
|
||||
previousVersion: string | null;
|
||||
nextVersion: string;
|
||||
packageVersion: string | null;
|
||||
versionFilePath: string;
|
||||
syncedPaths: string[];
|
||||
};
|
||||
|
||||
function usage(): string {
|
||||
return [
|
||||
"Usage: node --import tsx scripts/ios-pin-version.ts (--from-gateway | --version <YYYY.M.D>) [--no-sync] [--root dir]",
|
||||
"",
|
||||
"Examples:",
|
||||
" node --import tsx scripts/ios-pin-version.ts --from-gateway",
|
||||
" node --import tsx scripts/ios-pin-version.ts --version 2026.4.10",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function parseArgs(argv: string[]): CliOptions {
|
||||
let explicitVersion: string | null = null;
|
||||
let fromGateway = false;
|
||||
let rootDir = path.resolve(".");
|
||||
let sync = true;
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
switch (arg) {
|
||||
case "--from-gateway": {
|
||||
fromGateway = true;
|
||||
break;
|
||||
}
|
||||
case "--version": {
|
||||
explicitVersion = argv[index + 1] ?? null;
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "--no-sync": {
|
||||
sync = false;
|
||||
break;
|
||||
}
|
||||
case "--root": {
|
||||
const value = argv[index + 1];
|
||||
if (!value) {
|
||||
throw new Error("Missing value for --root.");
|
||||
}
|
||||
rootDir = path.resolve(value);
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "-h":
|
||||
case "--help": {
|
||||
console.log(`${usage()}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fromGateway === (explicitVersion !== null)) {
|
||||
throw new Error("Choose exactly one of --from-gateway or --version <YYYY.M.D>.");
|
||||
}
|
||||
|
||||
if (explicitVersion !== null && !explicitVersion.trim()) {
|
||||
throw new Error("Missing value for --version.");
|
||||
}
|
||||
|
||||
return { explicitVersion, fromGateway, rootDir, sync };
|
||||
}
|
||||
|
||||
export function pinIosVersion(params: CliOptions): PinIosVersionResult {
|
||||
const rootDir = path.resolve(params.rootDir);
|
||||
let previousVersion: string | null = null;
|
||||
try {
|
||||
previousVersion = resolveIosVersion(rootDir).canonicalVersion;
|
||||
} catch {
|
||||
previousVersion = null;
|
||||
}
|
||||
|
||||
const gatewayVersion = params.fromGateway ? resolveGatewayVersionForIosRelease(rootDir) : null;
|
||||
const packageVersion = gatewayVersion?.packageVersion ?? null;
|
||||
const nextVersion =
|
||||
gatewayVersion?.pinnedIosVersion ?? normalizePinnedIosVersion(params.explicitVersion ?? "");
|
||||
const versionFilePath = writeIosVersionManifest(nextVersion, rootDir);
|
||||
const syncedPaths = params.sync ? syncIosVersioning({ mode: "write", rootDir }).updatedPaths : [];
|
||||
|
||||
return {
|
||||
previousVersion,
|
||||
nextVersion,
|
||||
packageVersion,
|
||||
versionFilePath,
|
||||
syncedPaths,
|
||||
};
|
||||
}
|
||||
|
||||
export async function main(argv: string[]): Promise<number> {
|
||||
try {
|
||||
const options = parseArgs(argv);
|
||||
const result = pinIosVersion(options);
|
||||
const sourceText = result.packageVersion
|
||||
? ` from gateway version ${result.packageVersion}`
|
||||
: "";
|
||||
process.stdout.write(`Pinned iOS version to ${result.nextVersion}${sourceText}.\n`);
|
||||
if (result.previousVersion && result.previousVersion !== result.nextVersion) {
|
||||
process.stdout.write(`Previous pinned iOS version: ${result.previousVersion}.\n`);
|
||||
}
|
||||
process.stdout.write(
|
||||
`Updated version manifest: ${path.relative(process.cwd(), result.versionFilePath)}\n`,
|
||||
);
|
||||
if (options.sync) {
|
||||
if (result.syncedPaths.length === 0) {
|
||||
process.stdout.write("iOS versioning artifacts already up to date.\n");
|
||||
} else {
|
||||
process.stdout.write(
|
||||
`Updated iOS versioning artifacts:\n- ${result.syncedPaths.map((filePath) => path.relative(process.cwd(), filePath)).join("\n- ")}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const exitCode = await main(process.argv.slice(2));
|
||||
if (exitCode !== 0) {
|
||||
process.exit(exitCode);
|
||||
}
|
||||
}
|
||||
57
scripts/ios-sync-versioning.ts
Normal file
57
scripts/ios-sync-versioning.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import path from "node:path";
|
||||
import { syncIosVersioning } from "./lib/ios-version.ts";
|
||||
|
||||
type Mode = "check" | "write";
|
||||
|
||||
export function parseArgs(argv: string[]): { mode: Mode; rootDir: string } {
|
||||
let mode: Mode = "write";
|
||||
let rootDir = path.resolve(".");
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
switch (arg) {
|
||||
case "--check": {
|
||||
mode = "check";
|
||||
break;
|
||||
}
|
||||
case "--write": {
|
||||
mode = "write";
|
||||
break;
|
||||
}
|
||||
case "--root": {
|
||||
const value = argv[index + 1];
|
||||
if (!value) {
|
||||
throw new Error("Missing value for --root.");
|
||||
}
|
||||
rootDir = path.resolve(value);
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "-h":
|
||||
case "--help": {
|
||||
console.log(
|
||||
"Usage: node --import tsx scripts/ios-sync-versioning.ts [--write|--check] [--root dir]",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { mode, rootDir };
|
||||
}
|
||||
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const result = syncIosVersioning({ mode: options.mode, rootDir: options.rootDir });
|
||||
|
||||
if (options.mode === "check") {
|
||||
process.stdout.write("iOS versioning artifacts are up to date.\n");
|
||||
} else if (result.updatedPaths.length === 0) {
|
||||
process.stdout.write("iOS versioning artifacts already up to date.\n");
|
||||
} else {
|
||||
process.stdout.write(
|
||||
`Updated iOS versioning artifacts:\n- ${result.updatedPaths.map((filePath) => path.relative(process.cwd(), filePath)).join("\n- ")}\n`,
|
||||
);
|
||||
}
|
||||
78
scripts/ios-version.ts
Normal file
78
scripts/ios-version.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import path from "node:path";
|
||||
import { resolveIosVersion } from "./lib/ios-version.ts";
|
||||
|
||||
type CliOptions = {
|
||||
field: string | null;
|
||||
format: "json" | "shell";
|
||||
rootDir: string;
|
||||
};
|
||||
|
||||
function parseArgs(argv: string[]): CliOptions {
|
||||
let field: string | null = null;
|
||||
let format: "json" | "shell" = "json";
|
||||
let rootDir = path.resolve(".");
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
switch (arg) {
|
||||
case "--field": {
|
||||
field = argv[index + 1] ?? null;
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "--json": {
|
||||
format = "json";
|
||||
break;
|
||||
}
|
||||
case "--shell": {
|
||||
format = "shell";
|
||||
break;
|
||||
}
|
||||
case "--root": {
|
||||
const value = argv[index + 1];
|
||||
if (!value) {
|
||||
throw new Error("Missing value for --root.");
|
||||
}
|
||||
rootDir = path.resolve(value);
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "-h":
|
||||
case "--help": {
|
||||
console.log(
|
||||
`Usage: node --import tsx scripts/ios-version.ts [--json|--shell] [--field name] [--root dir]\n`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { field, format, rootDir };
|
||||
}
|
||||
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const version = resolveIosVersion(options.rootDir);
|
||||
|
||||
if (options.field) {
|
||||
const value = version[options.field as keyof typeof version];
|
||||
if (value === undefined) {
|
||||
throw new Error(`Unknown iOS version field '${options.field}'.`);
|
||||
}
|
||||
process.stdout.write(`${String(value)}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (options.format === "shell") {
|
||||
process.stdout.write(
|
||||
[
|
||||
`OPENCLAW_IOS_VERSION=${version.canonicalVersion}`,
|
||||
`OPENCLAW_MARKETING_VERSION=${version.marketingVersion}`,
|
||||
`OPENCLAW_BUILD_VERSION=${version.buildVersion}`,
|
||||
].join("\n") + "\n",
|
||||
);
|
||||
} else {
|
||||
process.stdout.write(`${JSON.stringify(version, null, 2)}\n`);
|
||||
}
|
||||
@@ -6,8 +6,8 @@ usage() {
|
||||
Usage:
|
||||
scripts/ios-write-version-xcconfig.sh [--build-number 7]
|
||||
|
||||
Writes apps/ios/build/Version.xcconfig from root package.json.version:
|
||||
- OPENCLAW_GATEWAY_VERSION = exact package.json version
|
||||
Writes apps/ios/build/Version.xcconfig from apps/ios/version.json:
|
||||
- OPENCLAW_IOS_VERSION = exact canonical iOS version
|
||||
- OPENCLAW_MARKETING_VERSION = short iOS/App Store version
|
||||
- OPENCLAW_BUILD_VERSION = explicit build number or local numeric fallback
|
||||
EOF
|
||||
@@ -17,7 +17,9 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
IOS_DIR="${ROOT_DIR}/apps/ios"
|
||||
BUILD_DIR="${IOS_DIR}/build"
|
||||
VERSION_XCCONFIG="${IOS_DIR}/build/Version.xcconfig"
|
||||
PACKAGE_VERSION="$(cd "${ROOT_DIR}" && node -p "require('./package.json').version" 2>/dev/null || true)"
|
||||
VERSION_HELPER="${ROOT_DIR}/scripts/ios-version.ts"
|
||||
IOS_VERSION=""
|
||||
MARKETING_VERSION=""
|
||||
BUILD_NUMBER=""
|
||||
|
||||
prepare_build_dir() {
|
||||
@@ -64,16 +66,19 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
PACKAGE_VERSION="$(printf '%s' "${PACKAGE_VERSION}" | tr -d '\n' | xargs)"
|
||||
if [[ -z "${PACKAGE_VERSION}" ]]; then
|
||||
echo "Unable to read package.json.version from ${ROOT_DIR}/package.json." >&2
|
||||
exit 1
|
||||
fi
|
||||
while IFS='=' read -r key value; do
|
||||
case "${key}" in
|
||||
OPENCLAW_IOS_VERSION)
|
||||
IOS_VERSION="${value}"
|
||||
;;
|
||||
OPENCLAW_MARKETING_VERSION)
|
||||
MARKETING_VERSION="${value}"
|
||||
;;
|
||||
esac
|
||||
done < <(cd "${ROOT_DIR}" && node --import tsx "${VERSION_HELPER}" --shell)
|
||||
|
||||
if [[ "${PACKAGE_VERSION}" =~ ^([0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2})([.-]?beta[.-][0-9]+)?$ ]]; then
|
||||
MARKETING_VERSION="${BASH_REMATCH[1]}"
|
||||
else
|
||||
echo "Unsupported package.json.version '${PACKAGE_VERSION}'. Expected 2026.3.13 or 2026.3.13-beta.1." >&2
|
||||
if [[ -z "${IOS_VERSION}" || -z "${MARKETING_VERSION}" ]]; then
|
||||
echo "Unable to resolve iOS version metadata from ${ROOT_DIR}/apps/ios/version.json." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -91,9 +96,9 @@ prepare_build_dir
|
||||
write_generated_file "${VERSION_XCCONFIG}" <<EOF
|
||||
// Auto-generated by scripts/ios-write-version-xcconfig.sh.
|
||||
// Local version override; do not commit.
|
||||
OPENCLAW_GATEWAY_VERSION = ${PACKAGE_VERSION}
|
||||
OPENCLAW_IOS_VERSION = ${IOS_VERSION}
|
||||
OPENCLAW_MARKETING_VERSION = ${MARKETING_VERSION}
|
||||
OPENCLAW_BUILD_VERSION = ${BUILD_NUMBER}
|
||||
EOF
|
||||
|
||||
echo "Prepared iOS version settings: gateway=${PACKAGE_VERSION} marketing=${MARKETING_VERSION} build=${BUILD_NUMBER}"
|
||||
echo "Prepared iOS version settings: ios=${IOS_VERSION} marketing=${MARKETING_VERSION} build=${BUILD_NUMBER}"
|
||||
|
||||
218
scripts/lib/ios-version.ts
Normal file
218
scripts/lib/ios-version.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export const IOS_VERSION_FILE = "apps/ios/version.json";
|
||||
export const IOS_CHANGELOG_FILE = "apps/ios/CHANGELOG.md";
|
||||
export const IOS_VERSION_XCCONFIG_FILE = "apps/ios/Config/Version.xcconfig";
|
||||
export const IOS_RELEASE_NOTES_FILE = "apps/ios/fastlane/metadata/en-US/release_notes.txt";
|
||||
|
||||
const PINNED_IOS_VERSION_PATTERN = /^(\d{4}\.\d{1,2}\.\d{1,2})$/u;
|
||||
const GATEWAY_VERSION_PATTERN = /^(\d{4}\.\d{1,2}\.\d{1,2})(?:-(?:beta\.\d+|\d+))?$/u;
|
||||
|
||||
export type IosVersionManifest = {
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type ResolvedIosVersion = {
|
||||
canonicalVersion: string;
|
||||
marketingVersion: string;
|
||||
buildVersion: string;
|
||||
versionFilePath: string;
|
||||
changelogPath: string;
|
||||
versionXcconfigPath: string;
|
||||
releaseNotesPath: string;
|
||||
};
|
||||
|
||||
export type SyncIosVersioningMode = "check" | "write";
|
||||
|
||||
function normalizeTrailingNewline(value: string): string {
|
||||
return value.endsWith("\n") ? value : `${value}\n`;
|
||||
}
|
||||
|
||||
export function normalizePinnedIosVersion(rawVersion: string): string {
|
||||
const trimmed = rawVersion.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error(`Missing iOS version in ${IOS_VERSION_FILE}.`);
|
||||
}
|
||||
|
||||
const match = PINNED_IOS_VERSION_PATTERN.exec(trimmed);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid iOS version '${rawVersion}'. Expected pinned CalVer like 2026.4.6.`);
|
||||
}
|
||||
|
||||
return match[1] ?? trimmed;
|
||||
}
|
||||
|
||||
export function normalizeGatewayVersionToPinnedIosVersion(rawVersion: string): string {
|
||||
const trimmed = rawVersion.trim().replace(/^v/u, "");
|
||||
if (!trimmed) {
|
||||
throw new Error("Missing root package.json version.");
|
||||
}
|
||||
|
||||
const match = GATEWAY_VERSION_PATTERN.exec(trimmed);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Invalid gateway version '${rawVersion}'. Expected YYYY.M.D, YYYY.M.D-beta.N, or YYYY.M.D-N.`,
|
||||
);
|
||||
}
|
||||
|
||||
return match[1] ?? trimmed;
|
||||
}
|
||||
|
||||
export function readRootPackageVersion(rootDir = path.resolve(".")): string {
|
||||
const packageJsonPath = path.join(rootDir, "package.json");
|
||||
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { version?: unknown };
|
||||
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
||||
if (!version) {
|
||||
throw new Error(`Missing package.json version in ${packageJsonPath}.`);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
export function resolveGatewayVersionForIosRelease(rootDir = path.resolve(".")): {
|
||||
packageVersion: string;
|
||||
pinnedIosVersion: string;
|
||||
} {
|
||||
const packageVersion = readRootPackageVersion(rootDir);
|
||||
return {
|
||||
packageVersion,
|
||||
pinnedIosVersion: normalizeGatewayVersionToPinnedIosVersion(packageVersion),
|
||||
};
|
||||
}
|
||||
|
||||
export function readIosVersionManifest(rootDir = path.resolve(".")): IosVersionManifest {
|
||||
const versionFilePath = path.join(rootDir, IOS_VERSION_FILE);
|
||||
return JSON.parse(readFileSync(versionFilePath, "utf8")) as IosVersionManifest;
|
||||
}
|
||||
|
||||
export function writeIosVersionManifest(version: string, rootDir = path.resolve(".")): string {
|
||||
const versionFilePath = path.join(rootDir, IOS_VERSION_FILE);
|
||||
const normalizedVersion = normalizePinnedIosVersion(version);
|
||||
const nextContent = `${JSON.stringify({ version: normalizedVersion }, null, 2)}\n`;
|
||||
writeFileSync(versionFilePath, nextContent, "utf8");
|
||||
return versionFilePath;
|
||||
}
|
||||
|
||||
export function resolveIosVersion(rootDir = path.resolve(".")): ResolvedIosVersion {
|
||||
const versionFilePath = path.join(rootDir, IOS_VERSION_FILE);
|
||||
const changelogPath = path.join(rootDir, IOS_CHANGELOG_FILE);
|
||||
const versionXcconfigPath = path.join(rootDir, IOS_VERSION_XCCONFIG_FILE);
|
||||
const releaseNotesPath = path.join(rootDir, IOS_RELEASE_NOTES_FILE);
|
||||
const manifest = readIosVersionManifest(rootDir);
|
||||
const canonicalVersion = normalizePinnedIosVersion(manifest.version ?? "");
|
||||
|
||||
return {
|
||||
canonicalVersion,
|
||||
marketingVersion: canonicalVersion,
|
||||
buildVersion: "1",
|
||||
versionFilePath,
|
||||
changelogPath,
|
||||
versionXcconfigPath,
|
||||
releaseNotesPath,
|
||||
};
|
||||
}
|
||||
|
||||
export function renderIosVersionXcconfig(version: ResolvedIosVersion): string {
|
||||
return `// Shared iOS version defaults.\n// Source of truth: apps/ios/version.json\n// Generated by scripts/ios-sync-versioning.ts.\n\nOPENCLAW_IOS_VERSION = ${version.canonicalVersion}\nOPENCLAW_MARKETING_VERSION = ${version.marketingVersion}\nOPENCLAW_BUILD_VERSION = ${version.buildVersion}\n\n#include? "../build/Version.xcconfig"\n`;
|
||||
}
|
||||
|
||||
function matchChangelogHeading(line: string, heading: string): boolean {
|
||||
const normalized = line.trim();
|
||||
return normalized === `## ${heading}` || normalized.startsWith(`## ${heading} - `);
|
||||
}
|
||||
|
||||
export function extractChangelogSection(content: string, heading: string): string | null {
|
||||
const lines = content.split(/\r?\n/);
|
||||
const startIndex = lines.findIndex((line) => matchChangelogHeading(line, heading));
|
||||
if (startIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let endIndex = lines.length;
|
||||
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
||||
if (lines[index]?.startsWith("## ")) {
|
||||
endIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const body = lines
|
||||
.slice(startIndex + 1, endIndex)
|
||||
.join("\n")
|
||||
.trim();
|
||||
return body || null;
|
||||
}
|
||||
|
||||
export function renderIosReleaseNotes(
|
||||
version: ResolvedIosVersion,
|
||||
changelogContent: string,
|
||||
): string {
|
||||
const candidateHeadings = [version.canonicalVersion, "Unreleased"];
|
||||
|
||||
for (const heading of candidateHeadings) {
|
||||
const body = extractChangelogSection(changelogContent, heading);
|
||||
if (body) {
|
||||
return `${body}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unable to find iOS changelog notes for ${version.canonicalVersion}. Add a matching section to ${IOS_CHANGELOG_FILE}.`,
|
||||
);
|
||||
}
|
||||
|
||||
function syncFile(params: {
|
||||
mode: SyncIosVersioningMode;
|
||||
path: string;
|
||||
nextContent: string;
|
||||
label: string;
|
||||
}): boolean {
|
||||
const nextContent = normalizeTrailingNewline(params.nextContent);
|
||||
const currentContent = readFileSync(params.path, "utf8");
|
||||
if (currentContent === nextContent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (params.mode === "check") {
|
||||
throw new Error(`${params.label} is stale: ${path.relative(process.cwd(), params.path)}`);
|
||||
}
|
||||
|
||||
writeFileSync(params.path, nextContent, "utf8");
|
||||
return true;
|
||||
}
|
||||
|
||||
export function syncIosVersioning(params?: { mode?: SyncIosVersioningMode; rootDir?: string }): {
|
||||
updatedPaths: string[];
|
||||
} {
|
||||
const mode = params?.mode ?? "write";
|
||||
const rootDir = path.resolve(params?.rootDir ?? ".");
|
||||
const version = resolveIosVersion(rootDir);
|
||||
const changelogContent = readFileSync(version.changelogPath, "utf8");
|
||||
const nextVersionXcconfig = renderIosVersionXcconfig(version);
|
||||
const nextReleaseNotes = renderIosReleaseNotes(version, changelogContent);
|
||||
const updatedPaths: string[] = [];
|
||||
|
||||
if (
|
||||
syncFile({
|
||||
mode,
|
||||
path: version.versionXcconfigPath,
|
||||
nextContent: nextVersionXcconfig,
|
||||
label: "iOS version xcconfig",
|
||||
})
|
||||
) {
|
||||
updatedPaths.push(version.versionXcconfigPath);
|
||||
}
|
||||
|
||||
if (
|
||||
syncFile({
|
||||
mode,
|
||||
path: version.releaseNotesPath,
|
||||
nextContent: nextReleaseNotes,
|
||||
label: "iOS release notes",
|
||||
})
|
||||
) {
|
||||
updatedPaths.push(version.releaseNotesPath);
|
||||
}
|
||||
|
||||
return { updatedPaths };
|
||||
}
|
||||
Reference in New Issue
Block a user