@livx.cc/appwrap 0.36.1 → 0.37.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
CHANGED
package/src/cli.ts
CHANGED
|
@@ -87,6 +87,12 @@ function resolveAssetRoot(rel: string): string {
|
|
|
87
87
|
const TEMPLATE_DIR = resolveAssetRoot('runtime');
|
|
88
88
|
const CI_TEMPLATE_DIR = resolveAssetRoot('templates/ci');
|
|
89
89
|
|
|
90
|
+
/** This CLI's own published version — used to pin the `bunx @livx.cc/appwrap@^x.y.z` invocations the
|
|
91
|
+
* emitted workflow runs. Pinning to THIS version's floor means CI fails LOUDLY ("version not found")
|
|
92
|
+
* until that version is published, instead of bunx silently resolving an older published build that
|
|
93
|
+
* lacks `release`/`init` flags. package.json sits one level above src/ (published) or the monorepo dir. */
|
|
94
|
+
const CLI_VERSION: string = (await import(resolve(import.meta.dir, '..', 'package.json'), { with: { type: 'json' } })).default.version;
|
|
95
|
+
|
|
90
96
|
// Load the capability manifest VALUES from the resolved runtime (pure data — safe outside NativeScript).
|
|
91
97
|
// Top-level await resolves before any command dispatches at the bottom of this file.
|
|
92
98
|
const { MODULES, OPTIONAL_GROUPS } = (await import(
|
|
@@ -959,23 +965,34 @@ export function isFrameworkRepo(root: string): boolean {
|
|
|
959
965
|
return existsSync(join(root, 'packages/appwrap-cli/src/cli.ts'));
|
|
960
966
|
}
|
|
961
967
|
|
|
962
|
-
/** Emit CI scaffolding (GH Actions → git repo root, fastlane → native/).
|
|
963
|
-
|
|
968
|
+
/** Emit CI scaffolding (GH Actions → git repo root, fastlane → native/).
|
|
969
|
+
* GH workflows are never overwritten (users may customize them). The fastlane lane IS appwrap-managed
|
|
970
|
+
* (the release recipe, not for hand-editing — see AGENTS.md), so it is RE-EMITTED on `--force` to keep
|
|
971
|
+
* the recipe current after a framework upgrade; without --force it's still first-time-only. */
|
|
972
|
+
function copyCiTemplates(cwd: string, outDir: string, cfg: AppwrapConfig, force = false): void {
|
|
964
973
|
if (!existsSync(CI_TEMPLATE_DIR)) return;
|
|
965
974
|
const repoRoot = gitRoot(cwd);
|
|
966
975
|
// GitHub only reads `.github/workflows` at the REPO ROOT — in a monorepo, writing it under the
|
|
967
976
|
// package cwd (e.g. packages/app/.github) is dead config and regenerates a stray workflow each init.
|
|
968
|
-
|
|
977
|
+
// [from, to, overwritable]
|
|
978
|
+
const targets: Array<[string, string, boolean]> = [[join(CI_TEMPLATE_DIR, 'fastlane'), join(outDir, 'fastlane'), force]];
|
|
969
979
|
// …but if the repo root IS the appwrap framework itself (in-repo example), DON'T scaffold consumer
|
|
970
980
|
// workflows into the framework's .github — that's the stray-workflow-each-init bug.
|
|
971
981
|
if (isFrameworkRepo(repoRoot)) {
|
|
972
982
|
console.log(' ci ← GH Actions scaffold skipped (inside the appwrap framework repo — manages its own CI)');
|
|
973
983
|
} else {
|
|
974
|
-
targets.unshift([join(CI_TEMPLATE_DIR, 'github/workflows'), join(repoRoot, '.github/workflows')]);
|
|
984
|
+
targets.unshift([join(CI_TEMPLATE_DIR, 'github/workflows'), join(repoRoot, '.github/workflows'), false]);
|
|
975
985
|
}
|
|
976
|
-
for (const [from, to] of targets) {
|
|
986
|
+
for (const [from, to, overwrite] of targets) {
|
|
977
987
|
mkdirSync(to, { recursive: true });
|
|
978
|
-
cpSync(from, to, { recursive: true, force:
|
|
988
|
+
cpSync(from, to, { recursive: true, force: overwrite, errorOnExist: false });
|
|
989
|
+
}
|
|
990
|
+
// Pin the emitted workflow's `bunx @livx.cc/appwrap@^x.y.z` to THIS CLI's version floor, so a CI run
|
|
991
|
+
// using a freshly-emitted workflow can't silently resolve an older published build that lacks the
|
|
992
|
+
// `init`/`release` commands (it would 404 loudly instead). Idempotent: re-init finds no placeholder.
|
|
993
|
+
if (!isFrameworkRepo(repoRoot)) {
|
|
994
|
+
const wf = join(repoRoot, '.github/workflows/appwrap-release-ios.yml');
|
|
995
|
+
if (existsSync(wf)) writeFileSync(wf, readFileSync(wf, 'utf8').replaceAll('__APPWRAP_VERSION__', CLI_VERSION));
|
|
979
996
|
}
|
|
980
997
|
// Stamp the app id + team into the emitted fastlane (signing needs them; the templates ship
|
|
981
998
|
// `__APP_ID__`/`__TEAM_ID__` placeholders). Idempotent: re-init finds no placeholders → no-op.
|
|
@@ -1060,7 +1077,7 @@ async function init(cwd: string, flags: Record<string, string>): Promise<void> {
|
|
|
1060
1077
|
console.log(`🎁 appwrap init → ${outDir}`);
|
|
1061
1078
|
mkdirSync(outDir, { recursive: true });
|
|
1062
1079
|
regenerateCore(cwd, outDir, cfg, { firstRun: true, flags });
|
|
1063
|
-
copyCiTemplates(cwd, outDir, cfg); // first-time
|
|
1080
|
+
copyCiTemplates(cwd, outDir, cfg, 'force' in flags); // GH workflows: first-time only; fastlane lane: re-emit on --force
|
|
1064
1081
|
writeFileSync(join(outDir, '.gitignore'), 'node_modules/\nplatforms/\nhooks/\n');
|
|
1065
1082
|
applyOverrides(cwd, outDir, cfg); // escape hatch — last, so custom native code wins
|
|
1066
1083
|
stampVersionManifest(outDir, cfg); // provenance — also marks the dir appwrap-managed
|
|
@@ -1176,6 +1193,84 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
|
|
|
1176
1193
|
}
|
|
1177
1194
|
}
|
|
1178
1195
|
|
|
1196
|
+
/** `appwrap release ios` — the ONE build+sign+upload-to-TestFlight command, identical locally and in CI.
|
|
1197
|
+
*
|
|
1198
|
+
* It re-stamps the config + PWA (`sync`) and then delegates the full archive/sign/upload to the emitted
|
|
1199
|
+
* fastlane lane (`native/fastlane` `:beta`) — the SINGLE source of truth for the iOS release recipe (the
|
|
1200
|
+
* lane runs `ns prepare ios --release` → match signing → build_app → upload_to_testflight). CI is a thin
|
|
1201
|
+
* wrapper that just calls this. Keeping the recipe in fastlane (not duplicated in TS) means local and CI
|
|
1202
|
+
* run byte-identical steps.
|
|
1203
|
+
*
|
|
1204
|
+
* Knobs (all optional; mirror the workflow):
|
|
1205
|
+
* --server-url <url> override loader:'server' serverUrl for this release (lab vs prod backend)
|
|
1206
|
+
* --env <name> convenience: resolve serverUrl from cfg.envs[name] when present (see config)
|
|
1207
|
+
* --build-number <n> set the store CFBundleVersion (sets APPWRAP_BUILD_NUMBER for the lane)
|
|
1208
|
+
*
|
|
1209
|
+
* Signing/ASC config is read from env by the lane (ASC_KEY_ID / ASC_ISSUER_ID / ASC_KEY_P8 /
|
|
1210
|
+
* MATCH_GIT_URL / MATCH_PASSWORD) — secrets never live in appwrap.config. */
|
|
1211
|
+
async function release(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
|
|
1212
|
+
const platform = positionals[0];
|
|
1213
|
+
if (platform !== 'ios') {
|
|
1214
|
+
console.error('Usage: appwrap release ios [--server-url <url>] [--env <name>] [--build-number <n>] [--config <path>] [--out native]\n' +
|
|
1215
|
+
' (Android: `appwrap build android --release --aab` then `fastlane android beta`.)');
|
|
1216
|
+
process.exit(1);
|
|
1217
|
+
}
|
|
1218
|
+
const cfg = await loadConfig(cwd, flags);
|
|
1219
|
+
const outDir = resolve(cwd, flags.out ?? 'native');
|
|
1220
|
+
if (!existsSync(outDir)) {
|
|
1221
|
+
console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first (CI must \`init\`, native/ is gitignored).`);
|
|
1222
|
+
process.exit(1);
|
|
1223
|
+
}
|
|
1224
|
+
const fastfile = join(outDir, 'fastlane', 'Fastfile');
|
|
1225
|
+
if (!existsSync(fastfile)) {
|
|
1226
|
+
console.error(`✖ No fastlane lane at ${fastfile} — run \`appwrap init\` to emit it (it carries the release recipe).`);
|
|
1227
|
+
process.exit(1);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Optional backend-url override for loader:'server' apps (lab vs prod). --server-url wins; else
|
|
1231
|
+
// --env resolves from cfg.envs[name] when the consumer config declares it.
|
|
1232
|
+
let serverUrl = flags['server-url'] || undefined;
|
|
1233
|
+
if (!serverUrl && flags.env) {
|
|
1234
|
+
serverUrl = (cfg as { envs?: Record<string, string> }).envs?.[flags.env];
|
|
1235
|
+
if (!serverUrl) {
|
|
1236
|
+
console.error(`✖ --env ${flags.env} given but cfg.envs[${flags.env}] is not set in the config.`);
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const stampCfg = serverUrl ? { ...cfg, loader: 'server' as const, serverUrl } : cfg;
|
|
1241
|
+
|
|
1242
|
+
// Build number: explicit flag → APPWRAP_BUILD_NUMBER (which the CLI's stamping already honors and
|
|
1243
|
+
// wins over the derived default). In CI the workflow sets APPWRAP_BUILD_NUMBER itself.
|
|
1244
|
+
const env = { ...process.env };
|
|
1245
|
+
if (flags['build-number']) {
|
|
1246
|
+
if (!/^\d+$/.test(flags['build-number'])) {
|
|
1247
|
+
console.error(`✖ --build-number must be a positive integer (got "${flags['build-number']}").`);
|
|
1248
|
+
process.exit(1);
|
|
1249
|
+
}
|
|
1250
|
+
env.APPWRAP_BUILD_NUMBER = flags['build-number'];
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Re-stamp config + copy the latest PWA into native/ so the lane archives current sources. (The lane
|
|
1254
|
+
// also runs `ns prepare ios --release`; sync here makes the wrapper config/PWA authoritative first.)
|
|
1255
|
+
await sync(cwd, flags);
|
|
1256
|
+
if (serverUrl) {
|
|
1257
|
+
stampShellConfig(outDir, stampCfg);
|
|
1258
|
+
console.log(`✓ Release loader → ${serverUrl}${flags.env ? ` (env: ${flags.env})` : ''}`);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
console.log(`▶ fastlane ios beta (cwd: ${outDir}/fastlane → native/)${env.APPWRAP_BUILD_NUMBER ? ` build #${env.APPWRAP_BUILD_NUMBER}` : ''}`);
|
|
1262
|
+
try {
|
|
1263
|
+
execFileSync('fastlane', ['ios', 'beta'], { cwd: outDir, stdio: 'inherit', env });
|
|
1264
|
+
} catch {
|
|
1265
|
+
console.error('\n✖ TestFlight release failed. Common causes:\n' +
|
|
1266
|
+
' • Missing ASC/match env: ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_P8 (base64), MATCH_GIT_URL, MATCH_PASSWORD.\n' +
|
|
1267
|
+
' • Certs/profiles not seeded — run `fastlane match appstore` once against MATCH_GIT_URL.\n' +
|
|
1268
|
+
' • CFBundleVersion already used for this marketing version → pass a higher --build-number.');
|
|
1269
|
+
process.exit(1);
|
|
1270
|
+
}
|
|
1271
|
+
console.log('✓ Uploaded to TestFlight (App Store Connect processing — check the build list / wait for the email).');
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1179
1274
|
interface AppleTeam { teamId: string; name: string; email?: string; paid: boolean }
|
|
1180
1275
|
interface DeviceInfo { id: string; name: string; model: string; transport: string }
|
|
1181
1276
|
|
|
@@ -1712,13 +1807,17 @@ async function main(): Promise<void> {
|
|
|
1712
1807
|
case 'deploy':
|
|
1713
1808
|
await deploy(cwd, flags, positionals);
|
|
1714
1809
|
break;
|
|
1810
|
+
case 'release':
|
|
1811
|
+
await release(cwd, flags, positionals);
|
|
1812
|
+
break;
|
|
1715
1813
|
case 'logs':
|
|
1716
1814
|
await logs(cwd, flags, positionals);
|
|
1717
1815
|
break;
|
|
1718
1816
|
default:
|
|
1719
|
-
console.log('Usage: appwrap <init|sync|dev|build|deploy|logs> [--config <path>] [--out native]\n' +
|
|
1817
|
+
console.log('Usage: appwrap <init|sync|dev|build|deploy|release|logs> [--config <path>] [--out native]\n' +
|
|
1720
1818
|
' config: appwrap.config.ts (preferred) → .js → appwrap.json\n' +
|
|
1721
1819
|
' build <ios|android> [--release] [--aab] deploy ios [--device <id|name>] [--no-launch]\n' +
|
|
1820
|
+
' release ios [--server-url <url>] [--env <name>] [--build-number <n>] (build+sign+upload to TestFlight)\n' +
|
|
1722
1821
|
' logs ios [--once] [--native] dev [--url <url> | --port <p>]');
|
|
1723
1822
|
process.exit(command ? 1 : 0);
|
|
1724
1823
|
}
|
|
@@ -6,6 +6,36 @@ default_platform(:ios)
|
|
|
6
6
|
APP_ID = '__APP_ID__'.freeze
|
|
7
7
|
TEAM_ID = '__TEAM_ID__'.freeze
|
|
8
8
|
|
|
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
|
|
38
|
+
|
|
9
39
|
platform :ios do
|
|
10
40
|
desc 'Build the NativeScript wrapper and upload to TestFlight'
|
|
11
41
|
lane :beta do
|
|
@@ -23,7 +53,8 @@ platform :ios do
|
|
|
23
53
|
git_url: ENV['MATCH_GIT_URL'], api_key: api_key)
|
|
24
54
|
|
|
25
55
|
# NativeScript prepares the Xcode project inside the wrapper dir (native/ = parent of fastlane/).
|
|
26
|
-
|
|
56
|
+
# Wrapped to sanitize the gem env so the system `pod` can find CocoaPods (see appwrap_prepare_ios).
|
|
57
|
+
appwrap_prepare_ios
|
|
27
58
|
|
|
28
59
|
# NS defaults the project to automatic signing → archive can't find a profile. Pin manual signing
|
|
29
60
|
# to the match-provided App Store profile (name exported by match into ENV).
|
|
@@ -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.
|