@troylar/flightdeck 2.0.1 → 2.1.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/CHANGELOG.md +25 -0
- package/README.md +488 -107
- package/bin/flightdeck.js +55 -21
- package/dist/flightdeck.html +1840 -256
- package/package.json +1 -1
package/bin/flightdeck.js
CHANGED
|
@@ -676,10 +676,13 @@ function badTimesWhy(b) {
|
|
|
676
676
|
(unusable here, or changed since the run started: R's own reason comes first), then every problem of the run file
|
|
677
677
|
and of that version. Sets r._unrunnable; returns the problems. */
|
|
678
678
|
function judgeRun(PF, r, pub) {
|
|
679
|
-
|
|
680
|
-
const
|
|
679
|
+
/* A test run's version is its stage (2.1): staged/<process>/<stage>/, whose proposed number is the run's version. */
|
|
680
|
+
const test = PF.run.isTest(r) && typeof r.folder === 'string' && PF.publish.STAGE.test(r.folder);
|
|
681
|
+
const A = PF.appjson, out = [], valid = typeof r.folder === 'string' && (test || !!PF.publish.versionOf(r.folder)) && /^[a-z0-9][a-z0-9-]{0,59}$/.test(r.process);
|
|
682
|
+
const dir = PF.run.pubDir(r);
|
|
681
683
|
if (pub && valid && !pub._readFailed) {
|
|
682
|
-
const
|
|
684
|
+
const info = pub.ok && test ? { folder: pub.info.folder, mapHash: pub.info.mapHash, version: pub.info.stage.proposed } : pub.info;
|
|
685
|
+
const why = pub.unusable ? pub.why : pub.ok ? PF.repo.runMismatch(r, info) : '';
|
|
683
686
|
if (why && !r._unrunnable) r._unrunnable = why;
|
|
684
687
|
if (!pub.ok) { const x = A.pubProblem(pub); out.push({ path: dir, kind: x.kind, why: x.why }); }
|
|
685
688
|
/* The version is fine; the run doesn't match it. Named at the run, so the version is never taken as unreadable. */
|
|
@@ -688,7 +691,7 @@ function judgeRun(PF, r, pub) {
|
|
|
688
691
|
return out.concat(runProblems(PF, r));
|
|
689
692
|
}
|
|
690
693
|
/* A version folder that isn't in the folder, as readVersion would find it: none of its files there. */
|
|
691
|
-
function missingVersion(PF, r) { const v = PF.publish.verify(r.folder, { 'map.md': null, 'types.json': null, 'publication.json': null }); v.folder = r.folder; v.process = r.process; return v; }
|
|
694
|
+
function missingVersion(PF, r) { const v = PF.publish.verify(r.folder, { 'map.md': null, 'types.json': null, 'publication.json': null }, null, PF.run.isTest(r) ? { stage: true } : null); v.folder = r.folder; v.process = r.process; return v; }
|
|
692
695
|
/* What's wrong with people.json as a people list, or '' (no file is fine: names are then null). */
|
|
693
696
|
function peopleWhy(p) {
|
|
694
697
|
if (!p || p.hash == null) return '';
|
|
@@ -727,8 +730,8 @@ function adapterProblems(adapter) {
|
|
|
727
730
|
part only (the plans are all read, since a plan's process is inside the file). */
|
|
728
731
|
function needs(req) {
|
|
729
732
|
const sub = (top) => top + (req.proc ? '/' + req.proc : '');
|
|
730
|
-
if (req.cmd === 'processes' || req.cmd === 'process') return [req.cmd === 'process' ? 'publications/' + req.proc : 'publications'];
|
|
731
|
-
if (req.cmd === 'runs') return [sub('runs'), 'proposals', sub('publications'), 'people.json'];
|
|
733
|
+
if (req.cmd === 'processes' || req.cmd === 'process') return [req.cmd === 'process' ? 'publications/' + req.proc : 'publications'].concat(req.stage ? ['staged/' + req.proc + '/' + req.stage] : []);
|
|
734
|
+
if (req.cmd === 'runs') return [sub('runs'), 'proposals', sub('publications'), 'people.json'].concat(req.tests ? [sub('staged')] : []);
|
|
732
735
|
return [sub('followups'), sub('runs'), 'people.json'].concat(req.proc ? [sub('publications'), 'proposals'] : []);
|
|
733
736
|
}
|
|
734
737
|
/* A view of the folder that holds only those paths (and the folders above them, listed to reach them). Anything else
|
|
@@ -739,7 +742,7 @@ function covers(prefixes) { return (p) => { p = p || ''; return p === '' || pref
|
|
|
739
742
|
/* What run and next depend on: the run's folder, its version once it's known, people.json, and that run's own follow-up
|
|
740
743
|
decisions (the files named <run id>.*, not the other runs' in the same folder). */
|
|
741
744
|
function runCovers(proc, id, r) {
|
|
742
|
-
const base = covers(['runs/' + proc + '/' + id, 'people.json'].concat(r && typeof r.folder === 'string' ? ['publications/' + r.process + '/' + r.folder] : []));
|
|
745
|
+
const base = covers(['runs/' + proc + '/' + id, 'people.json'].concat(r && typeof r.folder === 'string' ? [(r.test === true ? 'staged/' : 'publications/') + r.process + '/' + r.folder] : []));
|
|
743
746
|
return (p) => base(p) || p === 'followups' || p === 'followups/' + proc || p === 'followups/' + proc + '/' || String(p).startsWith('followups/' + proc + '/' + id + '.');
|
|
744
747
|
}
|
|
745
748
|
function dependsOn(fs, prefixes) {
|
|
@@ -814,14 +817,37 @@ async function readData(eng, adapter, req) {
|
|
|
814
817
|
if (!two) { d.processes = A.v1Processes(d.processes); d.runs = d.runs.map(A.v1Run).filter(Boolean); }
|
|
815
818
|
take(folderProblems(PF, d));
|
|
816
819
|
/* Every run judged as run judges it, against its own version (runs reads the versions; followups doesn't need them). */
|
|
817
|
-
|
|
818
|
-
|
|
820
|
+
/* Test runs (2.1, flightdeck-read/2 only): left out of runs unless --tests asks for them (D3), and then judged against their
|
|
821
|
+
stage, read here as the app reads it. */
|
|
822
|
+
const stages = {};
|
|
823
|
+
if (two && req.cmd === 'runs' && req.tests) {
|
|
824
|
+
const want = {};
|
|
825
|
+
d.runs.forEach((r) => { if (PF.run.isTest(r) && PF.publish.STAGE.test(String(r.folder)) && PROC_ID.test(r.process)) want[r.process + '/' + r.folder] = r; });
|
|
826
|
+
for (const k of Object.keys(want).sort()) {
|
|
827
|
+
const r = want[k];
|
|
828
|
+
stages[k] = await repo.readStage(L.fs, r.process, r.folder).catch((e) => ({ ok: false, why: fileWhy(e && e.message || String(e)), folder: r.folder, process: r.process, _readFailed: true }));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const pubOf = (r) => {
|
|
832
|
+
if (PF.run.isTest(r)) return stages[r.process + '/' + r.folder] || null;
|
|
833
|
+
const proc = d.processes.filter((x) => x.process === r.process)[0], p = proc && proc.pubs.filter((x) => x.folder === r.folder)[0]; return p || missingVersion(PF, r);
|
|
834
|
+
};
|
|
835
|
+
const tested = (r) => !two || !!req.tests || !PF.run.isTest(r);
|
|
836
|
+
d.runs.forEach((r) => { if (req.cmd !== 'runs' || tested(r)) take(judgeRun(PF, r, req.cmd === 'runs' ? pubOf(r) : null)); });
|
|
819
837
|
const nameOf = A.names(d.people), runs = d.runs.filter((r) => !newerRun(PF, r)), all = (list) => list;
|
|
820
838
|
const known = (p) => d.processes.some((x) => x.process === p) || d.runs.some((r) => r.process === p) || d.plans.some((x) => x.process === p);
|
|
821
|
-
if (req.proc && !known(req.proc)) return notFound('There\'s no process ' + req.proc + ' in the team folder.');
|
|
839
|
+
if (req.proc && !known(req.proc) && !req.stage) return notFound('There\'s no process ' + req.proc + ' in the team folder.');
|
|
822
840
|
if (req.cmd === 'processes') return done(all, A.processes(d.processes), d.readAt);
|
|
823
841
|
if (req.cmd === 'process') {
|
|
824
842
|
const proc = d.processes.filter((x) => x.process === req.proc)[0];
|
|
843
|
+
/* A staged version (2.1, --stage): its checklist as a test run would run it, read by the stage rules. */
|
|
844
|
+
if (req.stage) {
|
|
845
|
+
const st = await repo.readStage(L.fs, req.proc, req.stage).catch((e) => ({ ok: false, why: fileWhy(e && e.message || String(e)), _readFailed: true }));
|
|
846
|
+
if (!st.ok && !st.info) return notFound('There\'s no staged version ' + req.stage + ' of ' + req.proc + ' in the team folder' + (st.why ? ' (' + st.why + ')' : '') + '.');
|
|
847
|
+
if (!st.ok) take([{ path: 'staged/' + req.proc + '/' + req.stage + '/', kind: A.pubProblem(st).kind, why: st.why }]);
|
|
848
|
+
st.stage = true; st.folder = req.stage;
|
|
849
|
+
return done(all, A.process2(proc || { process: req.proc, pubs: [], index: { latest: null, clashes: [] } }, st), d.readAt);
|
|
850
|
+
}
|
|
825
851
|
if (!proc || !proc.pubs.length) return notFound('There\'s no published checklist for ' + req.proc + ' in the team folder.');
|
|
826
852
|
let pub = null;
|
|
827
853
|
const vOf = (p) => (p.info && typeof p.info.version === 'number' ? p.info.version : PF.publish.versionOf(p.folder));
|
|
@@ -833,16 +859,17 @@ async function readData(eng, adapter, req) {
|
|
|
833
859
|
}
|
|
834
860
|
if (req.cmd === 'runs') {
|
|
835
861
|
const st = req.status || 'all', mine = (p) => !req.proc || p === req.proc;
|
|
836
|
-
const
|
|
862
|
+
const matching = runs.filter((r) => mine(r.process) && (st === 'all' || (st === 'running' && !r.ended) || (st === 'ended' && !!r.ended)));
|
|
863
|
+
const list = matching.filter(tested), leftOut = two ? matching.length - list.length : 0;
|
|
837
864
|
const plans = st === 'all' || st === 'planned' ? d.plans.filter((p) => mine(p.process)) : [];
|
|
838
865
|
/* Which version a plan starts on is unknown (null), not missing, when any of its process's versions couldn't be read or
|
|
839
866
|
verified: only problems with the versions themselves count (a run that doesn't match its version is named at the run). */
|
|
840
867
|
const probs = ledger(), unsure = (proc) => probs.some((x) => ['', 'publications', 'publications/'].indexOf(x.path) >= 0 || x.path === 'publications/' + proc || x.path.indexOf('publications/' + proc + '/') === 0);
|
|
841
|
-
return done(all,
|
|
868
|
+
return done(all, two ? A.runs2(list, plans, nameOf, d.processes, unsure, leftOut) : A.runs(list, plans, nameOf, d.processes, unsure), d.readAt);
|
|
842
869
|
}
|
|
843
870
|
/* followups: every run's follow-ups come from its run.json, so any run that couldn't be read may hide some. */
|
|
844
871
|
const items = F.list(runs.filter((r) => !req.proc || r.process === req.proc), d.fuDecisions).filter((x) => req.status !== 'open' || F.isOpen(x));
|
|
845
|
-
return done(all, A.followUps(items, nameOf), d.readAt);
|
|
872
|
+
return done(all, (two ? A.followUps2 : A.followUps)(items, nameOf), d.readAt);
|
|
846
873
|
} catch (e) {
|
|
847
874
|
return err(EXIT.error, 'failed', 'The team folder couldn\'t be read: ' + (e && e.message || String(e)));
|
|
848
875
|
}
|
|
@@ -850,7 +877,7 @@ async function readData(eng, adapter, req) {
|
|
|
850
877
|
|
|
851
878
|
/* The command line around readData: flags, the folder, the engine, and JSON out. Usage problems before any reading exit 1. */
|
|
852
879
|
async function readMain(argv) {
|
|
853
|
-
const VAL = ['--process', '--status', '--person', '--version', '--folder', '--contract'], val = {};
|
|
880
|
+
const VAL = ['--process', '--status', '--person', '--version', '--folder', '--contract', '--stage'], val = {};
|
|
854
881
|
const usage = (why) => ({ out: JSON.stringify(failureJson(null, 'usage', why), null, 2), code: EXIT.error });
|
|
855
882
|
const args = argv.filter((a, i) => !a.startsWith('--') && !(i > 0 && VAL.indexOf(argv[i - 1]) >= 0));
|
|
856
883
|
const cmd = args[0], asJson = argv.includes('--json');
|
|
@@ -861,13 +888,18 @@ async function readMain(argv) {
|
|
|
861
888
|
val[f] = argv[i + 1];
|
|
862
889
|
}
|
|
863
890
|
if (!asJson) fail(cmd + ' prints JSON for other apps. Add --json, for example: flightdeck ' + cmd + ' <team folder> --json');
|
|
864
|
-
const req = { cmd, proc: val['--process'] || null, status: val['--status'] || null, person: val['--person'] || null, version: val['--version'] || null, folder: val['--folder'] || null, contract: val['--contract'] || null };
|
|
891
|
+
const req = { cmd, proc: val['--process'] || null, status: val['--status'] || null, person: val['--person'] || null, version: val['--version'] || null, folder: val['--folder'] || null, contract: val['--contract'] || null, stage: val['--stage'] || null, tests: argv.includes('--tests') };
|
|
865
892
|
if (cmd === 'process') { req.proc = args[2]; if (!req.proc || !PROC_ID.test(req.proc)) return usage('say which process, for example: flightdeck process <team folder> release --json'); }
|
|
866
893
|
if (cmd === 'run' || cmd === 'next') { req.key = args[2]; if (!req.key || !RUN_KEY.test(req.key)) return usage('say which run as <process>/<run id>, for example: flightdeck ' + cmd + ' <team folder> release/k3m9p2q8 --json'); }
|
|
867
894
|
if (req.proc && !PROC_ID.test(req.proc)) return usage('a process ID is lowercase letters, numbers and dashes.');
|
|
868
895
|
if (req.version != null && !/^\d{1,6}$/.test(req.version)) return usage('--version is a version number, like 3.');
|
|
869
896
|
if (req.folder != null && !/^v\d+-[a-z0-9]+$/.test(req.folder)) return usage('--folder is a version folder, like v3-k3m9p2q8.');
|
|
870
897
|
if (req.contract != null && !/^\d{1,4}$/.test(req.contract)) return usage('--contract is a contract number, like 1.');
|
|
898
|
+
/* Test runs and stages (2.1) are flightdeck-read/2 only: contract 1 names a test run as newer, as 1.x did. */
|
|
899
|
+
if (req.tests && (cmd !== 'runs' || +req.contract !== 2)) return usage('--tests works with runs and --contract 2.');
|
|
900
|
+
if (req.stage != null && (cmd !== 'process' || +req.contract !== 2)) return usage('--stage works with process and --contract 2.');
|
|
901
|
+
if (req.stage != null && !/^stage-[a-z0-9]{8}$/.test(req.stage)) return usage('--stage is a staged version\'s folder, like stage-k3m9p2q8.');
|
|
902
|
+
if (req.stage != null && (req.version != null || req.folder != null)) return usage('--stage shows a staged version; leave out --version and --folder.');
|
|
871
903
|
const statuses = cmd === 'runs' ? ['running', 'planned', 'ended', 'all'] : cmd === 'followups' ? ['open', 'all'] : null;
|
|
872
904
|
if (req.status != null && (!statuses || statuses.indexOf(req.status) < 0)) return usage(statuses ? '--status is one of: ' + statuses.join(', ') + '.' : '--status works with runs and followups.');
|
|
873
905
|
let eng, dir;
|
|
@@ -1136,8 +1168,10 @@ async function reportData(eng, adapter, req, write) {
|
|
|
1136
1168
|
const d = await repo.loadAll(dependsOn(L.fs, scope), { readOnly: true, explain: true, deep: true });
|
|
1137
1169
|
take(folderProblems(PF, d));
|
|
1138
1170
|
const pubOf = (r) => { const proc = d.processes.filter((x) => x.process === r.process)[0], p = proc && proc.pubs.filter((x) => x.folder === r.folder)[0]; return p || missingVersion(PF, r); };
|
|
1139
|
-
|
|
1140
|
-
|
|
1171
|
+
/* A test run (2.1) is never a target: reports go to real runs. */
|
|
1172
|
+
const real = d.runs.filter((r) => !PF.run.isTest(r));
|
|
1173
|
+
real.forEach((r) => take(judgeRun(PF, r, pubOf(r))));
|
|
1174
|
+
let runs = real.filter((r) => !r._unrunnable && !r.ended && r.state === 'running' && (!req.proc || r.process === req.proc));
|
|
1141
1175
|
if (req.key) {
|
|
1142
1176
|
runs = runs.filter((r) => r.process + '/' + r.id === req.key);
|
|
1143
1177
|
if (!runs.length) return err(EXIT.notFound, 'not-found', 'There\'s no running run ' + req.key + ' in the team folder.');
|
|
@@ -1558,11 +1592,11 @@ const HELP = `flightdeck: team process checklists, in one HTML file
|
|
|
1558
1592
|
(this computer's time zone unless you say)
|
|
1559
1593
|
flightdeck processes <team folder> --json
|
|
1560
1594
|
for other apps: every process and its versions, as JSON
|
|
1561
|
-
flightdeck process <team folder> <process> [--version N | --folder v3-...] --json
|
|
1595
|
+
flightdeck process <team folder> <process> [--version N | --folder v3-... | --stage stage-...] --json
|
|
1562
1596
|
one version's phases, steps, gates and evidence (the latest usable one
|
|
1563
|
-
unless you say)
|
|
1564
|
-
flightdeck runs <team folder> [--process <id>] [--status running|planned|ended|all] --json
|
|
1565
|
-
runs and planned runs
|
|
1597
|
+
unless you say; --stage, with --contract 2: a staged version)
|
|
1598
|
+
flightdeck runs <team folder> [--process <id>] [--status running|planned|ended|all] [--tests] --json
|
|
1599
|
+
runs and planned runs (--tests, with --contract 2: test runs too)
|
|
1566
1600
|
flightdeck run <team folder> <process>/<run id> --json
|
|
1567
1601
|
one run: each step's state, who and when, its evidence, gates, follow-ups
|
|
1568
1602
|
flightdeck next <team folder> <process>/<run id> [--person <id>] --json
|