akm-cli 0.9.2-alpha.4 → 0.9.2
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 +493 -0
- package/STABILITY.md +23 -5
- package/dist/assets/hints/cli-hints-full.md +12 -7
- package/dist/assets/tasks/core/extract.yml +3 -5
- package/dist/assets/tasks/core/improve.yml +3 -5
- package/dist/assets/tasks/core/index-refresh.yml +3 -5
- package/dist/assets/tasks/core/sync.yml +3 -5
- package/dist/assets/tasks/core/version-check.yml +3 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
- package/dist/cli/unknown-flags.js +12 -1
- package/dist/cli.js +8 -1
- package/dist/commands/command/command-execution.js +23 -2
- package/dist/commands/health/improve-metrics.js +38 -0
- package/dist/commands/health/windows.js +8 -4
- package/dist/commands/health.js +8 -4
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/migrate-cli.js +130 -24
- package/dist/commands/proposal/validators/proposal-validators.js +7 -2
- package/dist/commands/tasks/explain.js +304 -0
- package/dist/commands/tasks/tasks-cli.js +185 -3
- package/dist/commands/tasks/tasks.js +265 -52
- package/dist/commands/workflow/plan.js +159 -0
- package/dist/commands/workflow-cli.js +94 -2
- package/dist/core/activation-policy.js +2 -12
- package/dist/core/adapter/adapters/akm-lint.js +7 -4
- package/dist/core/adapter/adapters/akm-metadata.js +26 -14
- package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
- package/dist/core/errors.js +45 -0
- package/dist/core/json-schema.js +15 -5
- package/dist/core/state/migrations.js +57 -0
- package/dist/core/state-db.js +16 -14
- package/dist/core/subprocess.js +47 -13
- package/dist/execution/guarded-source.js +44 -0
- package/dist/execution/input-contract.js +250 -0
- package/dist/execution/target-ref.js +63 -0
- package/dist/indexer/usage/usage-events.js +14 -3
- package/dist/integrations/agent/execution-lowering.js +12 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +12 -3
- package/dist/output/text/workflow-format.js +192 -10
- package/dist/output/text/workflow.js +2 -1
- package/dist/runtime.js +1 -0
- package/dist/scripts/akm-migrate-node.js +11838 -10118
- package/dist/scripts/akm-migrate.js +11828 -10117
- package/dist/setup/steps/tasks.js +34 -17
- package/dist/storage/repositories/task-history-repository.js +5 -1
- package/dist/storage/repositories/workflow-runs-repository.js +144 -6
- package/dist/tasks/backends/launchd.js +31 -84
- package/dist/tasks/embedded.js +13 -7
- package/dist/tasks/model/invocation.js +4 -0
- package/dist/tasks/prepare/prepare-script-target.js +9 -0
- package/dist/tasks/prepare/prepare-support.js +154 -0
- package/dist/tasks/prepare/prepare.js +117 -0
- package/dist/tasks/prepare/prepared-execution.js +4 -0
- package/dist/tasks/prepare/script-capture.js +80 -0
- package/dist/tasks/run/attempt-lifecycle.js +165 -0
- package/dist/tasks/run/load-task.js +117 -0
- package/dist/tasks/run/provenance.js +20 -0
- package/dist/tasks/run/run-command-task.js +92 -0
- package/dist/tasks/run/run-native-task.js +222 -0
- package/dist/tasks/run/run-task.js +99 -0
- package/dist/tasks/run/run-workflow-task.js +222 -0
- package/dist/tasks/run/task-history.js +134 -0
- package/dist/tasks/run/task-log.js +179 -0
- package/dist/tasks/run/task-result.js +19 -0
- package/dist/tasks/scheduler-binding.js +66 -2
- package/dist/tasks/scheduler-invocation.js +63 -3
- package/dist/tasks/scheduler-sync.js +77 -14
- package/dist/tasks/source/bounded-document.js +455 -0
- package/dist/tasks/source/parse-task-source.js +59 -0
- package/dist/tasks/source/project-v4.js +62 -0
- package/dist/tasks/source/task-input-diagnostics.js +36 -0
- package/dist/tasks/source/task-source-v4.js +626 -0
- package/dist/tasks/source-v3.js +10 -733
- package/dist/tasks/task-run-reserved-flags.js +79 -0
- package/dist/workflows/authoring/authoring.js +17 -8
- package/dist/workflows/exec/child-invocation.js +34 -0
- package/dist/workflows/exec/child-workflow.js +370 -0
- package/dist/workflows/exec/exec-unit.js +50 -170
- package/dist/workflows/exec/frozen-judge.js +19 -2
- package/dist/workflows/exec/native-executor.js +49 -27
- package/dist/workflows/exec/param-secrets.js +12 -0
- package/dist/workflows/exec/run-workflow.js +48 -59
- package/dist/workflows/exec/step-work.js +222 -80
- package/dist/workflows/exec/unit-dispatch.js +72 -0
- package/dist/workflows/freeze/child-output-references.js +94 -0
- package/dist/workflows/freeze/environment.js +174 -0
- package/dist/workflows/freeze/identity.js +22 -0
- package/dist/workflows/freeze/resolve-steps.js +78 -0
- package/dist/workflows/freeze/source-freeze.js +57 -0
- package/dist/workflows/freeze/step-values.js +68 -0
- package/dist/workflows/freeze/targets/child-workflow.js +206 -0
- package/dist/workflows/freeze/targets/command.js +81 -0
- package/dist/workflows/freeze/targets/script.js +57 -0
- package/dist/workflows/freeze/targets/shell.js +31 -0
- package/dist/workflows/freeze/targets/task.js +179 -0
- package/dist/workflows/freeze/task-bindings.js +180 -0
- package/dist/workflows/ir/compile.js +59 -11
- package/dist/workflows/ir/environment-v4.js +3 -3
- package/dist/workflows/ir/freeze-v4.js +41 -7
- package/dist/workflows/ir/params.js +58 -131
- package/dist/workflows/ir/plan-hash.js +3 -3
- package/dist/workflows/ir/schema-v4.js +246 -17
- package/dist/workflows/parser.js +74 -2
- package/dist/workflows/program/schema.js +5 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +24 -7
- package/dist/workflows/runtime/run-outputs.js +103 -0
- package/dist/workflows/runtime/runs.js +114 -9
- package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
- package/dist/workflows/source-files.js +5 -5
- package/dist/workflows/source-ir/compare.js +17 -0
- package/dist/workflows/source-ir/compile.js +7 -3
- package/dist/workflows/source-ir/github-yaml.js +64 -17
- package/dist/workflows/source-ir/schema.js +69 -21
- package/dist/workflows/source-ir/semantics.js +7 -25
- package/dist/workflows/source-ir/triggers.js +79 -0
- package/dist/workflows/source-ir/uses.js +33 -7
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/0.9.2.md +87 -11
- package/docs/migration/release-notes/README.md +3 -2
- package/docs/migration/v0.8-to-v0.9.md +13 -11
- package/docs/migration/v0.9.0-troubleshooting.md +20 -13
- package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
- package/docs/reference/README.md +1 -1
- package/docs/reference/cli.md +140 -46
- package/docs/reference/configuration.md +6 -5
- package/docs/reference/supported-formats.md +9 -5
- package/docs/reference/tasks.md +338 -75
- package/docs/reference/workflow-schema.md +290 -16
- package/docs/reference/workflows.md +57 -7
- package/package.json +1 -1
- package/schemas/akm-task.json +173 -118
- package/schemas/akm-workflow.json +28 -0
- package/dist/tasks/runner.js +0 -941
- package/dist/tasks/runtime-v3.js +0 -281
- package/dist/workflows/ir/source-freeze-v4.js +0 -506
- package/dist/workflows/source-ir/ordering.js +0 -38
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Declared-output resolution and the exported result (P3b, spec
|
|
6
|
+
* docs/plans/specs/p3b-child-executor.md §4.3, §4.4).
|
|
7
|
+
*
|
|
8
|
+
* Pure; no IO. `resolveWorkflowRunOutputs` reads PERSISTED step rows (never
|
|
9
|
+
* live in-memory evidence — `completeWorkflowStep` sees only the current
|
|
10
|
+
* step's; a resumed run has nothing else to rebuild the scope from, B-N12)
|
|
11
|
+
* and fails loudly, by name, when a declared output's source artifact was
|
|
12
|
+
* replaced by a truncation envelope at persistence.
|
|
13
|
+
*
|
|
14
|
+
* This module must stay IMPORT-CYCLE-FREE with `./runs.ts` (which imports
|
|
15
|
+
* it): `WORKFLOW_EVIDENCE_TRUNCATED_MARKER`'s value is therefore reproduced
|
|
16
|
+
* locally rather than imported — the same "reproduce across a boundary
|
|
17
|
+
* rather than import" idiom `ir/schema-v4.ts` already uses for
|
|
18
|
+
* `CHILD_WORKFLOW_DECODE_MAX_DEPTH`.
|
|
19
|
+
*/
|
|
20
|
+
import { validateJsonSchemaSubset } from "../../core/json-schema.js";
|
|
21
|
+
import { parseReference, resolveReferenceString } from "../program/expressions.js";
|
|
22
|
+
/** Mirrors `runs.ts`'s `WORKFLOW_EVIDENCE_TRUNCATED_MARKER` byte-for-byte — see this file's header for why it is reproduced, not imported. */
|
|
23
|
+
const EVIDENCE_TRUNCATED_MARKER = "__akm_evidence_truncated__";
|
|
24
|
+
function isTruncatedEvidenceValue(value) {
|
|
25
|
+
return (typeof value === "object" &&
|
|
26
|
+
value !== null &&
|
|
27
|
+
value[EVIDENCE_TRUNCATED_MARKER] === true);
|
|
28
|
+
}
|
|
29
|
+
/** Project a step artifact out of its persisted evidence — mirrors `exec/step-work.ts`'s `projectStepOutput`. */
|
|
30
|
+
function projectStepOutput(evidence) {
|
|
31
|
+
return Object.hasOwn(evidence, "output") ? evidence.output : evidence;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a plan's declared `outputs:` from a run's PERSISTED step rows, in
|
|
35
|
+
* declaration order (the frozen plan's `outputs` keys are already
|
|
36
|
+
* sorted-unique, `ir/schema-v4.ts`'s `decodeWorkflowOutputs`). Every failure
|
|
37
|
+
* mode is collected (not stopped-at-first) so a completion failure names
|
|
38
|
+
* every offending output at once.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveWorkflowRunOutputs(plan, steps) {
|
|
41
|
+
const stepOutputs = {};
|
|
42
|
+
for (const row of steps) {
|
|
43
|
+
if (!row.evidence_json)
|
|
44
|
+
continue;
|
|
45
|
+
let evidence;
|
|
46
|
+
try {
|
|
47
|
+
evidence = JSON.parse(row.evidence_json);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
stepOutputs[row.step_id] = projectStepOutput(evidence);
|
|
53
|
+
}
|
|
54
|
+
const scope = { params: {}, stepOutputs };
|
|
55
|
+
const errors = [];
|
|
56
|
+
const outputs = {};
|
|
57
|
+
for (const [name, declaration] of Object.entries(plan.outputs ?? {})) {
|
|
58
|
+
const parsed = parseReference(declaration.from);
|
|
59
|
+
if (!parsed.ok || parsed.expr.kind !== "stepOutput") {
|
|
60
|
+
errors.push(`output "${name}": "${declaration.from}" is not a valid step-output reference.`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const rootValue = Object.hasOwn(stepOutputs, parsed.expr.stepId) ? stepOutputs[parsed.expr.stepId] : undefined;
|
|
64
|
+
if (isTruncatedEvidenceValue(rootValue)) {
|
|
65
|
+
errors.push(`output "${name}" reads step "${parsed.expr.stepId}"'s artifact, which was truncated — it exceeded the ` +
|
|
66
|
+
`evidence persistence cap and was not stored.`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const resolved = resolveReferenceString(declaration.from, scope);
|
|
70
|
+
if (!resolved.ok) {
|
|
71
|
+
errors.push(`output "${name}": ${resolved.error.message}`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (isTruncatedEvidenceValue(resolved.value)) {
|
|
75
|
+
errors.push(`output "${name}" reads step "${parsed.expr.stepId}"'s artifact, which was truncated — it exceeded the ` +
|
|
76
|
+
`evidence persistence cap and was not stored.`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (declaration.schema) {
|
|
80
|
+
const schemaErrors = validateJsonSchemaSubset(resolved.value, declaration.schema);
|
|
81
|
+
if (schemaErrors.length > 0) {
|
|
82
|
+
for (const schemaError of schemaErrors)
|
|
83
|
+
errors.push(`output "${name}": ${schemaError}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
outputs[name] = resolved.value;
|
|
88
|
+
}
|
|
89
|
+
if (errors.length > 0)
|
|
90
|
+
return { ok: false, errors };
|
|
91
|
+
return { ok: true, outputs };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* What a completed run EXPORTS: the resolved declared outputs, or
|
|
95
|
+
* `{runId, status}` metadata when the plan declared none. The `{runId,
|
|
96
|
+
* status}` form is synthesized on read and never stored (row B-25).
|
|
97
|
+
*/
|
|
98
|
+
export function workflowRunExportedResult(row) {
|
|
99
|
+
if (row.outputs_json) {
|
|
100
|
+
return JSON.parse(row.outputs_json);
|
|
101
|
+
}
|
|
102
|
+
return { runId: row.id, status: row.status };
|
|
103
|
+
}
|
|
@@ -21,6 +21,7 @@ import { validateStepSummary } from "../validate-summary.js";
|
|
|
21
21
|
import { resolveAgentIdentity } from "./agent-identity.js";
|
|
22
22
|
import { evaluateCheckin } from "./checkin.js";
|
|
23
23
|
import { assertWorkflowSpineMatchesPlan, classifyWorkflowRunPlan, frozenStepRows, requireExecutableWorkflowPlan, } from "./plan-classifier.js";
|
|
24
|
+
import { resolveWorkflowRunOutputs } from "./run-outputs.js";
|
|
24
25
|
import { evaluateStaleUnits } from "./unit-checkin.js";
|
|
25
26
|
import { canonicalizeWorkflowRefInput, loadWorkflowAsset, resolveWorkflowEntryId } from "./workflow-asset-loader.js";
|
|
26
27
|
/**
|
|
@@ -176,7 +177,7 @@ export async function getWorkflowStatus(runId, opts) {
|
|
|
176
177
|
return withWorkflowRunsRepo((repo) => {
|
|
177
178
|
const run = readWorkflowRun(repo, runId);
|
|
178
179
|
const steps = readWorkflowRunSteps(repo, run.id);
|
|
179
|
-
const detail = buildWorkflowRunDetail(run, steps);
|
|
180
|
+
const detail = buildWorkflowRunDetail(repo, run, steps);
|
|
180
181
|
if (opts?.includeUnits) {
|
|
181
182
|
// The honest diagnostic surface (#22): read the unit journal straight and
|
|
182
183
|
// project each row, INCLUDING failures whose diagnostic text the
|
|
@@ -197,9 +198,16 @@ export async function hasWorkflowRun(runId) {
|
|
|
197
198
|
export async function listWorkflowRuns(input) {
|
|
198
199
|
const scopeKey = getCurrentWorkflowScopeKey();
|
|
199
200
|
const activeOnly = input?.activeOnly === true;
|
|
201
|
+
const includeChildren = input?.includeChildren === true;
|
|
200
202
|
if (input?.workflowRef === undefined) {
|
|
201
203
|
return withWorkflowRunsRepo((repo) => ({
|
|
202
|
-
runs: repo
|
|
204
|
+
runs: repo
|
|
205
|
+
.listRuns({
|
|
206
|
+
scopeKey,
|
|
207
|
+
...(activeOnly ? { activeOnly: true } : {}),
|
|
208
|
+
...(includeChildren ? { includeChildren: true } : {}),
|
|
209
|
+
})
|
|
210
|
+
.map(toWorkflowRunSummary),
|
|
203
211
|
}));
|
|
204
212
|
}
|
|
205
213
|
const exactRef = input.workflowRef.trim();
|
|
@@ -217,7 +225,7 @@ export async function listWorkflowRuns(input) {
|
|
|
217
225
|
}
|
|
218
226
|
catch (error) {
|
|
219
227
|
if (parsedExactRef.bundle !== undefined) {
|
|
220
|
-
const exactRows = await withWorkflowRunsRepo((repo) => repo.listRuns({ scopeKey, workflowRef: exactRef }));
|
|
228
|
+
const exactRows = await withWorkflowRunsRepo((repo) => repo.listRuns({ scopeKey, workflowRef: exactRef, ...(includeChildren ? { includeChildren: true } : {}) }));
|
|
221
229
|
if (exactRows.length === 0)
|
|
222
230
|
throw error;
|
|
223
231
|
return {
|
|
@@ -229,7 +237,12 @@ export async function listWorkflowRuns(input) {
|
|
|
229
237
|
}
|
|
230
238
|
return withWorkflowRunsRepo((repo) => ({
|
|
231
239
|
runs: repo
|
|
232
|
-
.listRuns({
|
|
240
|
+
.listRuns({
|
|
241
|
+
scopeKey,
|
|
242
|
+
workflowRefs,
|
|
243
|
+
...(activeOnly ? { activeOnly: true } : {}),
|
|
244
|
+
...(includeChildren ? { includeChildren: true } : {}),
|
|
245
|
+
})
|
|
233
246
|
.map(toWorkflowRunSummary),
|
|
234
247
|
}));
|
|
235
248
|
}
|
|
@@ -286,7 +299,7 @@ export async function resumeWorkflowRun(runId) {
|
|
|
286
299
|
throw new UsageError(`Workflow run ${run.id} is already completed and cannot be resumed.`);
|
|
287
300
|
}
|
|
288
301
|
if (run.status === "active") {
|
|
289
|
-
return buildWorkflowRunDetail(run, steps);
|
|
302
|
+
return buildWorkflowRunDetail(repo, run, steps);
|
|
290
303
|
}
|
|
291
304
|
// blocked or failed → flip back to active and re-open the current step so
|
|
292
305
|
// it can be reclassified (completed, failed, skipped) after resuming.
|
|
@@ -299,7 +312,7 @@ export async function resumeWorkflowRun(runId) {
|
|
|
299
312
|
});
|
|
300
313
|
const updated = { ...run, status: "active", updated_at: now };
|
|
301
314
|
const refreshedSteps = readWorkflowRunSteps(repo, run.id);
|
|
302
|
-
return buildWorkflowRunDetail(updated, refreshedSteps);
|
|
315
|
+
return buildWorkflowRunDetail(repo, updated, refreshedSteps);
|
|
303
316
|
});
|
|
304
317
|
}
|
|
305
318
|
/**
|
|
@@ -337,7 +350,7 @@ export async function abandonWorkflowRun(runId) {
|
|
|
337
350
|
checkin_armed_at: now,
|
|
338
351
|
};
|
|
339
352
|
const steps = readWorkflowRunSteps(repo, run.id);
|
|
340
|
-
const detail = buildWorkflowRunDetail(updated, steps);
|
|
353
|
+
const detail = buildWorkflowRunDetail(repo, updated, steps);
|
|
341
354
|
return detail;
|
|
342
355
|
});
|
|
343
356
|
}
|
|
@@ -575,6 +588,23 @@ export async function completeWorkflowStep(input) {
|
|
|
575
588
|
});
|
|
576
589
|
refreshedSteps = readWorkflowRunSteps(repo, run.id);
|
|
577
590
|
const state = deriveRunState(refreshedSteps);
|
|
591
|
+
// P3b (spec §4.3, B-N13): resolve + persist declared outputs INSIDE
|
|
592
|
+
// this same transaction, immediately after the run is known to have
|
|
593
|
+
// COMPLETED. A resolution failure throws here, and the transaction
|
|
594
|
+
// rolls back whole — the step completion included — so the observable
|
|
595
|
+
// outcome is fail-before-mutation: the step stays pending, the run
|
|
596
|
+
// stays active, and (since appendEvent runs outside this transaction)
|
|
597
|
+
// no event is appended.
|
|
598
|
+
let outputsJson; // undefined = untouched, keep the row's existing value
|
|
599
|
+
if (state.status === "completed" && plan.outputs) {
|
|
600
|
+
const resolved = resolveWorkflowRunOutputs(plan, refreshedSteps);
|
|
601
|
+
if (!resolved.ok) {
|
|
602
|
+
throw new UsageError(`Workflow run ${run.id} completed its final step but its declared outputs could not be resolved:\n` +
|
|
603
|
+
resolved.errors.map((e) => ` - ${e}`).join("\n"), "WORKFLOW_OUTPUT_INVALID");
|
|
604
|
+
}
|
|
605
|
+
outputsJson = JSON.stringify(resolved.outputs);
|
|
606
|
+
repo.setRunOutputs(run.id, outputsJson);
|
|
607
|
+
}
|
|
578
608
|
// Re-arm the check-in on every state change: a healthy, progressing run
|
|
579
609
|
// keeps pushing the stall window forward so the directive never fires.
|
|
580
610
|
repo.updateRunState({
|
|
@@ -592,9 +622,10 @@ export async function completeWorkflowStep(input) {
|
|
|
592
622
|
updated_at: completedAt,
|
|
593
623
|
completed_at: state.completedAt,
|
|
594
624
|
checkin_armed_at: completedAt,
|
|
625
|
+
...(outputsJson !== undefined ? { outputs_json: outputsJson } : {}),
|
|
595
626
|
};
|
|
596
627
|
});
|
|
597
|
-
const detail = buildWorkflowRunDetail(updatedRun, refreshedSteps);
|
|
628
|
+
const detail = buildWorkflowRunDetail(repo, updatedRun, refreshedSteps);
|
|
598
629
|
// #11: emit `workflow_step_completed` ONLY for a genuine `completed`
|
|
599
630
|
// transition; every other non-pending status (failed/skipped/blocked)
|
|
600
631
|
// carries the honest `workflow_step_updated` name. The status is ALWAYS
|
|
@@ -693,7 +724,7 @@ function readWorkflowRun(repo, runId) {
|
|
|
693
724
|
function readWorkflowRunSteps(repo, runId) {
|
|
694
725
|
return repo.getStepsForRun(runId);
|
|
695
726
|
}
|
|
696
|
-
function buildWorkflowRunDetail(run, steps) {
|
|
727
|
+
function buildWorkflowRunDetail(repo, run, steps) {
|
|
697
728
|
// Review M1: `workflow status` (and every other detail-shaped response) now
|
|
698
729
|
// evaluates the check-in, not just `workflow run`. Pure timestamp check —
|
|
699
730
|
// no background thread (see checkin.ts).
|
|
@@ -704,6 +735,7 @@ function buildWorkflowRunDetail(run, steps) {
|
|
|
704
735
|
agentHarness: run.agent_harness,
|
|
705
736
|
agentSessionId: run.agent_session_id,
|
|
706
737
|
});
|
|
738
|
+
const children = childRunTree(repo, run.id, run.id);
|
|
707
739
|
return {
|
|
708
740
|
run: toWorkflowRunSummary(run),
|
|
709
741
|
workflow: {
|
|
@@ -712,6 +744,73 @@ function buildWorkflowRunDetail(run, steps) {
|
|
|
712
744
|
steps: steps.map(toWorkflowRunStepState),
|
|
713
745
|
},
|
|
714
746
|
...(checkin ? { checkin } : {}),
|
|
747
|
+
...(children ? { children } : {}),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Build the parent-child status tree rooted at `rootRunId`, recursively, for
|
|
752
|
+
* whatever run's children are being listed (`forRunId`) — P3b, spec §4.5.
|
|
753
|
+
* `rootRunId` is threaded unchanged through the recursion, so every blocked
|
|
754
|
+
* node's `resume.then` names the SAME top-of-query run regardless of nesting
|
|
755
|
+
* depth: `akm workflow resume <rootRunId> && akm workflow run <rootRunId>` —
|
|
756
|
+
* never each node's own immediate parent, which the tree's caller has no
|
|
757
|
+
* command for.
|
|
758
|
+
*
|
|
759
|
+
* That command is sufficient to clear a block exactly ONE level deep (the
|
|
760
|
+
* root's own composing step blocked directly on this node) but NOT deeper
|
|
761
|
+
* (code-review round 4, finding 6 / Review log R6 — corrects a false claim
|
|
762
|
+
* this comment used to make here). Re-driving the root does **not** cascade
|
|
763
|
+
* back down through every intermediate composing step: `driveChildWorkflowUnit`
|
|
764
|
+
* (child-workflow.ts) never re-drives a child whose OWN status is `blocked`
|
|
765
|
+
* (row A-22) — no lease is even taken — so a re-drive just RE-OBSERVES the
|
|
766
|
+
* still-blocked status and re-propagates the block upward (an intermediate
|
|
767
|
+
* run is always blocked when a descendant is, row A-21, applied
|
|
768
|
+
* recursively), never reaching the deepest blocked node. Clearing a
|
|
769
|
+
* depth-2-or-deeper block requires resuming EVERY blocked run in the chain,
|
|
770
|
+
* deepest first, then re-running only the root — see "Recovering a blocked
|
|
771
|
+
* child" in docs/guides/run-workflows.md and "Blocked-child recovery" in
|
|
772
|
+
* docs/reference/workflow-schema.md for the worked multi-level sequence.
|
|
773
|
+
* `resume.then` is deliberately not widened to enumerate that chain (no
|
|
774
|
+
* envelope change, no new field) — the docs carry the multi-level sequence
|
|
775
|
+
* instead.
|
|
776
|
+
*
|
|
777
|
+
* Absent, never `[]`, when `forRunId` has no children (P3a's `childRunsOf`
|
|
778
|
+
* order: `created_at, id`).
|
|
779
|
+
*/
|
|
780
|
+
function childRunTree(repo, rootRunId, forRunId) {
|
|
781
|
+
const rows = repo.childRunsOf(forRunId);
|
|
782
|
+
if (rows.length === 0)
|
|
783
|
+
return undefined;
|
|
784
|
+
return rows.map((row) => toChildRunNode(repo, rootRunId, row));
|
|
785
|
+
}
|
|
786
|
+
function toChildRunNode(repo, rootRunId, row) {
|
|
787
|
+
const spawnedByUnitId = row.parent_unit_id ?? "";
|
|
788
|
+
// B-36: the parent STEP that spawned it, resolved via the real journaled
|
|
789
|
+
// unit row — null when that unit row is gone.
|
|
790
|
+
const stepId = row.parent_run_id && row.parent_unit_id
|
|
791
|
+
? (repo.getUnit(row.parent_run_id, row.parent_unit_id)?.step_id ?? null)
|
|
792
|
+
: null;
|
|
793
|
+
const children = childRunTree(repo, rootRunId, row.id);
|
|
794
|
+
return {
|
|
795
|
+
runId: row.id,
|
|
796
|
+
workflowRef: row.workflow_ref,
|
|
797
|
+
workflowTitle: row.workflow_title,
|
|
798
|
+
status: row.status,
|
|
799
|
+
spawnedByUnitId,
|
|
800
|
+
stepId,
|
|
801
|
+
currentStepId: row.current_step_id,
|
|
802
|
+
createdAt: row.created_at,
|
|
803
|
+
updatedAt: row.updated_at,
|
|
804
|
+
...(row.status === "blocked"
|
|
805
|
+
? {
|
|
806
|
+
resume: {
|
|
807
|
+
command: `akm workflow resume ${row.id}`,
|
|
808
|
+
// biome-ignore lint/suspicious/noThenProperty: mirrors spec §4.5's real WorkflowChildRunNode.resume.then field name
|
|
809
|
+
then: `akm workflow resume ${rootRunId} && akm workflow run ${rootRunId}`,
|
|
810
|
+
},
|
|
811
|
+
}
|
|
812
|
+
: {}),
|
|
813
|
+
...(children ? { children } : {}),
|
|
715
814
|
};
|
|
716
815
|
}
|
|
717
816
|
function toWorkflowRunSummary(run) {
|
|
@@ -738,6 +837,12 @@ function toWorkflowRunSummary(run) {
|
|
|
738
837
|
...(run.engine_lease_holder && run.engine_lease_until
|
|
739
838
|
? { engineLease: { holder: run.engine_lease_holder, until: run.engine_lease_until } }
|
|
740
839
|
: {}),
|
|
840
|
+
// P3b (spec §4.5): all three optional and conditionally spread, so every
|
|
841
|
+
// pre-existing (non-child, no-outputs-declared) run's envelope is
|
|
842
|
+
// byte-identical (Stable tier, rows B-27, B-45).
|
|
843
|
+
...(run.outputs_json ? { outputs: parseJsonObject(run.outputs_json) ?? {} } : {}),
|
|
844
|
+
...(run.parent_run_id ? { parentRunId: run.parent_run_id } : {}),
|
|
845
|
+
...(run.parent_unit_id ? { spawnedByUnitId: run.parent_unit_id } : {}),
|
|
741
846
|
};
|
|
742
847
|
}
|
|
743
848
|
/**
|
|
@@ -5,7 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
|
|
6
6
|
import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
7
7
|
import { loadConfig } from "../../core/config/config.js";
|
|
8
|
-
import { NotFoundError, UsageError } from "../../core/errors.js";
|
|
8
|
+
import { COMPOSITION_INVALID_MULTI_JOB_HINT, NotFoundError, UsageError } from "../../core/errors.js";
|
|
9
9
|
import { getDbPath } from "../../core/paths.js";
|
|
10
10
|
import { canonicalizeWorkflowName } from "../../core/recognition-util.js";
|
|
11
11
|
import { deriveInstallations } from "../../indexer/installations.js";
|
|
@@ -24,10 +24,10 @@ export function canonicalWorkflowRunRef(bundle, canonicalName) {
|
|
|
24
24
|
export function parseWorkflowRefInput(ref) {
|
|
25
25
|
const parsed = parseBundleRef(ref.trim());
|
|
26
26
|
if (parsed.fragment !== undefined) {
|
|
27
|
-
throw new UsageError(`Export fragment "#${parsed.fragment}" is not accepted in a workflow ref.`, "
|
|
27
|
+
throw new UsageError(`Export fragment "#${parsed.fragment}" is not accepted in a workflow ref.`, "TARGET_REF_INVALID");
|
|
28
28
|
}
|
|
29
29
|
if (parsed.conceptId.startsWith("workflow:")) {
|
|
30
|
-
throw new UsageError(`Invalid workflow ref "${ref.trim()}". Use [bundle//]conceptId, such as workflows/release.`, "
|
|
30
|
+
throw new UsageError(`Invalid workflow ref "${ref.trim()}". Use [bundle//]conceptId, such as workflows/release.`, "TARGET_REF_INVALID");
|
|
31
31
|
}
|
|
32
32
|
return parsed;
|
|
33
33
|
}
|
|
@@ -50,7 +50,7 @@ export async function loadWorkflowAsset(ref) {
|
|
|
50
50
|
return [{ source, bundleId }];
|
|
51
51
|
});
|
|
52
52
|
if (bundleRef.bundle && searchSources.length === 0) {
|
|
53
|
-
throw new UsageError(`Bundle "${bundleRef.bundle}" was not found among configured sources.`, "
|
|
53
|
+
throw new UsageError(`Bundle "${bundleRef.bundle}" was not found among configured sources.`, "WORKFLOW_SOURCE_INVALID");
|
|
54
54
|
}
|
|
55
55
|
let assetPath;
|
|
56
56
|
let sourcePath;
|
|
@@ -80,7 +80,7 @@ export async function loadWorkflowAsset(ref) {
|
|
|
80
80
|
if (rejectedSource) {
|
|
81
81
|
const sourceName = rejectedSource.bundleId;
|
|
82
82
|
const adapterId = rejectedSource.source.adapterId ?? "unassigned";
|
|
83
|
-
throw new UsageError(`Bundle "${sourceName}" uses adapter "${adapterId}", which does not support native workflow execution.`, "
|
|
83
|
+
throw new UsageError(`Bundle "${sourceName}" uses adapter "${adapterId}", which does not support native workflow execution.`, "WORKFLOW_SOURCE_INVALID");
|
|
84
84
|
}
|
|
85
85
|
throw new NotFoundError(`Workflow not found for ref: ${ref}`);
|
|
86
86
|
}
|
|
@@ -124,8 +124,16 @@ function compileWorkflowSourceFromDisk(assetPath, workspaceRoot) {
|
|
|
124
124
|
const content = fs.readFileSync(assetPath, "utf8");
|
|
125
125
|
const result = compileWorkflowSource(content, { path: assetPath, workspaceRoot });
|
|
126
126
|
if (!result.ok) {
|
|
127
|
+
// P4-N2's mapping (docs/plans/specs/p4-deletions-closeout.md §3.3.4): this
|
|
128
|
+
// is the FIRST compile of a workflow's own source that a ref resolves
|
|
129
|
+
// through (loadWorkflowAsset runs before compileResolveFreezeWorkflowV4),
|
|
130
|
+
// so it must apply the same code split as the freeze wrapper — otherwise
|
|
131
|
+
// row B-44's COMPOSITION_INVALID promise never reaches `startWorkflowRun`
|
|
132
|
+
// callers, which never get past this point on a multi-job source.
|
|
127
133
|
const details = result.errors.map((error) => ` ${error.path}:${error.line} — ${error.message}`).join("\n");
|
|
128
|
-
|
|
134
|
+
const isMultiJob = result.errors.length === 1 && result.errors[0]?.code === "multi-job-unsupported";
|
|
135
|
+
const code = isMultiJob ? "COMPOSITION_INVALID" : "WORKFLOW_SOURCE_INVALID";
|
|
136
|
+
throw new UsageError(`Workflow source has ${result.errors.length} error(s):\n${details}`, code, isMultiJob ? COMPOSITION_INVALID_MULTI_JOB_HINT : undefined);
|
|
129
137
|
}
|
|
130
138
|
return result.ir;
|
|
131
139
|
}
|
|
@@ -40,7 +40,7 @@ export class WorkflowSourceDomainError extends WorkflowSourceRejectionError {
|
|
|
40
40
|
const sortedCollisions = [...collidingSourcePaths].sort(comparePaths);
|
|
41
41
|
const code = issues.some((issue) => issue.code === "PATH_ESCAPE_VIOLATION")
|
|
42
42
|
? "PATH_ESCAPE_VIOLATION"
|
|
43
|
-
: "
|
|
43
|
+
: "WORKFLOW_SOURCE_INVALID";
|
|
44
44
|
const collisionDetail = sortedCollisions.length > 1 ? ` Valid owners also collide: ${sortedCollisions.join(", ")}.` : "";
|
|
45
45
|
super(`Workflow "${canonicalName}" has an invalid source ownership domain across candidates: ${sortedPaths.join(", ")}. ` +
|
|
46
46
|
`Problems: ${issues.map((issue) => issue.message).join(" ")}${collisionDetail} ` +
|
|
@@ -53,7 +53,7 @@ export class WorkflowSourceDomainError extends WorkflowSourceRejectionError {
|
|
|
53
53
|
export class WorkflowSourceIdentityError extends UsageError {
|
|
54
54
|
constructor(ref, indexedPath, authoritativePath) {
|
|
55
55
|
super(`Indexed workflow source identity for "${ref}" points to ${indexedPath}, but the authoritative source is ${authoritativePath}. ` +
|
|
56
|
-
"Refusing the stale index/cache identity; run `akm index --full` to reconcile it.", "
|
|
56
|
+
"Refusing the stale index/cache identity; run `akm index --full` to reconcile it.", "WORKFLOW_SOURCE_INVALID");
|
|
57
57
|
this.name = "WorkflowSourceIdentityError";
|
|
58
58
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
59
59
|
}
|
|
@@ -61,7 +61,7 @@ export class WorkflowSourceIdentityError extends UsageError {
|
|
|
61
61
|
export class WorkflowSourceNameError extends WorkflowSourceRejectionError {
|
|
62
62
|
constructor(sourcePath, nestedSuffix) {
|
|
63
63
|
super(`Workflow source filename ${sourcePath} has an extensionless stem ending in recognized workflow suffix "${nestedSuffix}". ` +
|
|
64
|
-
"Nested workflow suffixes are invalid; remove the inner .md or .yml suffix instead of relying on repeated stripping.", "
|
|
64
|
+
"Nested workflow suffixes are invalid; remove the inner .md or .yml suffix instead of relying on repeated stripping.", "WORKFLOW_SOURCE_INVALID", [sourcePath]);
|
|
65
65
|
this.name = "WorkflowSourceNameError";
|
|
66
66
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
67
67
|
}
|
|
@@ -69,14 +69,14 @@ export class WorkflowSourceNameError extends WorkflowSourceRejectionError {
|
|
|
69
69
|
export class WorkflowSourceLinkIdentityError extends WorkflowSourceRejectionError {
|
|
70
70
|
constructor(sourcePath, targetPath) {
|
|
71
71
|
super(`Workflow source ${sourcePath} resolves through a symlink to ${targetPath} with a different source format. ` +
|
|
72
|
-
"The authored workflow path and resolved source must use the same .md or .yml format.", "
|
|
72
|
+
"The authored workflow path and resolved source must use the same .md or .yml format.", "WORKFLOW_SOURCE_INVALID", [sourcePath]);
|
|
73
73
|
this.name = "WorkflowSourceLinkIdentityError";
|
|
74
74
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
export class WorkflowSourceLinkResolutionError extends WorkflowSourceRejectionError {
|
|
78
78
|
constructor(sourcePath) {
|
|
79
|
-
super(`Workflow source symlink ${sourcePath} cannot be resolved to a regular file.`, "
|
|
79
|
+
super(`Workflow source symlink ${sourcePath} cannot be resolved to a regular file.`, "WORKFLOW_SOURCE_INVALID", [
|
|
80
80
|
sourcePath,
|
|
81
81
|
]);
|
|
82
82
|
this.name = "WorkflowSourceLinkResolutionError";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Locale-independent code-point ordering, shared by the workflow source-IR
|
|
6
|
+
* lane (job `needs` canonicalization) and `akm lint` (name sorting).
|
|
7
|
+
*
|
|
8
|
+
* Split out of the deleted `source-ir/ordering.ts` (P4 §3.3, docs/plans/specs/
|
|
9
|
+
* p4-deletions-closeout.md): that file's other export,
|
|
10
|
+
* `canonicalTopologicalJobs`, existed only to order MULTIPLE ready jobs —
|
|
11
|
+
* moot once the adapter confines a workflow source to exactly one job. This
|
|
12
|
+
* comparator has an unrelated consumer (`src/commands/lint/index.ts`) and
|
|
13
|
+
* survives on its own.
|
|
14
|
+
*/
|
|
15
|
+
export function compareWorkflowSourceCodePoints(left, right) {
|
|
16
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
17
|
+
}
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { parseFrontmatterBlock } from "../../core/asset/frontmatter.js";
|
|
6
|
-
import { classifyTaskV3Triggers, classifyTaskV3Uses } from "../../tasks/source-v3.js";
|
|
7
6
|
import { parseWorkflow } from "../parser.js";
|
|
8
7
|
import { projectExecCore } from "../program/schema.js";
|
|
9
8
|
import { parseGithubWorkflowSource } from "./github-yaml.js";
|
|
10
9
|
import { sourceFailureResult, WorkflowSourceFailure } from "./result.js";
|
|
11
10
|
import { decodeWorkflowSourceIrV1, } from "./schema.js";
|
|
12
11
|
import { canonicalizeWorkflowWorkingDirectory, WorkflowSourceSemanticError } from "./semantics.js";
|
|
12
|
+
import { classifyWorkflowYamlTriggers } from "./triggers.js";
|
|
13
|
+
import { classifyWorkflowSourceUses } from "./uses.js";
|
|
13
14
|
export { looksLikeGithubWorkflowSource } from "./github-yaml.js";
|
|
14
15
|
export function compileGithubWorkflowSource(source, options) {
|
|
15
16
|
try {
|
|
@@ -17,8 +18,8 @@ export function compileGithubWorkflowSource(source, options) {
|
|
|
17
18
|
ok: true,
|
|
18
19
|
ir: decodeWorkflowSourceIrV1(parseGithubWorkflowSource(source, {
|
|
19
20
|
...options,
|
|
20
|
-
classifyUses: options.classifyUses ??
|
|
21
|
-
classifyTriggers: options.classifyTriggers ??
|
|
21
|
+
classifyUses: options.classifyUses ?? classifyWorkflowSourceUses,
|
|
22
|
+
classifyTriggers: options.classifyTriggers ?? classifyWorkflowYamlTriggers,
|
|
22
23
|
}), {
|
|
23
24
|
workspaceRoot: options.workspaceRoot,
|
|
24
25
|
}),
|
|
@@ -67,6 +68,9 @@ export function compileMarkdownWorkflowSource(source, options) {
|
|
|
67
68
|
...(parsed.document.description ? { description: parsed.document.description } : {}),
|
|
68
69
|
...(parsed.document.tags ? { tags: [...parsed.document.tags] } : {}),
|
|
69
70
|
...(parsed.document.params ? { params: jsonClone(parsed.document.params) } : {}),
|
|
71
|
+
...(parsed.document.outputs
|
|
72
|
+
? { outputs: jsonClone(parsed.document.outputs) }
|
|
73
|
+
: {}),
|
|
70
74
|
...(parsed.document.defaults
|
|
71
75
|
? { defaults: jsonClone(parsed.document.defaults) }
|
|
72
76
|
: {}),
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { isAlias, isMap, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
|
|
5
5
|
import { utf8Bytes, WORKFLOW_MAX_SOURCE_BYTES } from "../resource-limits.js";
|
|
6
|
-
import { canonicalTopologicalJobs } from "./ordering.js";
|
|
7
6
|
import { WorkflowSourceFailure } from "./result.js";
|
|
8
7
|
import { WORKFLOW_SOURCE_HOST_SHELLS, } from "./schema.js";
|
|
9
8
|
import { canonicalizeWorkflowCron, canonicalizeWorkflowRun, canonicalizeWorkflowWorkingDirectory, classifyWorkflowStepUses, validateWorkflowBuiltinCommand, WorkflowSourceSemanticError, } from "./semantics.js";
|
|
@@ -387,7 +386,7 @@ function verifyOwnerTriggerPlan(reader, root, onNode, triggers, options) {
|
|
|
387
386
|
binding.source === `on.schedule[${expected.ordinal}].cron`);
|
|
388
387
|
});
|
|
389
388
|
if (!matches) {
|
|
390
|
-
throw new WorkflowSourceFailure("trigger-classifier-drift", "The workflow trigger parser disagrees with the canonical
|
|
389
|
+
throw new WorkflowSourceFailure("trigger-classifier-drift", "The workflow trigger parser disagrees with the canonical workflow YAML trigger classifier.", reader.span(onNode));
|
|
391
390
|
}
|
|
392
391
|
}
|
|
393
392
|
function validateCron(reader, cron, node) {
|
|
@@ -398,19 +397,34 @@ function validateCron(reader, cron, node) {
|
|
|
398
397
|
semanticReaderFail(reader, cause, node);
|
|
399
398
|
}
|
|
400
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* The ONE place a job-count or job-dependency policy is enforced (P4 §3.3,
|
|
402
|
+
* docs/plans/specs/p4-deletions-closeout.md): AKM's YAML adapter accepts a
|
|
403
|
+
* familiar GitHub-step-shaped `name:`/`on:`/`jobs:` document, but requires
|
|
404
|
+
* exactly one job (brief §10) — it is an AKM workflow format executed by
|
|
405
|
+
* AKM's native engine, not a GitHub Actions graph. Job ordering, dependency
|
|
406
|
+
* validation and the 256-job bound all existed only to support MULTIPLE
|
|
407
|
+
* jobs; they are gone with the machinery, not relocated.
|
|
408
|
+
*/
|
|
401
409
|
function parseJobs(reader, node, options) {
|
|
402
410
|
const fields = reader.arbitraryFields(node, "workflow.jobs");
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
411
|
+
let first;
|
|
412
|
+
let second;
|
|
413
|
+
for (const entry of fields) {
|
|
414
|
+
if (!first)
|
|
415
|
+
first = entry;
|
|
416
|
+
else if (!second)
|
|
417
|
+
second = entry;
|
|
418
|
+
}
|
|
419
|
+
if (fields.size !== 1 || !first) {
|
|
420
|
+
reader.fail("multi-job-unsupported", `AKM workflow YAML requires exactly one job; this document declares ${fields.size}. AKM's YAML is an AKM workflow format executed by AKM's native engine, not GitHub Actions — split the jobs into separate workflows.`, second ? second[1].key : node);
|
|
421
|
+
}
|
|
422
|
+
const [id, pair] = first;
|
|
423
|
+
const job = parseJob(reader, id, pair, options);
|
|
424
|
+
if (job.needs.length > 0) {
|
|
425
|
+
reader.fail("multi-job-unsupported", `Job ${job.id} declares needs, but an AKM workflow has exactly one job; remove needs.`, pair.key);
|
|
426
|
+
}
|
|
427
|
+
return [job];
|
|
414
428
|
}
|
|
415
429
|
function parseJob(reader, id, pair, options) {
|
|
416
430
|
const node = pair.value;
|
|
@@ -450,9 +464,10 @@ function parseNeeds(reader, pair, jobId) {
|
|
|
450
464
|
for (const need of values)
|
|
451
465
|
if (!SOURCE_ID.test(need))
|
|
452
466
|
reader.fail("invalid-job-id", `Invalid needs id ${need}.`, pair.value);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
467
|
+
// Duplicate-entry checking (code duplicate-job-dependency) deleted with the
|
|
468
|
+
// rest of the multi-job dependency machinery (P4 §3.3): ANY non-empty
|
|
469
|
+
// needs — duplicated or not — is multi-job-unsupported at the caller
|
|
470
|
+
// (parseJobs), since a single-job workflow has nothing to depend on.
|
|
456
471
|
return values.sort();
|
|
457
472
|
}
|
|
458
473
|
function parseStep(reader, node, jobId, index, stepIds, options) {
|
|
@@ -487,7 +502,18 @@ function parseUsesStep(reader, usesPair, fields, options, common) {
|
|
|
487
502
|
}
|
|
488
503
|
const uses = reader.string(usesPair.value, "step.uses");
|
|
489
504
|
const target = classifyUses(reader, uses, usesPair.value, options.classifyUses ?? classifyWorkflowSourceUses);
|
|
490
|
-
|
|
505
|
+
// A-N3 (P2b, docs/plans/specs/p2b-input-bindings.md §1.7), widened in P3a
|
|
506
|
+
// (docs/plans/specs/p3a-plan-v5-child-freeze.md §4.2 step 7, row B-10) to
|
|
507
|
+
// ALSO cover a workflows/<ref> target: a tasks/<ref> or workflows/<ref>
|
|
508
|
+
// step's with: may bind any JSON value the composed target's declared
|
|
509
|
+
// input/param needs (an object/array literal, or a {from: "..."}
|
|
510
|
+
// reference) — decoding it through the scalar-only parseScalarMap would
|
|
511
|
+
// reject the very shapes both A-N3 and A-N8 exist to accept before
|
|
512
|
+
// decodeWorkflowSourceIrV1 (schema.ts) is ever reached. Every other target
|
|
513
|
+
// keeps the byte-identical scalar-only grammar.
|
|
514
|
+
const withValues = target.kind === "task" || target.kind === "workflow"
|
|
515
|
+
? parsePlainMap(reader, fields.get("with"), "step.with", INPUT_KEY)
|
|
516
|
+
: parseScalarMap(reader, fields.get("with"), "step.with", INPUT_KEY, true);
|
|
491
517
|
const commandMode = target.kind === "builtin-command"
|
|
492
518
|
? validateBuiltinCommand(reader, withValues, fields.get("with")?.value ?? usesPair.value)
|
|
493
519
|
: undefined;
|
|
@@ -560,6 +586,27 @@ function parseScalarMap(reader, pair, context, keyPattern, allowNull) {
|
|
|
560
586
|
}
|
|
561
587
|
return out;
|
|
562
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* Like {@link parseScalarMap} but accepts an arbitrary JSON value per key —
|
|
591
|
+
* a task-composition with: binding may be the declared input's own shape (an
|
|
592
|
+
* object/array literal, or a `{from: "..."}` reference), not just a scalar
|
|
593
|
+
* (A-N3). Depth/node bounds are already enforced document-wide by
|
|
594
|
+
* `checkTree`/`rejectAliases` before any field-level parsing runs, so this
|
|
595
|
+
* adds no new bound. `decodeWorkflowSourceIrV1` (schema.ts) decides what a
|
|
596
|
+
* declared input actually accepts.
|
|
597
|
+
*/
|
|
598
|
+
function parsePlainMap(reader, pair, context, keyPattern) {
|
|
599
|
+
if (!pair)
|
|
600
|
+
return undefined;
|
|
601
|
+
const fields = reader.arbitraryFields(pair.value, context);
|
|
602
|
+
const out = {};
|
|
603
|
+
for (const [key, valuePair] of fields) {
|
|
604
|
+
if (!keyPattern.test(key))
|
|
605
|
+
reader.fail("invalid-mapping-key", `${context} has invalid key ${JSON.stringify(key)}.`, valuePair.key);
|
|
606
|
+
out[key] = reader.plain(valuePair.value, `${context}.${key}`);
|
|
607
|
+
}
|
|
608
|
+
return out;
|
|
609
|
+
}
|
|
563
610
|
function wholeSourceSpan(source, filePath) {
|
|
564
611
|
return { path: filePath, start: 1, end: Math.max(1, source.split(/\r?\n/).length) };
|
|
565
612
|
}
|