100 lines
6 KiB
TypeScript
100 lines
6 KiB
TypeScript
// storebound flow, dry runs: a step whose input (the IPA / AAB, the uploaded build) only a real earlier step makes is
|
|
// planned, not failed; a real run is untouched. The beta template: required VERSION, GROUP empty = the config's internal group.
|
|
import { afterAll, describe, expect, test } from 'bun:test';
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { type DryPending, evalIf, expand, flowsOfFile, missingParams, noteDry, planStep, plannedForDryInput, substitute, validateFlows } from '../cli/lib/flow';
|
|
import { parseYaml } from '../cli/lib/yaml';
|
|
import { readFileSync } from 'node:fs';
|
|
|
|
const ROOT = join(import.meta.dir, '..');
|
|
const CLI = join(ROOT, 'cli', 'index.ts');
|
|
const tmp = mkdtempSync(join(tmpdir(), 'storebound-flow-dry-'));
|
|
afterAll(() => rmSync(tmp, { recursive: true, force: true }));
|
|
const sb = (cwd: string, ...args: string[]) => Bun.spawnSync([process.execPath, CLI, ...args], { cwd, stdout: 'pipe', stderr: 'pipe', stdin: 'ignore' });
|
|
const C = { apple: true, play: true, exists: () => false };
|
|
|
|
const load = (name: string) => flowsOfFile(`templates/workflows/${name}.yml`, parseYaml(readFileSync(join(ROOT, 'templates', 'workflows', `${name}.yml`), 'utf8')), name).flows[0]!;
|
|
|
|
/** What the runner decides per step in a mode: 'ran' (storebound is called), 'planned' (dry input missing) or 'skipped'. */
|
|
function simulate(name: string, mode: 'dry' | 'apply', vars: Record<string, string>) {
|
|
const f = load(name);
|
|
const pending: DryPending = { built: new Set(), uploaded: new Set() };
|
|
const out: Record<string, { how: string; argv?: string[]; owner?: string }> = {};
|
|
for (const s of expand(f, new Map([[f.name, f]]), 'steps').steps) {
|
|
const p = planStep(s, 'steps', mode, vars, C);
|
|
if (p.skip) { out[s.id] = { how: 'skipped', argv: p.argv, owner: p.owner }; continue; }
|
|
const why = plannedForDryInput(p, mode, pending);
|
|
if (why) { out[s.id] = { how: 'planned', argv: p.argv }; continue; }
|
|
noteDry(p, pending);
|
|
out[s.id] = { how: 'ran', argv: p.argv };
|
|
}
|
|
return out;
|
|
}
|
|
|
|
describe('dry flows plan what a dry earlier step cannot produce', () => {
|
|
test('beta: dry build then upload (and wait, distribute) are planned, reads and checks still run', () => {
|
|
const r = simulate('beta', 'dry', { VERSION: '1.2.0', GROUP: '', BUILD: '7', 'config.testflight.internal': 'Team' });
|
|
expect(r['build']).toMatchObject({ how: 'ran', argv: ['apple', 'build', '--version', '1.2.0', '--build-number', '7', '--dry'] });
|
|
expect(r['upload']!.how).toBe('planned');
|
|
expect(r['wait']!.how).toBe('planned');
|
|
expect(r['distribute-internal']!.how).toBe('planned');
|
|
expect(r['play-build']!.how).toBe('ran');
|
|
expect(r['play-upload']!.how).toBe('planned');
|
|
expect(r['check']!.how).toBe('ran');
|
|
});
|
|
|
|
test('a real run is untouched: upload with no IPA still runs (and fails inside the command)', () => {
|
|
const r = simulate('beta', 'apply', { VERSION: '1.2.0', GROUP: '', BUILD: '7', 'config.testflight.internal': 'Team' });
|
|
expect(r['upload']).toMatchObject({ how: 'ran', argv: ['apple', 'upload', '--no-wait', '--build', '7', '--allow-existing'] });
|
|
expect(r['play-upload']!.how).toBe('ran');
|
|
});
|
|
|
|
test('an upload of a named file, or without a dry build before it, is not planned away', () => {
|
|
const pending: DryPending = { built: new Set(['apple']), uploaded: new Set() };
|
|
const up = planStep({ id: 'u', run: 'apple upload build/App.ipa' }, 'steps', 'dry', {}, C);
|
|
expect(plannedForDryInput(up, 'dry', pending)).toBeNull();
|
|
const fresh: DryPending = { built: new Set(), uploaded: new Set() };
|
|
expect(plannedForDryInput(planStep({ id: 'u', run: 'apple upload --no-wait' }, 'steps', 'dry', {}, C), 'dry', fresh)).toBeNull();
|
|
});
|
|
|
|
test('release: the macOS pkg is tracked apart from the iOS IPA', () => {
|
|
const r = simulate('release', 'dry', { VERSION: '2.0.0', MAC_TAURI: 'src-tauri', PLAY_TRACK: 'beta', BUILD: '9' });
|
|
expect(r['build']!.how).toBe('ran');
|
|
expect(r['upload']!.how).toBe('planned');
|
|
expect(r['mac-build']!.argv).toContain('--dry');
|
|
expect(r['mac-upload']!.how).toBe('planned');
|
|
expect(r['play-upload']!.argv).toEqual(['play', 'upload', '--track', 'beta', '--dry']);
|
|
const off = simulate('release', 'dry', { VERSION: '2.0.0', MAC_TAURI: '', PLAY_TRACK: 'internal', BUILD: '9' });
|
|
expect(off['mac-build']!.how).toBe('skipped');
|
|
expect(off['mac-upload']!.how).toBe('skipped');
|
|
expect(off['promote']!.owner).toContain('internal');
|
|
});
|
|
});
|
|
|
|
describe('beta template defaults', () => {
|
|
test('VERSION is required, GROUP defaults to empty and falls to the internal group of the config', () => {
|
|
const f = load('beta');
|
|
expect(f.params['VERSION']).toBe('?');
|
|
expect(f.params['GROUP']).toBe('');
|
|
expect(missingParams({ ...f.params })).toEqual(['VERSION']);
|
|
expect(missingParams({ ...f.params, VERSION: '1.0.0' })).toEqual([]);
|
|
const steps = expand(f, new Map([[f.name, f]]), 'steps').steps;
|
|
const by = (v: Record<string, string>) => steps.filter(s => s.id.startsWith('distribute') || s.id === 'no-group')
|
|
.filter(s => evalIf(substitute(s.if ?? '', v), C)).map(s => s.id);
|
|
expect(by({ GROUP: '', 'config.testflight.internal': 'Team' })).toEqual(['distribute-internal']);
|
|
expect(by({ GROUP: 'QA', 'config.testflight.internal': 'Team' })).toEqual(['distribute']);
|
|
expect(by({ GROUP: '', 'config.testflight.internal': '' })).toEqual(['no-group']);
|
|
expect(validateFlows([f, load('release')])).toEqual([]);
|
|
});
|
|
|
|
test('flow run beta without VERSION fails early with a clear message', () => {
|
|
const dir = join(tmp, 'req');
|
|
mkdirSync(join(dir, 'store', 'workflows'), { recursive: true });
|
|
writeFileSync(join(dir, 'store', 'workflows', 'beta.yml'), readFileSync(join(ROOT, 'templates', 'workflows', 'beta.yml'), 'utf8'));
|
|
const p = sb(dir, 'flow', 'run', 'beta');
|
|
expect(p.exitCode).not.toBe(0);
|
|
expect(p.stderr.toString() + p.stdout.toString()).toContain('beta needs VERSION=<value>');
|
|
});
|
|
});
|