@livx.cc/appwrap 0.58.2 → 0.58.4
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/runtime/App_Resources/Android/app.gradle +1 -1
- package/runtime/app/main-page.ts +54 -1
- package/runtime/app/main-page.xml +8 -0
- package/runtime/app/shell/custom-webview.ios.ts +16 -0
- package/src/cli.ts +64 -9
- package/src/config.ts +15 -2
- package/templates/ci/github/workflows/appwrap-pr.yml +25 -5
- package/templates/ci/github/workflows/appwrap-release-android.yml +31 -10
- package/templates/ci/github/workflows/appwrap-release-ios.yml +9 -4
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@ android {
|
|
|
14
14
|
|
|
15
15
|
defaultConfig {
|
|
16
16
|
minSdkVersion 26 // Android 8.0 — Health Connect's floor (~99% of devices); was 24
|
|
17
|
-
targetSdkVersion
|
|
17
|
+
targetSdkVersion 36 // Play requires target API 36 (Android 16) from 31 Aug 2026
|
|
18
18
|
|
|
19
19
|
// Version Information
|
|
20
20
|
versionCode 2
|
package/runtime/app/main-page.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Application, AndroidApplication, EventData, Page, isAndroid, isIOS, knownFolders, path } from '@nativescript/core';
|
|
1
|
+
import { Application, AndroidApplication, Button, EventData, LoadEventData, Page, StackLayout, WebView, isAndroid, isIOS, knownFolders, path } from '@nativescript/core';
|
|
2
2
|
import { bridge } from './shell/bridge';
|
|
3
3
|
import { effectiveServerUrl } from './shell/server-url';
|
|
4
4
|
import { registerHandlers } from './shell/handlers';
|
|
@@ -126,6 +126,7 @@ export function onPageLoaded(args: EventData): void {
|
|
|
126
126
|
// if a future surface orphans a window with no dismiss callback — gate it on a "surface presented" flag.
|
|
127
127
|
void armNativeSurfaceRecovery; // referenced to keep the helper (still callable if a scoped need arises)
|
|
128
128
|
if (isAndroid) wireAndroidSafeArea(webView); // experimental edge-to-edge (no-op unless config on)
|
|
129
|
+
wireLoadFallback(page, webView); // loader:'server' failure → branded retry view (no white screen)
|
|
129
130
|
startEventForwarding();
|
|
130
131
|
loadBundle(webView);
|
|
131
132
|
// Env indicator banner: shown in the bottom safe area on relaunch when a non-default env override is
|
|
@@ -167,6 +168,58 @@ export function onPageLoaded(args: EventData): void {
|
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
/**
|
|
172
|
+
* loader:'server' load-failure fallback (App Review 2.1a). A server-loader shell has NO local UI —
|
|
173
|
+
* if the serverUrl is unreachable (offline, server down, DNS) the reviewer sees a bare white screen.
|
|
174
|
+
* NS core forwards BOTH webView:didFailNavigation: and didFailProvisionalNavigation: (iOS) and the
|
|
175
|
+
* main-frame onReceivedError (Android) into `loadFinishedEvent` with `args.error` — so one listener
|
|
176
|
+
* covers initial-load and in-app navigation failures on both platforms. On failure: show a simple
|
|
177
|
+
* branded "can't connect — retry" view + auto-retry with capped backoff; any successful load hides
|
|
178
|
+
* it and resets. iOS NSURLErrorCancelled ("cancelled", a superseded navigation) is NOT a failure.
|
|
179
|
+
* NOTE: web-process termination has no NS seam in a prod build (the didTerminate forwarder lives on
|
|
180
|
+
* the debug-only DevCertNavDelegate) — out of scope here.
|
|
181
|
+
*/
|
|
182
|
+
function wireLoadFallback(page: Page, webView: CustomWebView): void {
|
|
183
|
+
if (SHELL_CONFIG.loader !== 'server') return; // bundled loaders can't fail on network
|
|
184
|
+
const fallback = page.getViewById<StackLayout>('loadFallback');
|
|
185
|
+
const retryBtn = page.getViewById<Button>('loadRetryBtn');
|
|
186
|
+
if (!fallback || !retryBtn) return;
|
|
187
|
+
|
|
188
|
+
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
189
|
+
let backoffMs = 3000;
|
|
190
|
+
const clearTimer = () => { if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; } };
|
|
191
|
+
const retry = () => {
|
|
192
|
+
clearTimer();
|
|
193
|
+
appwrapNativeLog('[native:fallback] retrying server load');
|
|
194
|
+
// Android: loadBundle sets `src`, but re-setting the SAME value is a NS property no-op — drive
|
|
195
|
+
// the native reload directly. iOS's loadBundle path issues a fresh loadRequest, so reuse it.
|
|
196
|
+
const droid = isAndroid ? (webView.android as android.webkit.WebView | null) : null;
|
|
197
|
+
if (droid) droid.loadUrl(effectiveServerUrl());
|
|
198
|
+
else loadBundle(webView);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
webView.on(WebView.loadFinishedEvent, (args: LoadEventData) => {
|
|
202
|
+
const err = String(args.error ?? '');
|
|
203
|
+
if (!err) {
|
|
204
|
+
// successful load → hide + reset (idempotent; covers the auto-retry that finally lands)
|
|
205
|
+
clearTimer();
|
|
206
|
+
backoffMs = 3000;
|
|
207
|
+
if (fallback.visibility !== 'collapse') {
|
|
208
|
+
appwrapNativeLog('[native:fallback] load recovered — hiding fallback');
|
|
209
|
+
fallback.visibility = 'collapse';
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (/cancel/i.test(err)) return; // NSURLErrorCancelled: navigation superseded, not a failure
|
|
214
|
+
appwrapNativeLog(`[native:fallback] load failed: ${err}`);
|
|
215
|
+
fallback.visibility = 'visible';
|
|
216
|
+
if (!retryTimer) {
|
|
217
|
+
retryTimer = setTimeout(() => { backoffMs = Math.min(backoffMs * 2, 30_000); retry(); }, backoffMs);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
retryBtn.on(Button.tapEvent, retry);
|
|
221
|
+
}
|
|
222
|
+
|
|
170
223
|
function loadBundle(webView: CustomWebView): void {
|
|
171
224
|
const wwwPath = path.join(knownFolders.currentApp().path, 'www');
|
|
172
225
|
const entryPath = path.join(wwwPath, SHELL_CONFIG.entry);
|
|
@@ -6,5 +6,13 @@
|
|
|
6
6
|
backgroundColor="{{ backgroundColor }}">
|
|
7
7
|
<GridLayout rows="*">
|
|
8
8
|
<shell:CustomWebView id="webview" row="0" />
|
|
9
|
+
<!-- loader:'server' load-failure fallback (App Review 2.1a): shown by main-page.ts when the
|
|
10
|
+
server navigation fails (offline / server down) so the reviewer never sees a white screen. -->
|
|
11
|
+
<StackLayout id="loadFallback" row="0" visibility="collapse" backgroundColor="#ffffff" verticalAlignment="center" padding="32">
|
|
12
|
+
<Label text="Blank" fontSize="34" fontWeight="700" color="#000000" textAlignment="center" />
|
|
13
|
+
<Label text="Can't connect" fontSize="17" fontWeight="600" color="#333333" textAlignment="center" marginTop="18" />
|
|
14
|
+
<Label text="Check your internet connection — we'll keep trying." fontSize="14" color="#777777" textAlignment="center" textWrap="true" marginTop="6" />
|
|
15
|
+
<Button id="loadRetryBtn" text="Retry" fontSize="16" fontWeight="600" color="#ffffff" backgroundColor="#000000" borderRadius="22" height="44" width="160" marginTop="24" />
|
|
16
|
+
</StackLayout>
|
|
9
17
|
</GridLayout>
|
|
10
18
|
</Page>
|
|
@@ -127,6 +127,22 @@ class DevCertNavDelegate extends NSObject implements WKNavigationDelegate {
|
|
|
127
127
|
// EXPLICITLY — ObjC message forwarding (forwardingTargetForSelector) is unreliable for a @NativeClass,
|
|
128
128
|
// and a decision method (below) whose `decisionHandler` is never called STALLS the navigation → blank.
|
|
129
129
|
webViewDecidePolicyForNavigationActionDecisionHandler(webView: WKWebView, navigationAction: any, decisionHandler: (p: number) => void): void {
|
|
130
|
+
// External URL schemes (mailto:/tel:/app deeplinks/…) can't be rendered by the WKWebView —
|
|
131
|
+
// navigating to one in-place silently fails ("unsupported URL"). Mirror the Android shell's
|
|
132
|
+
// shouldOverrideUrlLoading: hand every non-web scheme to the OS and cancel the navigation.
|
|
133
|
+
// http/https/file/about/blob/data (+ empty) stay in-WebView.
|
|
134
|
+
try {
|
|
135
|
+
const url: NSURL | null = navigationAction?.request?.URL ?? null;
|
|
136
|
+
const scheme = url?.scheme?.toLowerCase?.() ?? '';
|
|
137
|
+
// 'app' is the shell's own WKURLSchemeHandler serving the bundled PWA (loader:'app') — in-WebView.
|
|
138
|
+
if (scheme && !['http', 'https', 'file', 'about', 'blob', 'data', 'javascript', 'app'].includes(scheme)) {
|
|
139
|
+
UIApplication.sharedApplication.openURLOptionsCompletionHandler(url!, NSDictionary.new() as any, null);
|
|
140
|
+
decisionHandler(0 /* WKNavigationActionPolicy.Cancel */);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.warn('AppWrap: external-scheme nav check failed', e);
|
|
145
|
+
}
|
|
130
146
|
if (this.fbResponds('webView:decidePolicyForNavigationAction:decisionHandler:')) {
|
|
131
147
|
(this.fallback as any).webViewDecidePolicyForNavigationActionDecisionHandler(webView, navigationAction, decisionHandler);
|
|
132
148
|
} else {
|
package/src/cli.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { createHash } from 'crypto';
|
|
|
13
13
|
import { cpSync, existsSync, mkdirSync, openSync, closeSync, readdirSync, readFileSync, readSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from 'fs';
|
|
14
14
|
import { builtinModules } from 'module';
|
|
15
15
|
import { networkInterfaces, tmpdir } from 'os';
|
|
16
|
-
import { basename, dirname, extname, join, resolve } from 'path';
|
|
16
|
+
import { basename, dirname, extname, join, relative, resolve } from 'path';
|
|
17
17
|
import { pathToFileURL } from 'url';
|
|
18
18
|
// PURE-DATA capability manifest (no NativeScript globals) — type-only import (erased at runtime);
|
|
19
19
|
// the VALUES are loaded dynamically below from the resolved runtime so the CLI works both in the
|
|
@@ -979,7 +979,9 @@ function stampIOSDisplayName(outDir: string, cfg: AppwrapConfig, req: NativeReqs
|
|
|
979
979
|
// and notifications fall back to a plain banner with the host app icon. native-kit's notification
|
|
980
980
|
// identity path (INSendMessageIntent → communication notification) needs this to render the sender.
|
|
981
981
|
if (cfg.iosEntitlements?.['com.apple.developer.usernotifications.communication']) {
|
|
982
|
-
|
|
982
|
+
// ONLY the intents the shell actually donates: INSendMessageIntent (communication-notification
|
|
983
|
+
// styling). Declaring INStartCallIntent in an app with no calling feature is an App Review flag.
|
|
984
|
+
extras.push(` <key>NSUserActivityTypes</key>\n <array>\n <string>INSendMessageIntent</string>\n </array>`);
|
|
983
985
|
}
|
|
984
986
|
if (extras.length) {
|
|
985
987
|
src = src.replace(
|
|
@@ -2009,6 +2011,50 @@ export function isFrameworkRepo(root: string): boolean {
|
|
|
2009
2011
|
return existsSync(join(root, 'packages/appwrap-cli/src/cli.ts'));
|
|
2010
2012
|
}
|
|
2011
2013
|
|
|
2014
|
+
/** Context the CI workflow templates are rendered against — derived from the repo shape + config.
|
|
2015
|
+
* - `subdir`: app dir relative to the git root ('' = app at repo root). Auto-detected from where the
|
|
2016
|
+
* appwrap config lives, so a monorepo gets path-scoped jobs + a PR paths filter for free.
|
|
2017
|
+
* - `tagPrefix`: release-trigger tag prefix. Monorepo default `<subdir>-v` (NOT `v`) so an unrelated
|
|
2018
|
+
* repo `v*` tag can never cut a store release; overridable via config `ci.tagPrefix`.
|
|
2019
|
+
* - `webBuild`: false for loader:'server' wraps (no local PWA bundle) — web build steps drop out. */
|
|
2020
|
+
export interface CiRenderContext { subdir: string; tagPrefix: string; webBuild: boolean; }
|
|
2021
|
+
|
|
2022
|
+
/** Derive the render context for `cwd` (the app dir holding the appwrap config) inside `repoRoot`. */
|
|
2023
|
+
export function ciRenderContext(cwd: string, repoRoot: string, cfg: AppwrapConfig): CiRenderContext {
|
|
2024
|
+
const rel = relative(repoRoot, cwd);
|
|
2025
|
+
const subdir = rel === '' || rel === '.' ? '' : rel;
|
|
2026
|
+
// Default tag prefix uses the LAST path segment (apps/foo → foo-v): tags are flat names, not paths.
|
|
2027
|
+
const tagPrefix = cfg.ci?.tagPrefix ?? (subdir ? `${basename(subdir)}-v` : 'v');
|
|
2028
|
+
return { subdir, tagPrefix, webBuild: cfg.loader !== 'server' };
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/** Render a CI workflow template: substitute tokens and resolve `#@if:` directives.
|
|
2032
|
+
* Directives (never survive into output): a trailing ` #@if:<cond>` keeps that line only when the
|
|
2033
|
+
* condition holds; standalone `#@if:<cond>` … `#@end` lines gate a block (non-nesting). Conds:
|
|
2034
|
+
* `monorepo` (subdir set) / `root` / `web` (local PWA build) / `server`. Tokens: `__DIR__` →
|
|
2035
|
+
* `<subdir>/` or '' (path prefix), `__APP_DIR__` → subdir (working-directory), `__TAG_PREFIX__`. */
|
|
2036
|
+
export function renderCiWorkflow(src: string, ctx: CiRenderContext): string {
|
|
2037
|
+
const holds = (c: string): boolean =>
|
|
2038
|
+
c === 'monorepo' ? !!ctx.subdir : c === 'root' ? !ctx.subdir : c === 'web' ? ctx.webBuild : c === 'server' ? !ctx.webBuild
|
|
2039
|
+
: c === 'doc' ? false // template-authoring notes — never emitted
|
|
2040
|
+
: true;
|
|
2041
|
+
const out: string[] = [];
|
|
2042
|
+
let dropBlock = false;
|
|
2043
|
+
for (const line of src.split('\n')) {
|
|
2044
|
+
const t = line.trim();
|
|
2045
|
+
if (t.startsWith('#@if:')) { dropBlock = !holds(t.slice(5)); continue; }
|
|
2046
|
+
if (t === '#@end') { dropBlock = false; continue; }
|
|
2047
|
+
if (dropBlock) continue;
|
|
2048
|
+
const m = line.match(/^(.*?)\s*#@if:([a-z]+)$/);
|
|
2049
|
+
if (m) { if (holds(m[2])) out.push(m[1]); }
|
|
2050
|
+
else out.push(line);
|
|
2051
|
+
}
|
|
2052
|
+
return out.join('\n')
|
|
2053
|
+
.replaceAll('__DIR__', ctx.subdir ? `${ctx.subdir}/` : '')
|
|
2054
|
+
.replaceAll('__APP_DIR__', ctx.subdir)
|
|
2055
|
+
.replaceAll('__TAG_PREFIX__', ctx.tagPrefix);
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2012
2058
|
/** Emit CI scaffolding (GH Actions → git repo root ONLY with --ci, fastlane → native/).
|
|
2013
2059
|
* Writing `.github/workflows` into a consumer repo is a repo-level side effect (it can shadow or
|
|
2014
2060
|
* clash with the project's own CI), so it is OPT-IN via `appwrap init --ci` — never implicit.
|
|
@@ -2027,19 +2073,28 @@ function copyCiTemplates(cwd: string, outDir: string, cfg: AppwrapConfig, force
|
|
|
2027
2073
|
const scaffoldWorkflows = withWorkflows && !isFrameworkRepo(repoRoot);
|
|
2028
2074
|
if (withWorkflows && !scaffoldWorkflows) {
|
|
2029
2075
|
console.log(' ci ← GH Actions scaffold skipped (inside the appwrap framework repo — manages its own CI)');
|
|
2030
|
-
} else if (scaffoldWorkflows) {
|
|
2031
|
-
targets.unshift([join(CI_TEMPLATE_DIR, 'github/workflows'), join(repoRoot, '.github/workflows'), false]);
|
|
2032
2076
|
}
|
|
2033
2077
|
for (const [from, to, overwrite] of targets) {
|
|
2034
2078
|
mkdirSync(to, { recursive: true });
|
|
2035
2079
|
cpSync(from, to, { recursive: true, force: overwrite, errorOnExist: false });
|
|
2036
2080
|
}
|
|
2037
|
-
//
|
|
2038
|
-
//
|
|
2039
|
-
//
|
|
2081
|
+
// Workflows are RENDERED per-repo-shape (not raw-copied): monorepo path scoping + tag prefix +
|
|
2082
|
+
// loader-aware web steps (see renderCiWorkflow), and the `bunx @livx.cc/appwrap@^x.y.z` pin is
|
|
2083
|
+
// stamped to THIS CLI's version floor so a CI run can't silently resolve an older published build
|
|
2084
|
+
// that lacks the `init`/`release` commands (it fails loudly instead). Never overwritten (users may
|
|
2085
|
+
// customize); re-init on an existing file is a no-op.
|
|
2040
2086
|
if (scaffoldWorkflows) {
|
|
2041
|
-
const
|
|
2042
|
-
|
|
2087
|
+
const ctx = ciRenderContext(cwd, repoRoot, cfg);
|
|
2088
|
+
const wfSrcDir = join(CI_TEMPLATE_DIR, 'github/workflows');
|
|
2089
|
+
const wfDir = join(repoRoot, '.github/workflows');
|
|
2090
|
+
mkdirSync(wfDir, { recursive: true });
|
|
2091
|
+
for (const file of readdirSync(wfSrcDir)) {
|
|
2092
|
+
const dest = join(wfDir, file);
|
|
2093
|
+
if (existsSync(dest)) continue;
|
|
2094
|
+
const rendered = renderCiWorkflow(readFileSync(join(wfSrcDir, file), 'utf8'), ctx)
|
|
2095
|
+
.replaceAll('__APPWRAP_VERSION__', CLI_VERSION);
|
|
2096
|
+
writeFileSync(dest, rendered);
|
|
2097
|
+
}
|
|
2043
2098
|
}
|
|
2044
2099
|
// Stamp the app id + team into the emitted fastlane (signing needs them; the templates ship
|
|
2045
2100
|
// `__APP_ID__`/`__TEAM_ID__` placeholders). Idempotent: re-init finds no placeholders → no-op.
|
package/src/config.ts
CHANGED
|
@@ -249,6 +249,15 @@ export interface AppwrapConfig {
|
|
|
249
249
|
loader?: 'app' | 'file' | 'server';
|
|
250
250
|
/** Live URL loaded when loader === 'server'. Set via config or `appwrap dev --url <url>`. */
|
|
251
251
|
serverUrl?: string;
|
|
252
|
+
/** CI scaffolding options (`appwrap init --ci`). The emitted GH Actions workflows auto-adapt to the
|
|
253
|
+
* repo shape (job paths + PR path filter scoped to the app subdir when the config sits below the git
|
|
254
|
+
* root; web-build steps dropped for loader:'server'); this block only overrides the derived defaults. */
|
|
255
|
+
ci?: {
|
|
256
|
+
/** Release tag prefix the release workflows trigger on (`tags: ['<prefix>*']`). Default: `v` at
|
|
257
|
+
* the repo root, `<subdir>-v` in a monorepo — so a monorepo's generic `v*` tags can never cut a
|
|
258
|
+
* store release by accident. */
|
|
259
|
+
tagPrefix?: string;
|
|
260
|
+
};
|
|
252
261
|
/** Runtime env-switcher (loader:'server' apps). Lets a build re-point the WebView between declared
|
|
253
262
|
* environments (prod / lab / a preview URL) at runtime — via the native dev-menu "Switch Environment"
|
|
254
263
|
* action + a bottom env-indicator banner — surviving a cold start, with NO separate native build.
|
|
@@ -348,6 +357,10 @@ export interface AppwrapConfig {
|
|
|
348
357
|
* build finds MULTIPLE candidate profiles for an id and you pick one interactively — so you're not
|
|
349
358
|
* re-prompted. Usually unset: a single matching profile is selected automatically. */
|
|
350
359
|
signingProfiles?: Record<string, string>;
|
|
360
|
+
/** Store-lane block (listing metadata, screenshot dirs, submission answers) consumed by an EE host
|
|
361
|
+
* (`appwrap-ee store bootstrap|push|assets`). CE carries it OPAQUE — nothing in CE reads it; the
|
|
362
|
+
* key is registered only so `unknownConfigKeys` doesn't warn on configs that declare it. */
|
|
363
|
+
store?: Record<string, unknown>;
|
|
351
364
|
/** Pure-native escape hatch: a directory (relative to the PWA project) whose contents are copied
|
|
352
365
|
* OVER the generated wrapper after stamping — for legacy/custom native code the declarative config
|
|
353
366
|
* can't express. Default `'appwrap.overrides'`; applied only if it exists. */
|
|
@@ -434,10 +447,10 @@ export function defineConfig(config: AppwrapConfig): AppwrapConfig {
|
|
|
434
447
|
* with no error, then an App Store rejection). A loud warning turns that silent no-op into a signal.
|
|
435
448
|
*/
|
|
436
449
|
export const KNOWN_CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
437
|
-
'androidAppLinks', 'appBoundDomains', 'backendOrigin', 'backgroundAudio', 'backgroundColor', 'backgroundTasks', 'buildNumber', 'debug',
|
|
450
|
+
'androidAppLinks', 'appBoundDomains', 'backendOrigin', 'backgroundAudio', 'backgroundColor', 'backgroundTasks', 'buildNumber', 'ci', 'debug',
|
|
438
451
|
'debugLog', 'desktop', 'devMenu', 'edgeToEdge', 'entry', 'icon', 'id', 'iosKeyboardExtraLift', 'loader', 'modules', 'modulePacks', 'name',
|
|
439
452
|
'envSwitcher', 'iosEntitlements', 'neutralizeServiceWorker', 'oauthRedirectSchemes', 'openNewWindowsInBrowser', 'orientation', 'overrides', 'permissions',
|
|
440
|
-
'plugins', 'push', 'pwaDist', 'queryPackages', 'queryUrlSchemes', 'serverUrl', 'shareTarget', 'signing', 'signingProfiles', 'statusBarStyle',
|
|
453
|
+
'plugins', 'push', 'pwaDist', 'queryPackages', 'queryUrlSchemes', 'serverUrl', 'shareTarget', 'signing', 'signingProfiles', 'statusBarStyle', 'store',
|
|
441
454
|
'splashIcon', 'storekitConfig', 'targetedDevices', 'teamId', 'themeColor', 'trackingDomains', 'urlScheme',
|
|
442
455
|
'usesNonExemptEncryption', 'vendorPaths', 'version',
|
|
443
456
|
]);
|
|
@@ -1,29 +1,49 @@
|
|
|
1
|
-
# appwrap PR checks — emitted by `appwrap init`. Adjust build commands to your project.
|
|
1
|
+
# appwrap PR checks — emitted by `appwrap init --ci`. Adjust build commands to your project.
|
|
2
|
+
#@if:doc
|
|
3
|
+
# Template directives (processed at emit by renderCiWorkflow; none survive into the scaffolded file):
|
|
4
|
+
# `#@if:<cond>` trailing a line keeps it only when the condition holds; a standalone `#@if:<cond>`
|
|
5
|
+
# … `#@end` pair gates a block (non-nesting). Conds: monorepo/root (app dir vs repo root),
|
|
6
|
+
# web/server (loader), doc (never emitted — this note).
|
|
7
|
+
# Tokens: __DIR__ → `<subdir>/` or ``; __APP_DIR__ → subdir; __TAG_PREFIX__ → release tag prefix.
|
|
8
|
+
#@end
|
|
2
9
|
name: PR
|
|
3
10
|
on:
|
|
4
11
|
pull_request:
|
|
12
|
+
paths: #@if:monorepo
|
|
13
|
+
- '__DIR__**' #@if:monorepo
|
|
14
|
+
- '.github/workflows/appwrap-pr.yml' #@if:monorepo
|
|
15
|
+
workflow_dispatch: #@if:monorepo
|
|
5
16
|
|
|
6
17
|
jobs:
|
|
18
|
+
#@if:web
|
|
7
19
|
web:
|
|
8
20
|
runs-on: ubuntu-latest
|
|
9
21
|
steps:
|
|
10
22
|
- uses: actions/checkout@v5
|
|
11
23
|
- uses: oven-sh/setup-bun@v2
|
|
12
24
|
- run: bun install
|
|
25
|
+
working-directory: __APP_DIR__ #@if:monorepo
|
|
13
26
|
- run: bun test || echo "no tests"
|
|
27
|
+
working-directory: __APP_DIR__ #@if:monorepo
|
|
14
28
|
- run: bun run build # → dist/ (the PWA bundle the wrapper ships)
|
|
29
|
+
working-directory: __APP_DIR__ #@if:monorepo
|
|
30
|
+
#@end
|
|
15
31
|
|
|
16
32
|
ios-build:
|
|
17
33
|
runs-on: macos-15
|
|
18
|
-
needs: web
|
|
34
|
+
needs: web #@if:web
|
|
19
35
|
steps:
|
|
20
36
|
- uses: actions/checkout@v5
|
|
21
37
|
- uses: oven-sh/setup-bun@v2
|
|
22
38
|
- uses: actions/setup-node@v5
|
|
23
39
|
with: { node-version: 22 }
|
|
24
|
-
- run: bun install && bun run build
|
|
40
|
+
- run: bun install && bun run build #@if:web
|
|
41
|
+
- run: bun install # server-loader wrap — no local web build (the app loads the live URL) #@if:server
|
|
42
|
+
working-directory: __APP_DIR__ #@if:monorepo
|
|
25
43
|
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|
|
26
44
|
- run: npm i -g nativescript@9.0.6
|
|
27
|
-
|
|
28
|
-
-
|
|
45
|
+
# Pinned to the appwrap version that emitted this workflow (see release workflow header for why).
|
|
46
|
+
- run: bunx @livx.cc/appwrap@^__APPWRAP_VERSION__ init # native/ is generated (gitignored) — regenerate it fresh in CI
|
|
47
|
+
working-directory: __APP_DIR__ #@if:monorepo
|
|
48
|
+
- working-directory: __DIR__native
|
|
29
49
|
run: npm install && ns build ios --emulator
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
# appwrap Android release — tag push (
|
|
1
|
+
# appwrap Android release — tag push (__TAG_PREFIX__*) → Play internal track.
|
|
2
|
+
# Template directives (`#@if:` / `#@end` / `__TOKENS__`) are processed at emit — see appwrap-pr.yml header. #@if:doc
|
|
2
3
|
# Build is the appwrap CLI (`appwrap build android --release --aab` → signed AAB);
|
|
3
4
|
# upload is fastlane supply. Secrets contract (repo settings → Actions secrets):
|
|
4
5
|
# ANDROID_KEYSTORE_BASE64 upload keystore, base64-encoded
|
|
@@ -6,12 +7,14 @@
|
|
|
6
7
|
# ANDROID_KEY_ALIAS key alias
|
|
7
8
|
# ANDROID_KEY_ALIAS_PASSWORD key (alias) password
|
|
8
9
|
# PLAY_SERVICE_ACCOUNT_JSON Play Developer API service-account JSON (the app must already
|
|
9
|
-
# exist on Play with one prior manual upload before supply works)
|
|
10
|
+
# exist on Play with one prior manual upload before supply works).
|
|
11
|
+
# OPTIONAL — when unset the job stays green and ships the AAB
|
|
12
|
+
# artifact only (graceful degradation until the SA exists).
|
|
10
13
|
name: Release Android
|
|
11
14
|
on:
|
|
12
15
|
workflow_dispatch:
|
|
13
16
|
push:
|
|
14
|
-
tags: ['
|
|
17
|
+
tags: ['__TAG_PREFIX__*']
|
|
15
18
|
|
|
16
19
|
# Monotonic store build number (Android versionCode): computed in the shell below — GitHub Actions
|
|
17
20
|
# expressions have NO arithmetic operators, so `${{ github.run_number + 100 }}` is an INVALID workflow
|
|
@@ -37,35 +40,53 @@ jobs:
|
|
|
37
40
|
path: |
|
|
38
41
|
~/.gradle/caches
|
|
39
42
|
~/.gradle/wrapper
|
|
40
|
-
key: gradle-${{ runner.os }}-${{ hashFiles('
|
|
43
|
+
key: gradle-${{ runner.os }}-${{ hashFiles('__DIR__bun.lock', '__DIR__package.json') }}
|
|
41
44
|
restore-keys: gradle-${{ runner.os }}-
|
|
42
45
|
- uses: android-actions/setup-android@v3
|
|
43
46
|
- run: sudo apt-get update -qq && sudo apt-get install -y -qq imagemagick # appwrap icon-gen needs an image tool (no sips on Linux) — else the default NS icon ships
|
|
44
|
-
- run: bun install && bun run build
|
|
47
|
+
- run: bun install && bun run build # → dist/ (the PWA bundle the wrapper ships) #@if:web
|
|
48
|
+
- run: bun install # server-loader wrap — no local web build (the app loads the live URL) #@if:server
|
|
49
|
+
working-directory: __APP_DIR__ #@if:monorepo
|
|
45
50
|
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|
|
46
51
|
- run: bun add -g nativescript@9.0.6
|
|
47
|
-
|
|
48
|
-
-
|
|
52
|
+
# Pinned to the appwrap version that emitted this workflow (see the iOS release workflow for why).
|
|
53
|
+
- run: bunx @livx.cc/appwrap@^__APPWRAP_VERSION__ init # native/ is generated (gitignored) — regenerate fresh in CI
|
|
54
|
+
working-directory: __APP_DIR__ #@if:monorepo
|
|
55
|
+
- working-directory: __DIR__native
|
|
49
56
|
run: bun install
|
|
50
57
|
- name: Decode upload keystore
|
|
51
58
|
run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > "$RUNNER_TEMP/upload.keystore"
|
|
52
59
|
- name: Build signed AAB
|
|
60
|
+
working-directory: __APP_DIR__ #@if:monorepo
|
|
53
61
|
env:
|
|
54
62
|
GRADLE_OPTS: "-Dorg.gradle.caching=true -Dorg.gradle.parallel=true -Dorg.gradle.daemon=false"
|
|
55
63
|
APPWRAP_ANDROID_KEYSTORE: ${{ runner.temp }}/upload.keystore
|
|
56
64
|
APPWRAP_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
|
57
65
|
APPWRAP_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
|
58
66
|
APPWRAP_ANDROID_KEYSTORE_ALIAS_PASSWORD: ${{ secrets.ANDROID_KEY_ALIAS_PASSWORD }}
|
|
59
|
-
run: bunx @livx.cc/appwrap build android --release --aab
|
|
67
|
+
run: bunx @livx.cc/appwrap@^__APPWRAP_VERSION__ build android --release --aab
|
|
60
68
|
- uses: actions/upload-artifact@v4 # keep the AAB even if Play upload is skipped
|
|
61
69
|
with:
|
|
62
70
|
name: app-release-aab
|
|
63
|
-
path:
|
|
71
|
+
path: __DIR__native/platforms/android/app/build/outputs/bundle/release/app-release.aab
|
|
72
|
+
# ---- Play upload, gracefully gated on the service account ----
|
|
73
|
+
# Without PLAY_SERVICE_ACCOUNT_JSON (e.g. the SA isn't provisioned yet) the job stays GREEN and
|
|
74
|
+
# ships the AAB artifact only. secrets.* can't appear in a step `if:` — indirect through env.
|
|
75
|
+
- name: Check Play service account
|
|
76
|
+
id: play-sa
|
|
77
|
+
env:
|
|
78
|
+
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
|
|
79
|
+
run: |
|
|
80
|
+
if [ -n "$PLAY_SERVICE_ACCOUNT_JSON" ]; then echo "present=true" >> "$GITHUB_OUTPUT";
|
|
81
|
+
else echo "present=false" >> "$GITHUB_OUTPUT"; echo "::notice::PLAY_SERVICE_ACCOUNT_JSON not set — skipping Play upload, AAB artifact only"; fi
|
|
64
82
|
- uses: ruby/setup-ruby@v1
|
|
83
|
+
if: steps.play-sa.outputs.present == 'true'
|
|
65
84
|
with: { ruby-version: '3.3', bundler-cache: false }
|
|
66
85
|
- run: gem install fastlane -v 2.236.1
|
|
86
|
+
if: steps.play-sa.outputs.present == 'true'
|
|
67
87
|
- name: Upload to Play (internal)
|
|
68
|
-
|
|
88
|
+
if: steps.play-sa.outputs.present == 'true'
|
|
89
|
+
working-directory: __DIR__native
|
|
69
90
|
env:
|
|
70
91
|
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
|
|
71
92
|
run: fastlane android beta
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
# appwrap iOS → TestFlight via fastlane. On-demand (workflow_dispatch) or tag push (
|
|
1
|
+
# appwrap iOS → TestFlight via fastlane. On-demand (workflow_dispatch) or tag push (__TAG_PREFIX__*).
|
|
2
|
+
# Template directives (`#@if:` / `#@end` / `__TOKENS__`) are processed at emit — see appwrap-pr.yml header. #@if:doc
|
|
2
3
|
# Secrets contract (repo settings → Actions secrets):
|
|
3
4
|
# MATCH_GIT_URL private repo holding signing certs (fastlane match) — SSH form for the deploy key
|
|
4
5
|
# MATCH_DEPLOY_KEY read-only SSH deploy key on that certs repo (so CI can clone it)
|
|
@@ -12,7 +13,7 @@ name: Release iOS
|
|
|
12
13
|
on:
|
|
13
14
|
workflow_dispatch:
|
|
14
15
|
push:
|
|
15
|
-
tags: ['
|
|
16
|
+
tags: ['__TAG_PREFIX__*']
|
|
16
17
|
|
|
17
18
|
# Monotonic store build number (CFBundleVersion): computed in the shell below — GitHub Actions
|
|
18
19
|
# expressions have NO arithmetic operators, so `${{ github.run_number + 100 }}` is an INVALID workflow
|
|
@@ -33,7 +34,9 @@ jobs:
|
|
|
33
34
|
- uses: oven-sh/setup-bun@v2
|
|
34
35
|
- uses: actions/setup-node@v5
|
|
35
36
|
with: { node-version: 22 }
|
|
36
|
-
- run: bun install && bun run build
|
|
37
|
+
- run: bun install && bun run build #@if:web
|
|
38
|
+
- run: bun install # server-loader wrap — no local web build (the app loads the live URL) #@if:server
|
|
39
|
+
working-directory: __APP_DIR__ #@if:monorepo
|
|
37
40
|
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|
|
38
41
|
- run: npm i -g nativescript@9.0.6
|
|
39
42
|
# bunx is PINNED to the appwrap version that emitted this workflow (stamped at `init`). Pinning is
|
|
@@ -43,7 +46,8 @@ jobs:
|
|
|
43
46
|
# published to the @livx.cc registry. PUBLISH-ORDERING: publish the appwrap CLI BEFORE the first CI
|
|
44
47
|
# run that uses a freshly-emitted workflow.
|
|
45
48
|
- run: bunx @livx.cc/appwrap@^__APPWRAP_VERSION__ init # native/ is generated (gitignored) — regenerate it fresh in CI
|
|
46
|
-
|
|
49
|
+
working-directory: __APP_DIR__ #@if:monorepo
|
|
50
|
+
- working-directory: __DIR__native
|
|
47
51
|
run: npm install
|
|
48
52
|
- uses: ruby/setup-ruby@v1
|
|
49
53
|
with: { ruby-version: '3.3', bundler-cache: false }
|
|
@@ -58,6 +62,7 @@ jobs:
|
|
|
58
62
|
# Single source of truth: the SAME command a developer runs locally. It re-stamps native/ and
|
|
59
63
|
# delegates the full archive/sign/upload to the emitted fastlane lane — no fastlane logic here.
|
|
60
64
|
- name: Build & upload to TestFlight
|
|
65
|
+
working-directory: __APP_DIR__ #@if:monorepo
|
|
61
66
|
env:
|
|
62
67
|
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
|
|
63
68
|
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|