chore(ios): generate release artifacts locally

This commit is contained in:
joshavant
2026-07-01 21:45:32 -05:00
parent 4a2a97777e
commit 8e95e56e2d
26 changed files with 248 additions and 331 deletions

View File

@@ -35,8 +35,6 @@ export const RELEASE_METADATA_PATHS = new Set([
"apps/android/fastlane/metadata/android/en-US/release_notes.txt",
"apps/android/version.json",
"apps/ios/CHANGELOG.md",
"apps/ios/Config/Version.xcconfig",
"apps/ios/fastlane/metadata/en-US/release_notes.txt",
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
"docs/.generated/config-baseline.sha256",
"docs/install/updating.md",

View File

@@ -8,7 +8,6 @@ import { RELEASE_METADATA_PATHS } from "./changed-lanes.mjs";
const VERSION_ONLY_TEXT_PATHS = new Set([
"apps/android/Config/Version.properties",
"apps/android/version.json",
"apps/ios/Config/Version.xcconfig",
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
]);

View File

@@ -150,6 +150,7 @@ fi
(
bash "${VERSION_HELPER}" --version "${IOS_VERSION}" --build-number "${BUILD_NUMBER}"
)
node "${ROOT_DIR}/scripts/ios-write-swift-filelist.mjs"
write_generated_file "${RELEASE_XCCONFIG}" <<EOF
// Auto-generated by scripts/ios-release-prepare.sh.

View File

@@ -110,6 +110,7 @@ fi
"${ROOT_DIR}/scripts/ios-configure-signing.sh"
"${ROOT_DIR}/scripts/ios-write-version-xcconfig.sh"
node "${ROOT_DIR}/scripts/ios-write-swift-filelist.mjs"
cd "${IOS_DIR}"
"${XCODEGEN_BIN}" generate

View File

@@ -7,7 +7,7 @@ export { parseVersionSyncArgs as parseArgs } from "./lib/version-script-args.ts"
function printUsage(): void {
process.stdout.write(
"Usage: node --import tsx scripts/ios-sync-versioning.ts [--write|--check] [--version YYYY.M.D] [--root dir]\n",
"Usage: node --import tsx scripts/ios-sync-versioning.ts [--write|--check] [--version YYYY.M.D] [--root dir]\n\nValidates that iOS versioning inputs can produce generated local artifacts.\n",
);
}
@@ -25,9 +25,11 @@ function main(argv = process.argv.slice(2)): number {
});
if (options.mode === "check") {
process.stdout.write("iOS versioning artifacts are up to date.\n");
process.stdout.write("iOS versioning inputs are valid.\n");
} else if (result.updatedPaths.length === 0) {
process.stdout.write("iOS versioning artifacts already up to date.\n");
process.stdout.write(
"iOS versioning inputs are valid; local artifacts are generated by iOS prep commands.\n",
);
} else {
process.stdout.write(
`Updated iOS versioning artifacts:\n- ${result.updatedPaths.map((filePath) => path.relative(process.cwd(), filePath)).join("\n- ")}\n`,

View File

@@ -1,5 +1,5 @@
// Ios Version script supports OpenClaw repository automation.
import { resolveIosVersion } from "./lib/ios-version.ts";
import { renderIosReleaseNotesForVersion, resolveIosVersion } from "./lib/ios-version.ts";
import { parseVersionQueryArgs } from "./lib/version-script-args.ts";
function printUsage(): void {
@@ -18,6 +18,16 @@ function main(argv = process.argv.slice(2)): number {
const version = resolveIosVersion(options.rootDir, { releaseVersion: options.releaseVersion });
if (options.field) {
if (options.field === "releaseNotes") {
process.stdout.write(
renderIosReleaseNotesForVersion({
releaseVersion: options.releaseVersion,
rootDir: options.rootDir,
}),
);
return 0;
}
const value = version[options.field as keyof typeof version];
if (value === undefined) {
throw new Error(`Unknown iOS version field '${options.field}'.`);

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env node
import { existsSync, lstatSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import path from "node:path";
const repoRoot = path.resolve(import.meta.dirname, "..");
const iosRoot = path.join(repoRoot, "apps", "ios");
const outputPath = path.join(iosRoot, "SwiftSources.input.xcfilelist");
const iosSourceRoots = [
"Sources",
"ShareExtension",
"ActivityWidget",
path.join("WatchApp", "Sources"),
];
const sharedSwiftFiles = [
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatPayloadDecoding.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatSheets.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatTheme.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatTransport.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+SessionKeys.swift",
"../shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/AnyCodable.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/BonjourEscapes.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/BonjourTypes.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/BridgeFrames.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/CameraCommands.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UIAction.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UICommands.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UIJSONL.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/CanvasCommandParams.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/CanvasCommands.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/Capabilities.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/JPEGTranscoder.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/NodeError.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/OpenClawKitResources.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/ScreenCommands.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/StoragePaths.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/SystemCommands.swift",
"../shared/OpenClawKit/Sources/OpenClawKit/TalkDirective.swift",
"../swabble/Sources/SwabbleKit/WakeWordGate.swift",
];
function normalizeFileListPath(filePath) {
return filePath.split(path.sep).join("/");
}
function collectSwiftFiles(rootRelativePath) {
const root = path.join(iosRoot, rootRelativePath);
if (!existsSync(root)) {
throw new Error(`Missing iOS Swift source root: ${rootRelativePath}`);
}
const entries = [];
const visit = (dir) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
visit(fullPath);
} else if (entry.isFile() && entry.name.endsWith(".swift")) {
entries.push(normalizeFileListPath(path.relative(iosRoot, fullPath)));
}
}
};
visit(root);
return entries;
}
function assertSharedFilesExist(filePaths) {
for (const filePath of filePaths) {
const absolutePath = path.resolve(iosRoot, filePath);
if (!existsSync(absolutePath)) {
throw new Error(`Missing shared Swift file listed for iOS lint: ${filePath}`);
}
}
}
function writeGeneratedFile(filePath, contents) {
if (existsSync(filePath) && lstatSync(filePath).isSymbolicLink()) {
throw new Error(`Refusing to overwrite symlinked file: ${filePath}`);
}
mkdirSync(path.dirname(filePath), { recursive: true });
writeFileSync(filePath, contents, "utf8");
}
assertSharedFilesExist(sharedSwiftFiles);
const iosFiles = iosSourceRoots.flatMap(collectSwiftFiles);
const fileList = [...new Set([...iosFiles, ...sharedSwiftFiles])].toSorted((left, right) =>
left.localeCompare(right),
);
writeGeneratedFile(outputPath, `${fileList.join("\n")}\n`);
process.stdout.write(`Prepared iOS Swift file list: ${path.relative(repoRoot, outputPath)}\n`);

View File

@@ -1,29 +1,21 @@
// Ios Version script supports OpenClaw repository automation.
import { readFileSync, writeFileSync } from "node:fs";
import { readFileSync } from "node:fs";
import path from "node:path";
import { parseReleaseVersion } from "./npm-publish-plan.mjs";
const IOS_CHANGELOG_FILE = "apps/ios/CHANGELOG.md";
const IOS_VERSION_XCCONFIG_FILE = "apps/ios/Config/Version.xcconfig";
const IOS_RELEASE_NOTES_FILE = "apps/ios/fastlane/metadata/en-US/release_notes.txt";
type ResolvedIosVersion = {
canonicalVersion: string;
marketingVersion: string;
buildVersion: string;
changelogPath: string;
versionXcconfigPath: string;
releaseNotesPath: string;
versionSource: "explicit" | "package";
versionSourcePath: string | null;
};
type SyncIosVersioningMode = "check" | "write";
function normalizeTrailingNewline(value: string): string {
return value.endsWith("\n") ? value : `${value}\n`;
}
function parsePinnedReleaseVersion(rawVersion: string): string | null {
const parsed = parseReleaseVersion(rawVersion.trim());
if (!parsed || parsed.version !== parsed.baseVersion) {
@@ -92,8 +84,6 @@ export function resolveIosVersion(
options?: { releaseVersion?: string | null },
): ResolvedIosVersion {
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 explicitReleaseVersion = options?.releaseVersion?.trim() ?? "";
const canonicalVersion = explicitReleaseVersion
? normalizePinnedIosVersion(explicitReleaseVersion)
@@ -104,17 +94,11 @@ export function resolveIosVersion(
marketingVersion: canonicalVersion,
buildVersion: "1",
changelogPath,
versionXcconfigPath,
releaseNotesPath,
versionSource: explicitReleaseVersion ? "explicit" : "package",
versionSourcePath: explicitReleaseVersion ? null : rootPackageJsonPath(rootDir),
};
}
export function renderIosVersionXcconfig(version: ResolvedIosVersion): string {
return `// Shared iOS version defaults.\n// Source of truth: package.json or explicit release --version.\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} - `);
@@ -160,26 +144,6 @@ export function renderIosReleaseNotes(
);
}
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;
releaseVersion?: string | null;
@@ -187,36 +151,21 @@ export function syncIosVersioning(params?: {
}): {
updatedPaths: string[];
} {
const mode = params?.mode ?? "write";
const rootDir = path.resolve(params?.rootDir ?? ".");
const releaseVersion = params?.releaseVersion;
const version = resolveIosVersion(rootDir, { releaseVersion });
const changelogContent = readFileSync(version.changelogPath, "utf8");
const nextVersionXcconfig = renderIosVersionXcconfig(version);
const nextReleaseNotes = renderIosReleaseNotes(version, changelogContent);
const updatedPaths: string[] = [];
renderIosReleaseNotes(version, changelogContent);
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 };
return { updatedPaths: [] };
}
export function renderIosReleaseNotesForVersion(params?: {
releaseVersion?: string | null;
rootDir?: string;
}): string {
const rootDir = path.resolve(params?.rootDir ?? ".");
const version = resolveIosVersion(rootDir, { releaseVersion: params?.releaseVersion });
const changelogContent = readFileSync(version.changelogPath, "utf8");
return renderIosReleaseNotes(version, changelogContent);
}