akm-cli 0.9.3 → 0.9.4
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 +73 -1
- package/README.md +1 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/cli.js +5 -5
- package/dist/commands/health/improve-metrics.js +17 -0
- package/dist/commands/health/windows.js +2 -2
- package/dist/commands/health.js +2 -2
- package/dist/commands/improve/preparation.js +8 -1
- package/dist/commands/lint/index.js +3 -7
- package/dist/commands/tasks/tasks-cli.js +28 -3
- package/dist/commands/tasks/tasks.js +29 -1
- package/dist/core/adapter/adapters/akm-adapter.js +21 -14
- package/dist/core/adapter/adapters/akm-lint.js +3 -2
- package/dist/core/adapter/adapters/akm-task-adapter.js +9 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +13 -11
- package/dist/core/adapter/recognize-match.js +1 -20
- package/dist/core/asset/asset-placement.js +21 -2
- package/dist/core/common.js +21 -1
- package/dist/indexer/lookup/adapter-concept-owner.js +6 -89
- package/dist/indexer/search/db-search.js +6 -0
- package/dist/indexer/walk/matchers.js +0 -22
- package/dist/output/shapes/helpers.js +19 -1
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/output/text/command-format.js +4 -0
- package/dist/registry/pinned-request-helper.js +2 -2
- package/dist/registry/pinned-transport.js +6 -6
- package/dist/scripts/akm-migrate-node.js +12624 -12555
- package/dist/scripts/akm-migrate.js +12624 -12555
- package/dist/storage/repositories/proposals-repository.js +33 -1
- package/dist/storage/repositories/task-history-repository.js +22 -10
- package/dist/tasks/run/task-history.js +23 -3
- package/dist/tasks/scheduler-sync-preview.js +43 -0
- package/dist/tasks/scheduler-sync.js +1 -0
- package/dist/tasks/source/bounded-document.js +1 -1
- package/dist/tasks/source/parse-task-source.js +77 -11
- package/dist/tasks/source/task-source-v3-frozen.js +428 -0
- package/dist/tasks/source/task-to-v3.js +500 -0
- package/dist/tasks/source/task-to-v4.js +467 -0
- package/docs/reference/cli.md +8 -3
- package/package.json +4 -4
|
@@ -0,0 +1,428 @@
|
|
|
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
|
+
* The frozen task-v3 reader (spec docs/plans/specs/p4-deletions-closeout.md
|
|
6
|
+
* §3.2.4, P4-N1).
|
|
7
|
+
*
|
|
8
|
+
* `src/` no longer accepts task v3; this copy exists so the migrator can
|
|
9
|
+
* still READ what it converts. It is frozen: it is never extended, and a
|
|
10
|
+
* change to task source v4 never propagates here. It deliberately
|
|
11
|
+
* duplicates code that used to live in `src/tasks/source-v3.ts` — moved
|
|
12
|
+
* body-intact, verbatim, from that file as it stood at the parent of P4's
|
|
13
|
+
* commit 2 (`refactor(p4): remove github-action locator grammar from
|
|
14
|
+
* native classification`) — i.e. INCLUDING the GitHub Action locator
|
|
15
|
+
* grammar that commit deleted from native classification.
|
|
16
|
+
* `src/tasks/source/task-to-v4.ts`'s `github-action-target-removed`
|
|
17
|
+
* blocked reason needs this migrator to still be able to CLASSIFY a
|
|
18
|
+
* locator (not merely detect its shape) so it can name the target
|
|
19
|
+
* explicitly rather than guess (`tests/migrate/task-v3-to-v4.test.ts`'s
|
|
20
|
+
* `github-action-target-removed` case).
|
|
21
|
+
*
|
|
22
|
+
* This is the ONLY place the task v3 grammar survives. The frozen-migrator
|
|
23
|
+
* rule runs one way — this file may import from `src/`, `src/` may never
|
|
24
|
+
* import from `scripts/`:
|
|
25
|
+
* `rg -n 'from "\.\./\.\./scripts|from "\.\./scripts|scripts/akm-migrate' src/`
|
|
26
|
+
* returns zero hits, before and after this file exists.
|
|
27
|
+
*
|
|
28
|
+
* Do not "fix" a bug found here by porting a task source v4 change, and do
|
|
29
|
+
* not add a task source v4 feature here. A behavior change to what this
|
|
30
|
+
* file accepts is a behavior change to what the migrator can read FROM —
|
|
31
|
+
* exactly the drift a frozen migrator exists to prevent. Two things this
|
|
32
|
+
* file does NOT carry over from the original, because no migrator module
|
|
33
|
+
* needs them: the bounded-document re-export block (D2-N4's own comment
|
|
34
|
+
* explained that block existed only so OTHER `src/` consumers did not need
|
|
35
|
+
* to import `./source/bounded-document` directly — every consumer here
|
|
36
|
+
* already imports what it needs from `../../tasks/source/bounded-document`
|
|
37
|
+
* on its own), and `taskExtensionDetail`/`TASK_EXTENSION`/
|
|
38
|
+
* `TASK_NEAR_MISS_EXTENSION`/`taskV3SourceErrorDetail` (presentation
|
|
39
|
+
* helpers for `src/`-side consumers, not part of reading a document).
|
|
40
|
+
*/
|
|
41
|
+
import { parseBuiltinCommandAction } from "../../commands/command/builtin-action.js";
|
|
42
|
+
import { bundleRefToString, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
43
|
+
import { UsageError } from "../../core/errors.js";
|
|
44
|
+
import { checkJsonSchemaDefinition } from "../../core/json-schema.js";
|
|
45
|
+
import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_RETRIES, } from "../../workflows/resource-limits.js";
|
|
46
|
+
import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, parseTimeout, parseTools, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, TASK_V3_MAX_SCHEDULES, validateWorkingDirectory, } from "./bounded-document.js";
|
|
47
|
+
export const TASK_V3_SCHEMA_VERSION = 3;
|
|
48
|
+
/** Closed authoring vocabulary. Arbitrary GitHub `{0}` shell templates are not accepted. */
|
|
49
|
+
export const TASK_V3_HOST_SHELLS = ["bash", "sh", "zsh", "pwsh", "powershell", "cmd"];
|
|
50
|
+
export const TASK_V2_MIGRATION_HINT = "Run `akm migrate apply --dry-run` to preview the task-v2 to task-v3 conversion, then run `akm migrate apply`.";
|
|
51
|
+
export function taskV2UnsupportedError(filePath, id) {
|
|
52
|
+
const label = id ? `Task "${id}"` : "Task";
|
|
53
|
+
return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: ${label} uses task schema version 2, which normal execution does not accept. File: ${filePath}`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_V2_MIGRATION_HINT);
|
|
54
|
+
}
|
|
55
|
+
const SOURCE_LABEL = "task v3 source";
|
|
56
|
+
/** Build the shared `BoundedDocumentContext` from this file's own (narrower) options shapes. */
|
|
57
|
+
function ctxFrom(options) {
|
|
58
|
+
return {
|
|
59
|
+
filePath: options.filePath,
|
|
60
|
+
sourceLabel: SOURCE_LABEL,
|
|
61
|
+
...(options.workspaceRoot ? { workspaceRoot: options.workspaceRoot } : {}),
|
|
62
|
+
...(options.lineAt ? { lineAt: options.lineAt } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const TOP_LEVEL_KEYS = ["version", "name", "uses", "run", "with", "env", "shell", "working-directory", "akm", "on"];
|
|
66
|
+
const AKM_KEYS = [
|
|
67
|
+
"schedule",
|
|
68
|
+
"enabled",
|
|
69
|
+
"description",
|
|
70
|
+
"when_to_use",
|
|
71
|
+
"tags",
|
|
72
|
+
"agent",
|
|
73
|
+
"engine",
|
|
74
|
+
"model",
|
|
75
|
+
"inference",
|
|
76
|
+
"outputSchema",
|
|
77
|
+
"tools",
|
|
78
|
+
"timeout",
|
|
79
|
+
"redact",
|
|
80
|
+
"maxSteps",
|
|
81
|
+
"maxRetries",
|
|
82
|
+
];
|
|
83
|
+
const ON_KEYS = ["schedule", "workflow_dispatch"];
|
|
84
|
+
const SHELL_SET = new Set(TASK_V3_HOST_SHELLS);
|
|
85
|
+
const GITHUB_OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
|
86
|
+
const GITHUB_REPOSITORY = /^[A-Za-z0-9_.-]+$/;
|
|
87
|
+
const GITHUB_ACTION_PATH_SEGMENT = /^[A-Za-z0-9_.-]+$/;
|
|
88
|
+
const GITHUB_REF_FORBIDDEN = new Set(["~", "^", ":", "?", "*", "[", "\\"]);
|
|
89
|
+
function hasForbiddenGithubRefCharacter(value) {
|
|
90
|
+
for (const character of value) {
|
|
91
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
92
|
+
if (codePoint <= 0x20 || codePoint === 0x7f || GITHUB_REF_FORBIDDEN.has(character))
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
function nullableSelector(value, ctx, key) {
|
|
98
|
+
const selector = stringField(value, ctx, ["akm", key], { nullable: true });
|
|
99
|
+
if (selector !== null && selector.trim().length === 0)
|
|
100
|
+
sourceError(ctx, ["akm", key], "must be null or a non-empty string.");
|
|
101
|
+
return selector;
|
|
102
|
+
}
|
|
103
|
+
function parseAkm(value, ctx) {
|
|
104
|
+
const input = asRecord(value, ctx, ["akm"]);
|
|
105
|
+
checkKeys(input, AKM_KEYS, ctx, ["akm"]);
|
|
106
|
+
const out = {};
|
|
107
|
+
if (own(input, "schedule")) {
|
|
108
|
+
const schedule = stringField(input.schedule, ctx, ["akm", "schedule"], { nonempty: true });
|
|
109
|
+
noGithubExpression(schedule, ctx, ["akm", "schedule"]);
|
|
110
|
+
out.schedule = schedule;
|
|
111
|
+
}
|
|
112
|
+
if (own(input, "enabled")) {
|
|
113
|
+
if (typeof input.enabled !== "boolean")
|
|
114
|
+
sourceError(ctx, ["akm", "enabled"], "must be a boolean.");
|
|
115
|
+
out.enabled = input.enabled;
|
|
116
|
+
}
|
|
117
|
+
for (const key of ["description", "when_to_use"]) {
|
|
118
|
+
if (own(input, key))
|
|
119
|
+
out[key] = stringField(input[key], ctx, ["akm", key]);
|
|
120
|
+
}
|
|
121
|
+
if (own(input, "tags"))
|
|
122
|
+
out.tags = parseStringArray(input.tags, ctx, ["akm", "tags"]);
|
|
123
|
+
for (const key of ["agent", "engine", "model"]) {
|
|
124
|
+
if (own(input, key))
|
|
125
|
+
out[key] = nullableSelector(input[key], ctx, key);
|
|
126
|
+
}
|
|
127
|
+
if (own(input, "inference")) {
|
|
128
|
+
const inference = presentJsonValue(input.inference, ctx, ["akm", "inference"]);
|
|
129
|
+
out.inference = inference === null ? null : asRecord(inference, ctx, ["akm", "inference"]);
|
|
130
|
+
}
|
|
131
|
+
if (own(input, "outputSchema")) {
|
|
132
|
+
const outputSchema = presentJsonValue(input.outputSchema, ctx, ["akm", "outputSchema"]);
|
|
133
|
+
if (outputSchema === null)
|
|
134
|
+
out.outputSchema = null;
|
|
135
|
+
else {
|
|
136
|
+
const schema = asRecord(outputSchema, ctx, ["akm", "outputSchema"]);
|
|
137
|
+
const issue = checkJsonSchemaDefinition(schema)[0];
|
|
138
|
+
if (issue)
|
|
139
|
+
sourceError(ctx, ["akm", "outputSchema"], `is not a supported JSON schema: ${issue.message}`);
|
|
140
|
+
out.outputSchema = schema;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (own(input, "tools"))
|
|
144
|
+
out.tools = parseTools(presentJsonValue(input.tools, ctx, ["akm", "tools"]), ctx);
|
|
145
|
+
if (own(input, "timeout"))
|
|
146
|
+
out.timeout = parseTimeout(input.timeout, ctx);
|
|
147
|
+
if (own(input, "redact")) {
|
|
148
|
+
const names = parseStringArray(input.redact, ctx, ["akm", "redact"], {
|
|
149
|
+
max: WORKFLOW_MAX_EXEC_PASS_ENV,
|
|
150
|
+
pattern: WORKFLOW_ENV_VAR_NAME_PATTERN,
|
|
151
|
+
});
|
|
152
|
+
if (new Set(names).size !== names.length)
|
|
153
|
+
sourceError(ctx, ["akm", "redact"], "must not contain duplicate names.");
|
|
154
|
+
out.redact = names;
|
|
155
|
+
}
|
|
156
|
+
if (own(input, "maxSteps")) {
|
|
157
|
+
if (!Number.isSafeInteger(input.maxSteps) || input.maxSteps < 1) {
|
|
158
|
+
sourceError(ctx, ["akm", "maxSteps"], "must be a positive safe integer.");
|
|
159
|
+
}
|
|
160
|
+
out.maxSteps = input.maxSteps;
|
|
161
|
+
}
|
|
162
|
+
if (own(input, "maxRetries")) {
|
|
163
|
+
if (!Number.isSafeInteger(input.maxRetries) ||
|
|
164
|
+
input.maxRetries < 0 ||
|
|
165
|
+
input.maxRetries > WORKFLOW_MAX_RETRIES) {
|
|
166
|
+
sourceError(ctx, ["akm", "maxRetries"], `must be an integer from 0 through ${WORKFLOW_MAX_RETRIES}.`);
|
|
167
|
+
}
|
|
168
|
+
out.maxRetries = input.maxRetries;
|
|
169
|
+
}
|
|
170
|
+
return Object.freeze(out);
|
|
171
|
+
}
|
|
172
|
+
function validGithubRevision(revision) {
|
|
173
|
+
if (revision.length === 0 ||
|
|
174
|
+
hasForbiddenGithubRefCharacter(revision) ||
|
|
175
|
+
revision.startsWith("/") ||
|
|
176
|
+
revision.endsWith("/") ||
|
|
177
|
+
revision.includes("..") ||
|
|
178
|
+
revision.includes("@{") ||
|
|
179
|
+
revision.includes("@")) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return revision
|
|
183
|
+
.split("/")
|
|
184
|
+
.every((segment) => segment.length > 0 &&
|
|
185
|
+
segment !== "." &&
|
|
186
|
+
segment !== ".." &&
|
|
187
|
+
!segment.startsWith(".") &&
|
|
188
|
+
!segment.endsWith(".") &&
|
|
189
|
+
!segment.endsWith(".lock"));
|
|
190
|
+
}
|
|
191
|
+
/** Classify one exact `uses` string. This function never resolves or guesses. */
|
|
192
|
+
export function classifyTaskV3Uses(value) {
|
|
193
|
+
if (typeof value !== "string" ||
|
|
194
|
+
value.length === 0 ||
|
|
195
|
+
value.trim() !== value ||
|
|
196
|
+
/\s/.test(value) ||
|
|
197
|
+
value.includes("${{")) {
|
|
198
|
+
throw new UsageError("Task v3 uses must be one exact, non-empty executable ref without expressions.", "INVALID_FLAG_VALUE");
|
|
199
|
+
}
|
|
200
|
+
if (value === "akm/command")
|
|
201
|
+
return Object.freeze({ kind: "builtin-command", ref: "akm/command" });
|
|
202
|
+
try {
|
|
203
|
+
const parsed = parseBundleRef(value);
|
|
204
|
+
if (parsed.fragment === undefined && bundleRefToString(parsed) === value) {
|
|
205
|
+
const slash = parsed.conceptId.indexOf("/");
|
|
206
|
+
const family = slash < 0 ? "" : parsed.conceptId.slice(0, slash);
|
|
207
|
+
const name = slash < 0 ? "" : parsed.conceptId.slice(slash + 1);
|
|
208
|
+
if (name.length > 0 && (family === "commands" || family === "workflows" || family === "scripts")) {
|
|
209
|
+
const kind = family === "commands" ? "command" : family === "workflows" ? "workflow" : "script";
|
|
210
|
+
return Object.freeze({ kind, ref: value });
|
|
211
|
+
}
|
|
212
|
+
if (family === "agents") {
|
|
213
|
+
throw new UsageError("An agent ref selects a persona and is not executable through task v3 uses.", "INVALID_FLAG_VALUE");
|
|
214
|
+
}
|
|
215
|
+
if (family === "tasks") {
|
|
216
|
+
throw new UsageError("A task ref is not an executable task-v3 uses target.", "INVALID_FLAG_VALUE");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (error instanceof UsageError && /agent ref|task ref/i.test(error.message))
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
const at = value.lastIndexOf("@");
|
|
225
|
+
if (at > 0 && at === value.indexOf("@")) {
|
|
226
|
+
const locator = value.slice(0, at);
|
|
227
|
+
const revision = value.slice(at + 1);
|
|
228
|
+
const segments = locator.split("/");
|
|
229
|
+
const [owner, repository, ...actionPath] = segments;
|
|
230
|
+
if (owner &&
|
|
231
|
+
repository &&
|
|
232
|
+
GITHUB_OWNER.test(owner) &&
|
|
233
|
+
GITHUB_REPOSITORY.test(repository) &&
|
|
234
|
+
repository !== "." &&
|
|
235
|
+
repository !== ".." &&
|
|
236
|
+
actionPath.every((segment) => GITHUB_ACTION_PATH_SEGMENT.test(segment) && segment !== "." && segment !== "..") &&
|
|
237
|
+
validGithubRevision(revision)) {
|
|
238
|
+
const action = {
|
|
239
|
+
kind: "github-action",
|
|
240
|
+
ref: value,
|
|
241
|
+
owner,
|
|
242
|
+
repository,
|
|
243
|
+
...(actionPath.length > 0 ? { path: actionPath.join("/") } : {}),
|
|
244
|
+
revision,
|
|
245
|
+
};
|
|
246
|
+
return Object.freeze(action);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw new UsageError("Task v3 uses must be akm/command, a canonical commands/, workflows/, or scripts/ asset ref, or owner/repo[/path]@ref. Agent/task/local/Docker/ambiguous targets are not executable.", "INVALID_FLAG_VALUE");
|
|
250
|
+
}
|
|
251
|
+
function parseOn(value, ctx) {
|
|
252
|
+
const input = asRecord(value, ctx, ["on"]);
|
|
253
|
+
const keys = Object.keys(input);
|
|
254
|
+
if (keys.length === 0)
|
|
255
|
+
sourceError(ctx, ["on"], "must declare schedule and/or workflow_dispatch.");
|
|
256
|
+
const unsupported = keys.find((key) => !ON_KEYS.includes(key));
|
|
257
|
+
if (unsupported)
|
|
258
|
+
sourceError(ctx, ["on", unsupported], "is an unsupported local service event; no scheduler binding was created.");
|
|
259
|
+
const schedules = [];
|
|
260
|
+
if (own(input, "schedule")) {
|
|
261
|
+
if (!Array.isArray(input.schedule) || input.schedule.length === 0) {
|
|
262
|
+
sourceError(ctx, ["on", "schedule"], "must be a non-empty list of {cron: string} records.");
|
|
263
|
+
}
|
|
264
|
+
if (input.schedule.length > TASK_V3_MAX_SCHEDULES) {
|
|
265
|
+
sourceError(ctx, ["on", "schedule"], `accepts at most ${TASK_V3_MAX_SCHEDULES} entries.`);
|
|
266
|
+
}
|
|
267
|
+
for (const [index, raw] of input.schedule.entries()) {
|
|
268
|
+
const entry = asRecord(raw, ctx, ["on", "schedule", index]);
|
|
269
|
+
checkKeys(entry, ["cron"], ctx, ["on", "schedule", index]);
|
|
270
|
+
if (!own(entry, "cron"))
|
|
271
|
+
sourceError(ctx, ["on", "schedule", index, "cron"], "is required.");
|
|
272
|
+
const cron = stringField(entry.cron, ctx, ["on", "schedule", index, "cron"], { nonempty: true });
|
|
273
|
+
noGithubExpression(cron, ctx, ["on", "schedule", index, "cron"]);
|
|
274
|
+
schedules.push(Object.freeze({ cron, source: `on.schedule[${index}].cron`, ordinal: index }));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
let manual = false;
|
|
278
|
+
if (own(input, "workflow_dispatch")) {
|
|
279
|
+
const dispatch = input.workflow_dispatch;
|
|
280
|
+
if (dispatch !== null) {
|
|
281
|
+
const mapping = asRecord(presentJsonValue(dispatch, ctx, ["on", "workflow_dispatch"]), ctx, [
|
|
282
|
+
"on",
|
|
283
|
+
"workflow_dispatch",
|
|
284
|
+
]);
|
|
285
|
+
if (Object.keys(mapping).length > 0) {
|
|
286
|
+
sourceError(ctx, ["on", "workflow_dispatch"], "must be null or an empty mapping; inputs are unsupported.");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
manual = true;
|
|
290
|
+
}
|
|
291
|
+
return Object.freeze({ manual, schedules: Object.freeze(schedules) });
|
|
292
|
+
}
|
|
293
|
+
function compileTriggers(input, akm, ctx) {
|
|
294
|
+
const hasSchedule = akm !== undefined && own(akm, "schedule");
|
|
295
|
+
const hasOn = own(input, "on");
|
|
296
|
+
if (hasSchedule === hasOn) {
|
|
297
|
+
sourceError(ctx, [], "must declare exactly one scheduling source: akm.schedule or on.");
|
|
298
|
+
}
|
|
299
|
+
if (hasOn)
|
|
300
|
+
return parseOn(presentJsonValue(input.on, ctx, ["on"]), ctx);
|
|
301
|
+
return Object.freeze({
|
|
302
|
+
manual: false,
|
|
303
|
+
schedules: Object.freeze([Object.freeze({ cron: akm?.schedule, source: "akm.schedule", ordinal: 0 })]),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function parseTaskV3TriggerFields(input, ctx) {
|
|
307
|
+
const akm = own(input, "akm") ? parseAkm(presentJsonValue(input.akm, ctx, ["akm"]), ctx) : undefined;
|
|
308
|
+
return Object.freeze({ ...(akm ? { akm } : {}), triggers: compileTriggers(input, akm, ctx) });
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Classify the strict trigger fragment `{akm?, on?}` into deterministic local
|
|
312
|
+
* scheduler bindings. Unused by the migrator today (the three migrator
|
|
313
|
+
* modules only call `parseTaskV3Yaml`) — vendored anyway for fidelity with
|
|
314
|
+
* the original file this was moved body-intact from, and because
|
|
315
|
+
* `parseTaskV3Document` shares its trigger-parsing helpers
|
|
316
|
+
* (`parseTaskV3TriggerFields`/`compileTriggers`/`parseOn`/`parseAkm`) with
|
|
317
|
+
* it. The LIVE, `src`-side classifier this was re-homed from is
|
|
318
|
+
* `classifyWorkflowYamlTriggers` (`src/workflows/source-ir/triggers.ts`,
|
|
319
|
+
* P4-N3) — that one, not this frozen copy, is what
|
|
320
|
+
* `src/workflows/source-ir/compile.ts` injects.
|
|
321
|
+
*/
|
|
322
|
+
export function classifyTaskV3Triggers(value, options) {
|
|
323
|
+
const ctx = ctxFrom(options);
|
|
324
|
+
const cloned = cloneBoundedJson(value, ctx, [], { nodes: 0 });
|
|
325
|
+
const input = asRecord(cloned, ctx, []);
|
|
326
|
+
checkKeys(input, ["akm", "on"], ctx, []);
|
|
327
|
+
return parseTaskV3TriggerFields(input, ctx).triggers;
|
|
328
|
+
}
|
|
329
|
+
export function parseTaskV3Document(value, options) {
|
|
330
|
+
const ctx = ctxFrom(options);
|
|
331
|
+
const cloned = cloneBoundedJson(value, ctx, [], { nodes: 0 });
|
|
332
|
+
const input = asRecord(cloned, ctx, []);
|
|
333
|
+
if (!own(input, "version"))
|
|
334
|
+
sourceError(ctx, ["version"], "is required and must be 3.");
|
|
335
|
+
if (input.version === 2)
|
|
336
|
+
throw taskV2UnsupportedError(options.filePath);
|
|
337
|
+
if (input.version !== TASK_V3_SCHEMA_VERSION)
|
|
338
|
+
sourceError(ctx, ["version"], "must be exactly 3.");
|
|
339
|
+
checkKeys(input, TOP_LEVEL_KEYS, ctx, []);
|
|
340
|
+
const hasUses = own(input, "uses");
|
|
341
|
+
const hasRun = own(input, "run");
|
|
342
|
+
if (hasUses === hasRun)
|
|
343
|
+
sourceError(ctx, [], "requires exactly one executable selector: uses or run.");
|
|
344
|
+
const name = own(input, "name") ? stringField(input.name, ctx, ["name"]) : undefined;
|
|
345
|
+
const env = own(input, "env") ? parseEnvironment(presentJsonValue(input.env, ctx, ["env"]), ctx) : undefined;
|
|
346
|
+
const { akm, triggers } = parseTaskV3TriggerFields(input, ctx);
|
|
347
|
+
let target;
|
|
348
|
+
if (hasUses) {
|
|
349
|
+
if (own(input, "shell"))
|
|
350
|
+
sourceError(ctx, ["shell"], "is legal only with run.");
|
|
351
|
+
if (own(input, "working-directory"))
|
|
352
|
+
sourceError(ctx, ["working-directory"], "is legal only with run.");
|
|
353
|
+
const usesText = stringField(input.uses, ctx, ["uses"], { nonempty: true });
|
|
354
|
+
let uses;
|
|
355
|
+
try {
|
|
356
|
+
uses = classifyTaskV3Uses(usesText);
|
|
357
|
+
}
|
|
358
|
+
catch (cause) {
|
|
359
|
+
sourceError(ctx, ["uses"], cause instanceof Error ? cause.message : String(cause));
|
|
360
|
+
}
|
|
361
|
+
let withValues;
|
|
362
|
+
if (own(input, "with"))
|
|
363
|
+
withValues = asRecord(presentJsonValue(input.with, ctx, ["with"]), ctx, ["with"]);
|
|
364
|
+
if (uses.kind === "builtin-command") {
|
|
365
|
+
let command;
|
|
366
|
+
try {
|
|
367
|
+
command = parseBuiltinCommandAction(withValues);
|
|
368
|
+
}
|
|
369
|
+
catch (cause) {
|
|
370
|
+
sourceError(ctx, ["with"], cause instanceof Error ? cause.message : String(cause));
|
|
371
|
+
}
|
|
372
|
+
target = Object.freeze({ kind: "uses", uses, ...(withValues ? { with: withValues } : {}), command });
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
target = Object.freeze({ kind: "uses", uses, ...(withValues ? { with: withValues } : {}) });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
if (own(input, "with"))
|
|
380
|
+
sourceError(ctx, ["with"], "is legal only with uses.");
|
|
381
|
+
const run = stringField(input.run, ctx, ["run"], { nonempty: true });
|
|
382
|
+
noGithubExpression(run, ctx, ["run"]);
|
|
383
|
+
let shell;
|
|
384
|
+
if (own(input, "shell")) {
|
|
385
|
+
const rawShell = stringField(input.shell, ctx, ["shell"], { nonempty: true });
|
|
386
|
+
if (!SHELL_SET.has(rawShell)) {
|
|
387
|
+
sourceError(ctx, ["shell"], `must be one of the closed host-shell table: ${TASK_V3_HOST_SHELLS.join(", ")}.`);
|
|
388
|
+
}
|
|
389
|
+
shell = rawShell;
|
|
390
|
+
}
|
|
391
|
+
let workingDirectory;
|
|
392
|
+
if (own(input, "working-directory")) {
|
|
393
|
+
workingDirectory = stringField(input["working-directory"], ctx, ["working-directory"], {
|
|
394
|
+
nonempty: true,
|
|
395
|
+
});
|
|
396
|
+
validateWorkingDirectory(workingDirectory, ctx);
|
|
397
|
+
}
|
|
398
|
+
target = Object.freeze({
|
|
399
|
+
kind: "run",
|
|
400
|
+
run,
|
|
401
|
+
...(shell ? { shell } : {}),
|
|
402
|
+
...(workingDirectory ? { workingDirectory } : {}),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return Object.freeze({
|
|
406
|
+
version: TASK_V3_SCHEMA_VERSION,
|
|
407
|
+
...(name !== undefined ? { name } : {}),
|
|
408
|
+
target,
|
|
409
|
+
...(env !== undefined ? { env } : {}),
|
|
410
|
+
...(akm !== undefined ? { akm } : {}),
|
|
411
|
+
triggers,
|
|
412
|
+
source: Object.freeze({ path: options.filePath }),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Parse hostile YAML without aliases/tags/merges, then enter the canonical
|
|
417
|
+
* object parser. The bounded-YAML front end itself is
|
|
418
|
+
* `readBoundedTaskSourceYaml` (`src/tasks/source/bounded-document.ts`) —
|
|
419
|
+
* this is a thin wrapper passing `sourceLabel: "task v3 source"`.
|
|
420
|
+
*/
|
|
421
|
+
export function parseTaskV3Yaml(input) {
|
|
422
|
+
const { root, lineAt } = readBoundedTaskSourceYaml(input, { sourceLabel: SOURCE_LABEL });
|
|
423
|
+
return parseTaskV3Document(root, {
|
|
424
|
+
filePath: input.filePath,
|
|
425
|
+
...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
|
|
426
|
+
lineAt,
|
|
427
|
+
});
|
|
428
|
+
}
|