validate: macOS versions need Mac screenshots, Play reads survive an ended edit; macOS build numbers across platforms; Mac build prebuild + apple.macAppCheck; signing wording off the Mac; dry flows plan uploads
This commit is contained in:
parent
b1f1b206d3
commit
f5da7c9f7a
21 changed files with 519 additions and 57 deletions
|
|
@ -205,7 +205,7 @@ export function validateFlows(flows: Flow[], o: { commands?: Set<string> } = {})
|
|||
if (ids.has(s.id)) e.push(`${at}: id used twice`);
|
||||
ids.add(s.id);
|
||||
for (const t of [s.run, s.sh, s.owner, s.if].filter((x): x is string => typeof x === 'string'))
|
||||
for (const m of t.matchAll(/\$\{([^}]*)\}/g)) if (!vars.has(m[1]!) && !/^env\.[A-Z0-9_]+$/.test(m[1]!)) e.push(`${at}: \${${m[1]}} is not a param or an earlier capture`);
|
||||
for (const m of t.matchAll(/\$\{([^}]*)\}/g)) if (!vars.has(m[1]!) && !/^env\.[A-Z0-9_]+$/.test(m[1]!) && !CONFIG_VARS.includes(m[1]!)) e.push(`${at}: \${${m[1]}} is not a param or an earlier capture`);
|
||||
if (s.capture) vars.add(s.capture);
|
||||
if (s.run !== undefined) {
|
||||
let argv: string[] = [];
|
||||
|
|
@ -230,6 +230,14 @@ export function validateFlows(flows: Flow[], o: { commands?: Set<string> } = {})
|
|||
|
||||
// ---- variables, conditions ----
|
||||
|
||||
/** Variables the runner fills from the config (empty when the config has none), usable in `run` and `if`. */
|
||||
export const CONFIG_VARS = ['config.testflight.internal'];
|
||||
|
||||
/** The required-param marker: `VERSION: "?"` in params means the run needs VERSION=... (no silent default). */
|
||||
export const REQUIRED = '?';
|
||||
/** Params still at the required marker (not given on the command line). Pure. */
|
||||
export function missingParams(vars: Record<string, string>): string[] { return Object.keys(vars).filter(k => vars[k] === REQUIRED); }
|
||||
|
||||
/** ${VAR} and ${env.NAME} substituted; unknown ones stay as written (the plan shows them). */
|
||||
export function substitute(text: string, vars: Record<string, string>, env: Record<string, string | undefined> = {}): string {
|
||||
return text.replace(/\$\{([^}]*)\}/g, (m, k: string) => (k.startsWith('env.') ? env[k.slice(4)] ?? m : vars[k] ?? m));
|
||||
|
|
@ -306,6 +314,39 @@ export function planStep(s: FlowStep, part: PlannedStep['part'], mode: Mode, var
|
|||
return { ...base, kind, argv: addDry ? [...argv, '--dry'] : addYes ? [...argv, '--yes'] : argv, dryAdded: addDry, ...(addYes ? { yesAdded: true } : {}), ...(mode === 'plan' ? { skip: 'plan only' } : {}) };
|
||||
}
|
||||
|
||||
const PRODUCERS = ['apple build', 'play build'];
|
||||
const CONSUMERS = ['apple upload', 'play upload'];
|
||||
const AFTER_UPLOAD = ['apple testflight wait', 'apple testflight distribute'];
|
||||
|
||||
/** Stores whose artifact (IPA / AAB / PKG) or uploaded build only exists after a real earlier step ("apple", "apple:mac", "play"). */
|
||||
export interface DryPending { built: Set<string>; uploaded: Set<string> }
|
||||
|
||||
const storeKey = (argv: string[]): string => {
|
||||
const i = argv.indexOf('--platform');
|
||||
const mac = (i >= 0 && /^(macos|mac)$/i.test(argv[i + 1] ?? '')) || argv.includes('--tauri');
|
||||
return `${argv[0]}${mac ? ':mac' : ''}`;
|
||||
};
|
||||
|
||||
/** Records a step that ran as a dry run: a dry build leaves no IPA / AAB. Pure. */
|
||||
export function noteDry(p: PlannedStep, pending: DryPending): void {
|
||||
if (!p.argv || !p.dryAdded || p.skip) return;
|
||||
if (PRODUCERS.includes(words(p.argv).slice(0, 2).join(' '))) pending.built.add(storeKey(p.argv));
|
||||
}
|
||||
|
||||
/** Why a step is only planned in a dry run: its input (the IPA / AAB, or the uploaded build) would come from an earlier
|
||||
* step that did not really run. null = run it (always null with --apply: a real upload with no IPA still fails). Pure. */
|
||||
export function plannedForDryInput(p: PlannedStep, mode: Mode, pending: DryPending): string | null {
|
||||
if (mode === 'apply' || !p.argv || p.skip) return null;
|
||||
const w = words(p.argv);
|
||||
const k2 = w.slice(0, 2).join(' ');
|
||||
if (CONSUMERS.includes(k2) && !p.argv.some(a => /\.(ipa|aab|pkg)$/.test(a)) && pending.built.has(storeKey(p.argv))) {
|
||||
pending.uploaded.add(storeKey(p.argv));
|
||||
return `planned: would upload the ${k2 === 'play upload' ? 'AAB' : storeKey(p.argv).endsWith(':mac') ? 'PKG' : 'IPA'} the ${w[0]} build step writes (dry run: nothing built)`;
|
||||
}
|
||||
if (AFTER_UPLOAD.includes(w.slice(0, 3).join(' ')) && pending.uploaded.has('apple')) return 'planned: the build is not on App Store Connect in a dry run';
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Index of the step to start from: --from <id> (exact, or the first id under a sub-flow prefix). */
|
||||
export function startIndex(steps: FlowStep[], from: string | undefined): number {
|
||||
if (!from) return 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue