@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/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
- const A = PF.appjson, out = [], valid = typeof r.folder === 'string' && !!PF.publish.versionOf(r.folder) && /^[a-z0-9][a-z0-9-]{0,59}$/.test(r.process);
680
- const dir = 'publications/' + r.process + '/' + r.folder + '/';
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 why = pub.unusable ? pub.why : pub.ok ? PF.repo.runMismatch(r, pub.info) : '';
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
- 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); };
818
- d.runs.forEach((r) => take(judgeRun(PF, r, req.cmd === 'runs' ? pubOf(r) : null)));
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 list = runs.filter((r) => mine(r.process) && (st === 'all' || (st === 'running' && !r.ended) || (st === 'ended' && !!r.ended)));
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, (two ? A.runs2 : A.runs)(list, plans, nameOf, d.processes, unsure), d.readAt);
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
- d.runs.forEach((r) => take(judgeRun(PF, r, pubOf(r))));
1140
- let runs = d.runs.filter((r) => !r._unrunnable && !r.ended && r.state === 'running' && (!req.proc || r.process === req.proc));
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