build.prebuild and a Capacitor web-asset staleness guard for apple build / play build
apple build and play build run build.prebuild (or apple.prebuild / play.prebuild) first, once per flow run; --no-prebuild skips it, --dry only prints it. A Capacitor project's copied public/index.html must then be newer than the last source commit and <webDir>/index.html, else the build stops (--allow-stale-web overrides). check warns when stale without a prebuild; the build output and dry plan show the web bundle's sha256 and mtime. The Capacitor project is also found in a monorepo (apps/native etc.); several ask for play.build.dir / apple.build.*. Pitfall 23.
This commit is contained in:
parent
f694af80eb
commit
5fbb9e63e3
14 changed files with 600 additions and 25 deletions
30
cli/flow.ts
30
cli/flow.ts
|
|
@ -10,16 +10,19 @@
|
|||
// the last run did not finish (.storebound/flows/<name>.json)
|
||||
// Progress: "[flow] 3/9 upload: storebound apple upload --no-wait --dry" before each step, the step's own output
|
||||
// indented, then ok / failed with the time. Exit 1 when a step failed (and had no continueOnError).
|
||||
// build.prebuild runs once per run: after a real apple build / play build step, its prebuild command goes to the later
|
||||
// steps in STOREBOUND_PREBUILD_DONE and they skip it.
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import type { Args } from './lib/args';
|
||||
import { CONFIG_NAMES, loadConfig } from './lib/config';
|
||||
import { CONFIG_NAMES, loadConfig, type ShipConfig } from './lib/config';
|
||||
import {
|
||||
type CondCtx, expand, type Flow, flowsOfFile, kindOf, type Mode, parseParams, planStep, type PlannedStep, resumePoint, type RunState, startIndex,
|
||||
type StepResult, validateFlows,
|
||||
} from './lib/flow';
|
||||
import { done, log, printJson, step, warn } from './lib/log';
|
||||
import { childEnv, pump, selfCommand } from './lib/runner';
|
||||
import { PREBUILD_DONE_ENV, prebuildFor, prebuildsDone } from './lib/web-assets';
|
||||
import { parseYaml } from './lib/yaml';
|
||||
|
||||
const TAG = 'flow';
|
||||
|
|
@ -68,10 +71,18 @@ function readState(root: string, name: string): RunState | null {
|
|||
try { return JSON.parse(readFileSync(statePath(root, name), 'utf8')) as RunState; } catch { return null; }
|
||||
}
|
||||
|
||||
async function condCtx(root: string): Promise<CondCtx> {
|
||||
let apple = false, play = false;
|
||||
try { const { config } = await loadConfig(undefined, root); apple = !!config.apple; play = !!config.play; } catch { /* no config: store conditions are false */ }
|
||||
return { apple, play, exists: p => existsSync(join(root, p)) };
|
||||
async function condCtx(root: string): Promise<CondCtx & { config: ShipConfig | null }> {
|
||||
let config: ShipConfig | null = null;
|
||||
try { config = (await loadConfig(undefined, root)).config; } catch { /* no config: store conditions are false */ }
|
||||
return { apple: !!config?.apple, play: !!config?.play, exists: p => existsSync(join(root, p)), config };
|
||||
}
|
||||
|
||||
/** The prebuild a finished build step ran (apple build / play build, for real, without --no-prebuild), else null:
|
||||
* later build steps of the same flow run skip that command (build.prebuild runs once per run, not per store). Pure. */
|
||||
export function prebuildRan(argv: string[], config: ShipConfig | null): string | null {
|
||||
if (!config || argv.includes('--dry') || argv.includes('--no-prebuild')) return null;
|
||||
const store = argv[0] === 'apple' && argv[1] === 'build' ? 'apple' : argv[0] === 'play' && argv[1] === 'build' ? 'play' : null;
|
||||
return store ? prebuildFor(config, store) ?? null : null;
|
||||
}
|
||||
|
||||
const KIND_LABEL: Record<string, string> = { read: 'read', write: 'write', wait: 'wait', owner: 'your click', shell: 'shell' };
|
||||
|
|
@ -83,9 +94,9 @@ function describe(p: PlannedStep): string {
|
|||
}
|
||||
|
||||
/** Runs one storebound step (or shell line) with stdin attached, so the command's own confirmation works; output indented. */
|
||||
async function exec(p: PlannedStep, root: string, quiet: boolean): Promise<{ code: number; last: string; out: string }> {
|
||||
async function exec(p: PlannedStep, root: string, quiet: boolean, extraEnv: Record<string, string> = {}): Promise<{ code: number; last: string; out: string }> {
|
||||
const cmd = p.shell !== undefined ? ['sh', '-c', p.shell] : [...selfCommand(), ...p.argv!];
|
||||
const proc = Bun.spawn(cmd, { cwd: root, stdin: 'inherit', stdout: 'pipe', stderr: 'pipe', env: childEnv() });
|
||||
const proc = Bun.spawn(cmd, { cwd: root, stdin: 'inherit', stdout: 'pipe', stderr: 'pipe', env: childEnv(extraEnv) });
|
||||
const lines: string[] = [];
|
||||
let last = '';
|
||||
const each = (line: string, kind: 'stdout' | 'stderr') => {
|
||||
|
|
@ -147,6 +158,7 @@ async function runFlow(args: Args, root: string): Promise<void> {
|
|||
const subParams = expand(flow, all, 'steps').params;
|
||||
const vars: Record<string, string> = { ...subParams, ...flow.params, ...(from ? last?.vars ?? {} : {}), ...params };
|
||||
const c = await condCtx(root);
|
||||
const prebuilt = prebuildsDone();
|
||||
const state: RunState = { flow: name, mode, startedAt: new Date().toISOString(), vars, results: [], ok: true };
|
||||
const env = process.env as Record<string, string | undefined>;
|
||||
const save = () => { if (mode === 'plan') return; mkdirSync(dirname(statePath(root, name)), { recursive: true }); writeFileSync(statePath(root, name), JSON.stringify(state, null, 2) + '\n'); };
|
||||
|
|
@ -174,10 +186,12 @@ async function runFlow(args: Args, root: string): Promise<void> {
|
|||
}
|
||||
if (!json) log(TAG, `${label}: ${describe(p)}${p.dryAdded ? ' (dry run)' : ''}`);
|
||||
const t = Date.now();
|
||||
const x = await exec(p, root, json);
|
||||
const x = await exec(p, root, json, prebuilt.size ? { [PREBUILD_DONE_ENV]: [...prebuilt].join('\n') } : {});
|
||||
r.exitCode = x.code; r.ms = Date.now() - t;
|
||||
if (x.code === 0) {
|
||||
r.status = 'ok';
|
||||
const pre = p.argv ? prebuildRan(p.argv, c.config) : null;
|
||||
if (pre) prebuilt.add(pre);
|
||||
if (p.capture) {
|
||||
if (/^[\w.:/@+-]{1,200}$/.test(x.last)) { vars[p.capture] = x.last; r.detail = `${p.capture}=${x.last}`; }
|
||||
else warn(TAG, `${s.id}: capture ${p.capture}: the last output line is not a single value ("${x.last.slice(0, 60)}")`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue