import { spawnSync } from "node:child_process"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; const DEFAULT_REPOSITORY = "tianma-if/edgeever"; const PLATFORMS = new Set(["android", "ios", "both"]); const ANDROID_TRACKS = new Set(["internal", "alpha", "beta", "production"]); const usage = `Usage: bun run publish:stores -- --release vX.Y.Z [options] Options: --release Required formal GitHub Release tag --platform android, ios, or both (default: both) --android-track production, alpha, beta, or internal (default: production) --recover-play-apk Skip Play upload and recover its already signed APK --ios-build-number Submit an existing App Store Connect build; omit to start Xcode Cloud --repository GitHub repository (default: ${DEFAULT_REPOSITORY}) --dry-run Print the workflow dispatch plan --help Show this help `; export const parseStoreDeliveryArgs = (argv) => { const options = { releaseTag: "", platform: "both", androidTrack: "production", repository: DEFAULT_REPOSITORY, dryRun: false, recoverPlayApk: false, iosBuildNumber: "", help: false, }; const valueOptions = new Map([ ["--release", "releaseTag"], ["--platform", "platform"], ["--android-track", "androidTrack"], ["--ios-build-number", "iosBuildNumber"], ["--repository", "repository"], ]); for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]; if (argument === "--dry-run") { options.dryRun = true; continue; } if (argument === "--recover-play-apk") { options.recoverPlayApk = true; continue; } if (argument === "--help") { options.help = true; continue; } const key = valueOptions.get(argument); if (!key) { throw new Error(`Unknown option: ${argument}`); } const value = argv[index + 1]?.trim(); if (!value || value.startsWith("--")) { throw new Error(`${argument} requires a value.`); } options[key] = value; index += 1; } if (options.help) { return options; } if (!/^v\d+\.\d+\.\d+$/.test(options.releaseTag)) { throw new Error("--release must use stable vX.Y.Z format."); } if (!PLATFORMS.has(options.platform)) { throw new Error("--platform must be android, ios, or both."); } if (!ANDROID_TRACKS.has(options.androidTrack)) { throw new Error( "--android-track must be internal, alpha, beta, or production.", ); } if (!/^[^/\s]+\/[^/\s]+$/.test(options.repository)) { throw new Error("--repository must use owner/name format."); } if (options.iosBuildNumber && !/^[1-9]\d*$/.test(options.iosBuildNumber)) { throw new Error("--ios-build-number must be a positive integer."); } if (options.iosBuildNumber && options.platform === "android") { throw new Error("--ios-build-number requires --platform ios or both."); } return options; }; const run = (options) => { const args = [ "workflow", "run", "store-delivery.yml", "--repo", options.repository, "--ref", "main", "-f", `release_tag=${options.releaseTag}`, "-f", `platform=${options.platform}`, "-f", `android_track=${options.androidTrack}`, "-f", `recover_play_apk=${options.recoverPlayApk}`, "-f", `ios_build_number=${options.iosBuildNumber}`, ]; if (options.dryRun) { console.log(`gh ${args.join(" ")}`); return; } const result = spawnSync("gh", args, { cwd: resolve("."), encoding: "utf8", env: process.env, stdio: ["ignore", "pipe", "inherit"], }); if (result.status !== 0) { throw new Error( `GitHub workflow dispatch failed with status ${result.status ?? 1}.`, ); } const output = String(result.stdout ?? "").trim(); console.log(output || "Store delivery workflow dispatched."); }; if ( process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) ) { try { const options = parseStoreDeliveryArgs(process.argv.slice(2)); if (options.help) { console.log(usage); } else { run(options); } } catch (error) { console.error( `[store-delivery] failed: ${error instanceof Error ? error.message : String(error)}`, ); process.exit(1); } }