@livx.cc/appwrap 0.36.1 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.ts +148 -8
- package/src/config.ts +5 -0
- package/src/derive.ts +20 -0
- package/templates/ci/fastlane/Fastfile +93 -32
- package/templates/ci/github/workflows/appwrap-release-ios.yml +10 -3
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type * as CapManifest from '../../../runtime/app/shell/capabilities.manif
|
|
|
22
22
|
import type { AppwrapConfig } from './config';
|
|
23
23
|
import {
|
|
24
24
|
androidScreenOrientation,
|
|
25
|
+
applyBuildNumberFlag,
|
|
25
26
|
iosOrientations,
|
|
26
27
|
mergeManifest,
|
|
27
28
|
resolveBuildNumber,
|
|
@@ -87,6 +88,12 @@ function resolveAssetRoot(rel: string): string {
|
|
|
87
88
|
const TEMPLATE_DIR = resolveAssetRoot('runtime');
|
|
88
89
|
const CI_TEMPLATE_DIR = resolveAssetRoot('templates/ci');
|
|
89
90
|
|
|
91
|
+
/** This CLI's own published version — used to pin the `bunx @livx.cc/appwrap@^x.y.z` invocations the
|
|
92
|
+
* emitted workflow runs. Pinning to THIS version's floor means CI fails LOUDLY ("version not found")
|
|
93
|
+
* until that version is published, instead of bunx silently resolving an older published build that
|
|
94
|
+
* lacks `release`/`init` flags. package.json sits one level above src/ (published) or the monorepo dir. */
|
|
95
|
+
const CLI_VERSION: string = (await import(resolve(import.meta.dir, '..', 'package.json'), { with: { type: 'json' } })).default.version;
|
|
96
|
+
|
|
90
97
|
// Load the capability manifest VALUES from the resolved runtime (pure data — safe outside NativeScript).
|
|
91
98
|
// Top-level await resolves before any command dispatches at the bottom of this file.
|
|
92
99
|
const { MODULES, OPTIONAL_GROUPS } = (await import(
|
|
@@ -519,6 +526,20 @@ function stampTeamId(outDir: string, cfg: AppwrapConfig, ctx?: { cwd: string; co
|
|
|
519
526
|
writeFileSync(xcconfig, src);
|
|
520
527
|
}
|
|
521
528
|
|
|
529
|
+
/** Stamp TARGETED_DEVICE_FAMILY into build.xcconfig from cfg.targetedDevices. `'iphone'` → `1`
|
|
530
|
+
* (iPhone-only → UIDeviceFamily=[1], so the App Store doesn't require iPad screenshots);
|
|
531
|
+
* `'universal'`/unset → `1,2` (NativeScript's default). Idempotent: replaces any prior value. */
|
|
532
|
+
function stampDeviceFamily(outDir: string, cfg: AppwrapConfig): void {
|
|
533
|
+
const xcconfig = join(outDir, 'App_Resources/iOS/build.xcconfig');
|
|
534
|
+
if (!existsSync(xcconfig)) return;
|
|
535
|
+
const value = cfg.targetedDevices === 'iphone' ? '1' : '1,2';
|
|
536
|
+
let src = readFileSync(xcconfig, 'utf8');
|
|
537
|
+
src = /TARGETED_DEVICE_FAMILY\s*=/.test(src)
|
|
538
|
+
? src.replace(/TARGETED_DEVICE_FAMILY\s*=\s*[^;\n]*;?/, `TARGETED_DEVICE_FAMILY = ${value};`)
|
|
539
|
+
: src + `\nTARGETED_DEVICE_FAMILY = ${value};\n`;
|
|
540
|
+
writeFileSync(xcconfig, src);
|
|
541
|
+
}
|
|
542
|
+
|
|
522
543
|
/** Wire a StoreKit config file for LOCAL iOS IAP testing (no App Store Connect needed).
|
|
523
544
|
* NativeScript copies App_Resources/iOS/* into the generated project and adds it as a file
|
|
524
545
|
* reference — but it never points the scheme at it, so StoreKit has no catalog. We (1) drop the
|
|
@@ -959,23 +980,34 @@ export function isFrameworkRepo(root: string): boolean {
|
|
|
959
980
|
return existsSync(join(root, 'packages/appwrap-cli/src/cli.ts'));
|
|
960
981
|
}
|
|
961
982
|
|
|
962
|
-
/** Emit CI scaffolding (GH Actions → git repo root, fastlane → native/).
|
|
963
|
-
|
|
983
|
+
/** Emit CI scaffolding (GH Actions → git repo root, fastlane → native/).
|
|
984
|
+
* GH workflows are never overwritten (users may customize them). The fastlane lane IS appwrap-managed
|
|
985
|
+
* (the release recipe, not for hand-editing — see AGENTS.md), so it is RE-EMITTED on `--force` to keep
|
|
986
|
+
* the recipe current after a framework upgrade; without --force it's still first-time-only. */
|
|
987
|
+
function copyCiTemplates(cwd: string, outDir: string, cfg: AppwrapConfig, force = false): void {
|
|
964
988
|
if (!existsSync(CI_TEMPLATE_DIR)) return;
|
|
965
989
|
const repoRoot = gitRoot(cwd);
|
|
966
990
|
// GitHub only reads `.github/workflows` at the REPO ROOT — in a monorepo, writing it under the
|
|
967
991
|
// package cwd (e.g. packages/app/.github) is dead config and regenerates a stray workflow each init.
|
|
968
|
-
|
|
992
|
+
// [from, to, overwritable]
|
|
993
|
+
const targets: Array<[string, string, boolean]> = [[join(CI_TEMPLATE_DIR, 'fastlane'), join(outDir, 'fastlane'), force]];
|
|
969
994
|
// …but if the repo root IS the appwrap framework itself (in-repo example), DON'T scaffold consumer
|
|
970
995
|
// workflows into the framework's .github — that's the stray-workflow-each-init bug.
|
|
971
996
|
if (isFrameworkRepo(repoRoot)) {
|
|
972
997
|
console.log(' ci ← GH Actions scaffold skipped (inside the appwrap framework repo — manages its own CI)');
|
|
973
998
|
} else {
|
|
974
|
-
targets.unshift([join(CI_TEMPLATE_DIR, 'github/workflows'), join(repoRoot, '.github/workflows')]);
|
|
999
|
+
targets.unshift([join(CI_TEMPLATE_DIR, 'github/workflows'), join(repoRoot, '.github/workflows'), false]);
|
|
975
1000
|
}
|
|
976
|
-
for (const [from, to] of targets) {
|
|
1001
|
+
for (const [from, to, overwrite] of targets) {
|
|
977
1002
|
mkdirSync(to, { recursive: true });
|
|
978
|
-
cpSync(from, to, { recursive: true, force:
|
|
1003
|
+
cpSync(from, to, { recursive: true, force: overwrite, errorOnExist: false });
|
|
1004
|
+
}
|
|
1005
|
+
// Pin the emitted workflow's `bunx @livx.cc/appwrap@^x.y.z` to THIS CLI's version floor, so a CI run
|
|
1006
|
+
// using a freshly-emitted workflow can't silently resolve an older published build that lacks the
|
|
1007
|
+
// `init`/`release` commands (it would 404 loudly instead). Idempotent: re-init finds no placeholder.
|
|
1008
|
+
if (!isFrameworkRepo(repoRoot)) {
|
|
1009
|
+
const wf = join(repoRoot, '.github/workflows/appwrap-release-ios.yml');
|
|
1010
|
+
if (existsSync(wf)) writeFileSync(wf, readFileSync(wf, 'utf8').replaceAll('__APPWRAP_VERSION__', CLI_VERSION));
|
|
979
1011
|
}
|
|
980
1012
|
// Stamp the app id + team into the emitted fastlane (signing needs them; the templates ship
|
|
981
1013
|
// `__APP_ID__`/`__TEAM_ID__` placeholders). Idempotent: re-init finds no placeholders → no-op.
|
|
@@ -1019,6 +1051,7 @@ function regenerateCore(cwd: string, outDir: string, cfg: AppwrapConfig, opts: {
|
|
|
1019
1051
|
stampNativeScriptConfig(outDir, cfg);
|
|
1020
1052
|
stampIOSDisplayName(outDir, cfg, req);
|
|
1021
1053
|
stampTeamId(outDir, cfg, { cwd, configPath: resolveConfigPath(cwd, opts.flags ?? {}) });
|
|
1054
|
+
stampDeviceFamily(outDir, cfg);
|
|
1022
1055
|
stampAndroidAppName(outDir, cfg, req);
|
|
1023
1056
|
stampAndroidVersion(outDir, cfg);
|
|
1024
1057
|
stampAndroidGradleDeps(outDir, req.androidGradleDeps);
|
|
@@ -1060,7 +1093,7 @@ async function init(cwd: string, flags: Record<string, string>): Promise<void> {
|
|
|
1060
1093
|
console.log(`🎁 appwrap init → ${outDir}`);
|
|
1061
1094
|
mkdirSync(outDir, { recursive: true });
|
|
1062
1095
|
regenerateCore(cwd, outDir, cfg, { firstRun: true, flags });
|
|
1063
|
-
copyCiTemplates(cwd, outDir, cfg); // first-time
|
|
1096
|
+
copyCiTemplates(cwd, outDir, cfg, 'force' in flags); // GH workflows: first-time only; fastlane lane: re-emit on --force
|
|
1064
1097
|
writeFileSync(join(outDir, '.gitignore'), 'node_modules/\nplatforms/\nhooks/\n');
|
|
1065
1098
|
applyOverrides(cwd, outDir, cfg); // escape hatch — last, so custom native code wins
|
|
1066
1099
|
stampVersionManifest(outDir, cfg); // provenance — also marks the dir appwrap-managed
|
|
@@ -1141,6 +1174,14 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
|
|
|
1141
1174
|
console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first`);
|
|
1142
1175
|
process.exit(1);
|
|
1143
1176
|
}
|
|
1177
|
+
// --build-number → process.env.APPWRAP_BUILD_NUMBER before sync() stamps the plist/gradle (same
|
|
1178
|
+
// mechanism `release` uses). Env-var path still honored by sync()'s buildNumberOf when no flag.
|
|
1179
|
+
try {
|
|
1180
|
+
applyBuildNumberFlag(flags['build-number']);
|
|
1181
|
+
} catch (e) {
|
|
1182
|
+
console.error(`✖ ${(e as Error).message}`);
|
|
1183
|
+
process.exit(1);
|
|
1184
|
+
}
|
|
1144
1185
|
// Make sure the wrapper reflects the latest config + PWA before compiling (also validates the config).
|
|
1145
1186
|
await sync(cwd, flags);
|
|
1146
1187
|
|
|
@@ -1176,6 +1217,97 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
|
|
|
1176
1217
|
}
|
|
1177
1218
|
}
|
|
1178
1219
|
|
|
1220
|
+
/** `appwrap release ios` — the ONE build+sign+upload-to-TestFlight command, identical locally and in CI.
|
|
1221
|
+
*
|
|
1222
|
+
* It re-stamps the config + PWA (`sync`) and then delegates the full archive/sign/upload to the emitted
|
|
1223
|
+
* fastlane lane (`native/fastlane` `:beta`) — the SINGLE source of truth for the iOS release recipe (the
|
|
1224
|
+
* lane runs `ns prepare ios --release` → match signing → build_app → upload_to_testflight). CI is a thin
|
|
1225
|
+
* wrapper that just calls this. Keeping the recipe in fastlane (not duplicated in TS) means local and CI
|
|
1226
|
+
* run byte-identical steps.
|
|
1227
|
+
*
|
|
1228
|
+
* Knobs (all optional; mirror the workflow):
|
|
1229
|
+
* --server-url <url> override loader:'server' serverUrl for this release (lab vs prod backend)
|
|
1230
|
+
* --env <name> convenience: resolve serverUrl from cfg.envs[name] when present (see config)
|
|
1231
|
+
* --build-number <n> set the store CFBundleVersion (sets APPWRAP_BUILD_NUMBER for the lane)
|
|
1232
|
+
*
|
|
1233
|
+
* Signing/ASC config is read from env by the lane (ASC_KEY_ID / ASC_ISSUER_ID / ASC_KEY_P8 /
|
|
1234
|
+
* MATCH_GIT_URL / MATCH_PASSWORD) — secrets never live in appwrap.config.
|
|
1235
|
+
*
|
|
1236
|
+
* `appwrap submit ios` reuses this same body with `lane: 'release'` → fastlane `:release`
|
|
1237
|
+
* (`upload_to_app_store`): a binary-only App Store production promote. Metadata/screenshots stay in
|
|
1238
|
+
* ASC; pass `--submit-for-review` to also submit the build for review. */
|
|
1239
|
+
async function release(cwd: string, flags: Record<string, string>, positionals: string[], lane: 'beta' | 'release' = 'beta'): Promise<void> {
|
|
1240
|
+
const submit = lane === 'release';
|
|
1241
|
+
const cmd = submit ? 'submit' : 'release';
|
|
1242
|
+
const platform = positionals[0];
|
|
1243
|
+
if (platform !== 'ios') {
|
|
1244
|
+
console.error(`Usage: appwrap ${cmd} ios [--server-url <url>] [--env <name>] [--build-number <n>]` +
|
|
1245
|
+
(submit ? ' [--submit-for-review]' : '') + ' [--config <path>] [--out native]\n' +
|
|
1246
|
+
' (Android: `appwrap build android --release --aab` then `fastlane android beta`.)');
|
|
1247
|
+
process.exit(1);
|
|
1248
|
+
}
|
|
1249
|
+
const cfg = await loadConfig(cwd, flags);
|
|
1250
|
+
const outDir = resolve(cwd, flags.out ?? 'native');
|
|
1251
|
+
if (!existsSync(outDir)) {
|
|
1252
|
+
console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first (CI must \`init\`, native/ is gitignored).`);
|
|
1253
|
+
process.exit(1);
|
|
1254
|
+
}
|
|
1255
|
+
const fastfile = join(outDir, 'fastlane', 'Fastfile');
|
|
1256
|
+
if (!existsSync(fastfile)) {
|
|
1257
|
+
console.error(`✖ No fastlane lane at ${fastfile} — run \`appwrap init\` to emit it (it carries the release recipe).`);
|
|
1258
|
+
process.exit(1);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Optional backend-url override for loader:'server' apps (lab vs prod). --server-url wins; else
|
|
1262
|
+
// --env resolves from cfg.envs[name] when the consumer config declares it.
|
|
1263
|
+
let serverUrl = flags['server-url'] || undefined;
|
|
1264
|
+
if (!serverUrl && flags.env) {
|
|
1265
|
+
serverUrl = (cfg as { envs?: Record<string, string> }).envs?.[flags.env];
|
|
1266
|
+
if (!serverUrl) {
|
|
1267
|
+
console.error(`✖ --env ${flags.env} given but cfg.envs[${flags.env}] is not set in the config.`);
|
|
1268
|
+
process.exit(1);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const stampCfg = serverUrl ? { ...cfg, loader: 'server' as const, serverUrl } : cfg;
|
|
1272
|
+
|
|
1273
|
+
// Build number: explicit flag → APPWRAP_BUILD_NUMBER. Stamping (sync → stampIOSDisplayName →
|
|
1274
|
+
// buildNumberOf) reads process.env.APPWRAP_BUILD_NUMBER, and sync() runs BEFORE fastlane archives
|
|
1275
|
+
// the (already-stamped) Info.plist — so the flag must land on process.env *here*, before sync(),
|
|
1276
|
+
// not only on the child env. In CI the workflow sets APPWRAP_BUILD_NUMBER itself (env path).
|
|
1277
|
+
try {
|
|
1278
|
+
applyBuildNumberFlag(flags['build-number']);
|
|
1279
|
+
} catch (e) {
|
|
1280
|
+
console.error(`✖ ${(e as Error).message}`);
|
|
1281
|
+
process.exit(1);
|
|
1282
|
+
}
|
|
1283
|
+
const env = { ...process.env };
|
|
1284
|
+
// `submit ios --submit-for-review` → also submit the binary for App Store review (the lane defaults
|
|
1285
|
+
// to a safe binary-only promote). The flag is boolean (presence = '' via parseArgs).
|
|
1286
|
+
if (submit && 'submit-for-review' in flags) env.APPWRAP_SUBMIT_FOR_REVIEW = 'true';
|
|
1287
|
+
|
|
1288
|
+
// Re-stamp config + copy the latest PWA into native/ so the lane archives current sources. (The lane
|
|
1289
|
+
// also runs `ns prepare ios --release`; sync here makes the wrapper config/PWA authoritative first.)
|
|
1290
|
+
await sync(cwd, flags);
|
|
1291
|
+
if (serverUrl) {
|
|
1292
|
+
stampShellConfig(outDir, stampCfg);
|
|
1293
|
+
console.log(`✓ ${submit ? 'Submit' : 'Release'} loader → ${serverUrl}${flags.env ? ` (env: ${flags.env})` : ''}`);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
console.log(`▶ fastlane ios ${lane} (cwd: ${outDir}/fastlane → native/)${env.APPWRAP_BUILD_NUMBER ? ` build #${env.APPWRAP_BUILD_NUMBER}` : ''}`);
|
|
1297
|
+
try {
|
|
1298
|
+
execFileSync('fastlane', ['ios', lane], { cwd: outDir, stdio: 'inherit', env });
|
|
1299
|
+
} catch {
|
|
1300
|
+
console.error(`\n✖ ${submit ? 'App Store submit' : 'TestFlight release'} failed. Common causes:\n` +
|
|
1301
|
+
' • Missing ASC/match env: ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_P8 (base64), MATCH_GIT_URL, MATCH_PASSWORD.\n' +
|
|
1302
|
+
' • Certs/profiles not seeded — run `fastlane match appstore` once against MATCH_GIT_URL.\n' +
|
|
1303
|
+
' • CFBundleVersion already used for this marketing version → pass a higher --build-number.');
|
|
1304
|
+
process.exit(1);
|
|
1305
|
+
}
|
|
1306
|
+
console.log(submit
|
|
1307
|
+
? `✓ Binary promoted to the App Store (production)${env.APPWRAP_SUBMIT_FOR_REVIEW === 'true' ? ' and submitted for review' : ' — submit for review in App Store Connect (or pass --submit-for-review)'}.`
|
|
1308
|
+
: '✓ Uploaded to TestFlight (App Store Connect processing — check the build list / wait for the email).');
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1179
1311
|
interface AppleTeam { teamId: string; name: string; email?: string; paid: boolean }
|
|
1180
1312
|
interface DeviceInfo { id: string; name: string; model: string; transport: string }
|
|
1181
1313
|
|
|
@@ -1712,13 +1844,21 @@ async function main(): Promise<void> {
|
|
|
1712
1844
|
case 'deploy':
|
|
1713
1845
|
await deploy(cwd, flags, positionals);
|
|
1714
1846
|
break;
|
|
1847
|
+
case 'release':
|
|
1848
|
+
await release(cwd, flags, positionals, 'beta');
|
|
1849
|
+
break;
|
|
1850
|
+
case 'submit':
|
|
1851
|
+
await release(cwd, flags, positionals, 'release');
|
|
1852
|
+
break;
|
|
1715
1853
|
case 'logs':
|
|
1716
1854
|
await logs(cwd, flags, positionals);
|
|
1717
1855
|
break;
|
|
1718
1856
|
default:
|
|
1719
|
-
console.log('Usage: appwrap <init|sync|dev|build|deploy|logs> [--config <path>] [--out native]\n' +
|
|
1857
|
+
console.log('Usage: appwrap <init|sync|dev|build|deploy|release|submit|logs> [--config <path>] [--out native]\n' +
|
|
1720
1858
|
' config: appwrap.config.ts (preferred) → .js → appwrap.json\n' +
|
|
1721
1859
|
' build <ios|android> [--release] [--aab] deploy ios [--device <id|name>] [--no-launch]\n' +
|
|
1860
|
+
' release ios [--server-url <url>] [--env <name>] [--build-number <n>] (build+sign+upload to TestFlight)\n' +
|
|
1861
|
+
' submit ios [--build-number <n>] [--submit-for-review] (promote the binary to the App Store; metadata stays in ASC)\n' +
|
|
1722
1862
|
' logs ios [--once] [--native] dev [--url <url> | --port <p>]');
|
|
1723
1863
|
process.exit(command ? 1 : 0);
|
|
1724
1864
|
}
|
package/src/config.ts
CHANGED
|
@@ -34,6 +34,11 @@ export interface AppwrapConfig {
|
|
|
34
34
|
* axis; `any` (default) leaves rotation free (sans upside-down on iOS). Stamped into iOS
|
|
35
35
|
* `UISupportedInterfaceOrientations` (+ `~ipad`) and Android `android:screenOrientation`. */
|
|
36
36
|
orientation?: 'portrait' | 'landscape' | 'any';
|
|
37
|
+
/** iOS targeted device family. `'iphone'` builds an iPhone-only app (TARGETED_DEVICE_FAMILY=1 →
|
|
38
|
+
* UIDeviceFamily=[1]); `'universal'` (default — NativeScript's behavior) targets iPhone + iPad,
|
|
39
|
+
* which makes the App Store require a full iPad screenshot set. Set `'iphone'` for a phone-only
|
|
40
|
+
* app to avoid that requirement. iOS-only; no-op on Android. */
|
|
41
|
+
targetedDevices?: 'iphone' | 'universal';
|
|
37
42
|
/** Android only (experimental). When true, the WebView draws genuinely edge-to-edge UNDER the
|
|
38
43
|
* transparent system bars (NS `androidOverflowEdge='dont-apply'`) and the real safe-area insets
|
|
39
44
|
* are injected as `--saie-*` CSS vars + native `env(safe-area-inset-*)`, so a multi-theme PWA
|
package/src/derive.ts
CHANGED
|
@@ -97,6 +97,26 @@ export function resolveBuildNumber(
|
|
|
97
97
|
return deriveBuild(cfg.version);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Apply a `--build-number` CLI flag to the env so stamping picks it up. The stamping chain
|
|
102
|
+
* (sync → stampIOSDisplayName/gradle → buildNumberOf → resolveBuildNumber) reads
|
|
103
|
+
* `APPWRAP_BUILD_NUMBER` off the env, and stamping runs BEFORE the native build/archive — so the
|
|
104
|
+
* flag must land on the env *before* sync(), not only on a child process env copy.
|
|
105
|
+
* Returns the validated build-number string (set on `env`), or undefined if no flag was given.
|
|
106
|
+
* Throws on a non-positive-integer flag so the caller can print + exit.
|
|
107
|
+
*/
|
|
108
|
+
export function applyBuildNumberFlag(
|
|
109
|
+
flag: string | undefined,
|
|
110
|
+
env: { APPWRAP_BUILD_NUMBER?: string } = process.env
|
|
111
|
+
): string | undefined {
|
|
112
|
+
if (flag == null || flag === '') return undefined;
|
|
113
|
+
if (!/^\d+$/.test(flag)) {
|
|
114
|
+
throw new Error(`--build-number must be a positive integer (got "${flag}").`);
|
|
115
|
+
}
|
|
116
|
+
env.APPWRAP_BUILD_NUMBER = flag;
|
|
117
|
+
return flag;
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
/**
|
|
101
121
|
* Collapse a PWA web-manifest `orientation` value to our three states. The manifest spec allows
|
|
102
122
|
* `portrait`/`landscape` plus the `-primary`/`-secondary`/`*-up`/`natural` variants — we don't model
|
|
@@ -6,43 +6,84 @@ default_platform(:ios)
|
|
|
6
6
|
APP_ID = '__APP_ID__'.freeze
|
|
7
7
|
TEAM_ID = '__TEAM_ID__'.freeze
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
# Run `ns prepare` (which shells out to `pod install`) with a SANITIZED gem env.
|
|
10
|
+
#
|
|
11
|
+
# Why: fastlane runs under its own Ruby and exports GEM_HOME/GEM_PATH into every child process. On a
|
|
12
|
+
# Mac, NativeScript's `pod` is the system one (`/usr/local/bin/pod`, shebang-pinned to Ruby 2.6, whose
|
|
13
|
+
# CocoaPods lives in /Library/Ruby/Gems/2.6.0). The inherited GEM_HOME redirects its gem lookup to
|
|
14
|
+
# fastlane's Ruby dir → `can't find gem cocoapods (Gem::GemNotFoundException)` → NS prints
|
|
15
|
+
# `'arch install' command failed` (exit 127). Interactively `pod` works because no GEM_HOME is set.
|
|
16
|
+
#
|
|
17
|
+
# Fix: only when the active `pod` is that system-Ruby-2.6 binary, drop the leaked GEM_* vars so it
|
|
18
|
+
# falls back to its own default gem dir. CI runners use a self-managed Ruby (setup-ruby) whose `pod`
|
|
19
|
+
# is NOT the system 2.6 one, so the env is left untouched there — no behavior change on CI.
|
|
20
|
+
def appwrap_prepare_ios
|
|
21
|
+
prepare = "cd '#{File.expand_path('..', __dir__)}' && ns prepare ios --release"
|
|
22
|
+
pod = `command -v pod`.strip
|
|
23
|
+
system_pod = !pod.empty? && File.exist?(pod) &&
|
|
24
|
+
File.read(pod, 256).include?('Ruby.framework/Versions/2.6')
|
|
25
|
+
unless system_pod
|
|
26
|
+
sh(prepare)
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
leaked = %w[GEM_HOME GEM_PATH RUBYLIB RUBYOPT BUNDLE_GEMFILE BUNDLE_BIN_PATH]
|
|
30
|
+
saved = {}
|
|
31
|
+
leaked.each { |k| saved[k] = ENV[k]; ENV.delete(k) }
|
|
32
|
+
begin
|
|
33
|
+
sh(prepare)
|
|
34
|
+
ensure
|
|
35
|
+
saved.each { |k, v| ENV[k] = v unless v.nil? }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
13
38
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
39
|
+
# Shared archive-and-sign sequence for BOTH the TestFlight (:beta) and App Store (:release) lanes.
|
|
40
|
+
# Returns the ASC API-key object so each lane can reuse it for its final upload step — the lanes
|
|
41
|
+
# differ ONLY in that final step (upload_to_testflight vs upload_to_app_store). Keep the recipe here,
|
|
42
|
+
# not duplicated per-lane.
|
|
43
|
+
def appwrap_archive_and_sign_ios
|
|
44
|
+
setup_ci # on CI: provisions a temporary unlocked keychain so codesign can use the match cert
|
|
20
45
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
46
|
+
api_key = app_store_connect_api_key(
|
|
47
|
+
key_id: ENV['ASC_KEY_ID'],
|
|
48
|
+
issuer_id: ENV['ASC_ISSUER_ID'],
|
|
49
|
+
key_content: ENV['ASC_KEY_P8'],
|
|
50
|
+
is_key_content_base64: true
|
|
51
|
+
)
|
|
24
52
|
|
|
25
|
-
|
|
26
|
-
|
|
53
|
+
# readonly: certs/profiles must be seeded once via `fastlane match appstore` (see workflow header).
|
|
54
|
+
match(type: 'appstore', readonly: true, app_identifier: APP_ID,
|
|
55
|
+
git_url: ENV['MATCH_GIT_URL'], api_key: api_key)
|
|
27
56
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
update_code_signing_settings(
|
|
32
|
-
use_automatic_signing: false,
|
|
33
|
-
path: 'platforms/ios/native.xcodeproj',
|
|
34
|
-
team_id: TEAM_ID,
|
|
35
|
-
bundle_identifier: APP_ID,
|
|
36
|
-
code_sign_identity: 'Apple Distribution',
|
|
37
|
-
profile_name: profile
|
|
38
|
-
)
|
|
57
|
+
# NativeScript prepares the Xcode project inside the wrapper dir (native/ = parent of fastlane/).
|
|
58
|
+
# Wrapped to sanitize the gem env so the system `pod` can find CocoaPods (see appwrap_prepare_ios).
|
|
59
|
+
appwrap_prepare_ios
|
|
39
60
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
# NS defaults the project to automatic signing → archive can't find a profile. Pin manual signing
|
|
62
|
+
# to the match-provided App Store profile (name exported by match into ENV).
|
|
63
|
+
profile = ENV["sigh_#{APP_ID}_appstore_profile-name"] || "match AppStore #{APP_ID}"
|
|
64
|
+
update_code_signing_settings(
|
|
65
|
+
use_automatic_signing: false,
|
|
66
|
+
path: 'platforms/ios/native.xcodeproj',
|
|
67
|
+
team_id: TEAM_ID,
|
|
68
|
+
bundle_identifier: APP_ID,
|
|
69
|
+
code_sign_identity: 'Apple Distribution',
|
|
70
|
+
profile_name: profile
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
build_app(
|
|
74
|
+
workspace: 'platforms/ios/native.xcworkspace',
|
|
75
|
+
scheme: 'native',
|
|
76
|
+
export_method: 'app-store',
|
|
77
|
+
export_options: { provisioningProfiles: { APP_ID => profile } }
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
api_key
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
platform :ios do
|
|
84
|
+
desc 'Build the NativeScript wrapper and upload to TestFlight'
|
|
85
|
+
lane :beta do
|
|
86
|
+
api_key = appwrap_archive_and_sign_ios
|
|
46
87
|
|
|
47
88
|
# Wait for App Store Connect to finish processing and FAIL if it doesn't — Apple can accept the
|
|
48
89
|
# upload yet silently drop the build (e.g. an out-of-range CFBundleVersion). skip:true hides that.
|
|
@@ -54,6 +95,26 @@ platform :ios do
|
|
|
54
95
|
tf_wait_timeout = tf_wait_env.to_i > 0 ? tf_wait_env.to_i : 3600
|
|
55
96
|
upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: false, wait_processing_timeout_duration: tf_wait_timeout)
|
|
56
97
|
end
|
|
98
|
+
|
|
99
|
+
desc 'Build the NativeScript wrapper and promote the binary to the App Store (production).'
|
|
100
|
+
lane :release do
|
|
101
|
+
api_key = appwrap_archive_and_sign_ios
|
|
102
|
+
|
|
103
|
+
# Binary-only promote: the listing/metadata/screenshots/IAP stay in App Store Connect — this lane
|
|
104
|
+
# only ships the signed binary. SAFE by default: no review submit, no automatic release, headless
|
|
105
|
+
# (force: true → no HTML preview). Set APPWRAP_SUBMIT_FOR_REVIEW=true to also submit for review.
|
|
106
|
+
# Guard BOTH unset (nil) and blank env the same way as APPWRAP_TF_WAIT_TIMEOUT above.
|
|
107
|
+
submit_for_review = ENV['APPWRAP_SUBMIT_FOR_REVIEW'].to_s.strip.downcase == 'true'
|
|
108
|
+
upload_to_app_store(
|
|
109
|
+
api_key: api_key,
|
|
110
|
+
submit_for_review: submit_for_review,
|
|
111
|
+
automatic_release: false,
|
|
112
|
+
skip_metadata: true,
|
|
113
|
+
skip_screenshots: true,
|
|
114
|
+
precheck_include_in_app_purchases: false,
|
|
115
|
+
force: true
|
|
116
|
+
)
|
|
117
|
+
end
|
|
57
118
|
end
|
|
58
119
|
|
|
59
120
|
platform :android do
|
|
@@ -34,7 +34,13 @@ jobs:
|
|
|
34
34
|
- run: bun install && bun run build
|
|
35
35
|
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|
|
36
36
|
- run: npm i -g nativescript@9.0.6
|
|
37
|
-
|
|
37
|
+
# bunx is PINNED to the appwrap version that emitted this workflow (stamped at `init`). Pinning is
|
|
38
|
+
# load-bearing: an UNPINNED `bunx @livx.cc/appwrap` resolves to whatever is currently published,
|
|
39
|
+
# which may pre-date the `init`/`release` commands and would silently run old code + exit 1 with a
|
|
40
|
+
# confusing usage dump. With the pin, CI fails LOUDLY ("No matching version") until that version is
|
|
41
|
+
# published to the @livx.cc registry. PUBLISH-ORDERING: publish the appwrap CLI BEFORE the first CI
|
|
42
|
+
# run that uses a freshly-emitted workflow.
|
|
43
|
+
- run: bunx @livx.cc/appwrap@^__APPWRAP_VERSION__ init # native/ is generated (gitignored) — regenerate it fresh in CI
|
|
38
44
|
- working-directory: native
|
|
39
45
|
run: npm install
|
|
40
46
|
- uses: ruby/setup-ruby@v1
|
|
@@ -47,14 +53,15 @@ jobs:
|
|
|
47
53
|
chmod 600 ~/.ssh/match_deploy_key
|
|
48
54
|
printf 'Host github.com\n IdentityFile ~/.ssh/match_deploy_key\n IdentitiesOnly yes\n' >> ~/.ssh/config
|
|
49
55
|
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
|
56
|
+
# Single source of truth: the SAME command a developer runs locally. It re-stamps native/ and
|
|
57
|
+
# delegates the full archive/sign/upload to the emitted fastlane lane — no fastlane logic here.
|
|
50
58
|
- name: Build & upload to TestFlight
|
|
51
|
-
working-directory: native
|
|
52
59
|
env:
|
|
53
60
|
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
|
|
54
61
|
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
|
55
62
|
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
|
56
63
|
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
|
57
64
|
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
|
|
58
|
-
run:
|
|
65
|
+
run: bunx @livx.cc/appwrap@^__APPWRAP_VERSION__ release ios
|
|
59
66
|
|
|
60
67
|
# Production promotion is a manual gate: promote the TestFlight build in App Store Connect.
|