akm-cli 0.9.1 → 0.9.2-alpha.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 +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -0,0 +1,577 @@
|
|
|
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
|
+
import { isAlias, isMap, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
|
|
5
|
+
import { utf8Bytes, WORKFLOW_MAX_SOURCE_BYTES } from "../resource-limits.js";
|
|
6
|
+
import { canonicalTopologicalJobs } from "./ordering.js";
|
|
7
|
+
import { WorkflowSourceFailure } from "./result.js";
|
|
8
|
+
import { WORKFLOW_SOURCE_HOST_SHELLS, } from "./schema.js";
|
|
9
|
+
import { canonicalizeWorkflowCron, canonicalizeWorkflowRun, canonicalizeWorkflowWorkingDirectory, classifyWorkflowStepUses, validateWorkflowBuiltinCommand, WorkflowSourceSemanticError, } from "./semantics.js";
|
|
10
|
+
import { classifyWorkflowSourceUses, } from "./uses.js";
|
|
11
|
+
const ROOT_KEYS = ["name", "on", "jobs"];
|
|
12
|
+
const TRIGGER_KEYS = ["schedule", "workflow_dispatch"];
|
|
13
|
+
const SCHEDULE_KEYS = ["cron"];
|
|
14
|
+
const JOB_KEYS = ["name", "needs", "runs-on", "steps"];
|
|
15
|
+
const STEP_KEYS = ["id", "name", "uses", "run", "with", "env", "shell", "working-directory"];
|
|
16
|
+
const UNSAFE_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
17
|
+
const HOST_SHELLS = new Set(WORKFLOW_SOURCE_HOST_SHELLS);
|
|
18
|
+
const SOURCE_ID = /^[A-Za-z_][A-Za-z0-9_-]{0,127}$/;
|
|
19
|
+
const INPUT_KEY = /^[A-Za-z_][A-Za-z0-9_.-]{0,127}$/;
|
|
20
|
+
const ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
21
|
+
const MAX_YAML_DEPTH = 32;
|
|
22
|
+
const MAX_YAML_NODES = 20_000;
|
|
23
|
+
const MAX_STEPS_PER_JOB = 256;
|
|
24
|
+
/** A bounded, non-expanding ownership probe. Validation belongs to compilation. */
|
|
25
|
+
export function looksLikeGithubWorkflowSource(source) {
|
|
26
|
+
if (utf8Bytes(source) > WORKFLOW_MAX_SOURCE_BYTES)
|
|
27
|
+
return false;
|
|
28
|
+
let doc;
|
|
29
|
+
try {
|
|
30
|
+
doc = parseDocument(source, { strict: true, uniqueKeys: true, version: "1.2" });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (!isMap(doc.contents))
|
|
36
|
+
return false;
|
|
37
|
+
const keys = new Set();
|
|
38
|
+
for (const pair of doc.contents.items) {
|
|
39
|
+
if (isScalar(pair.key) && typeof pair.key.value === "string")
|
|
40
|
+
keys.add(pair.key.value);
|
|
41
|
+
}
|
|
42
|
+
return keys.has("on") && keys.has("jobs");
|
|
43
|
+
}
|
|
44
|
+
export function parseGithubWorkflowSource(source, options) {
|
|
45
|
+
const documentSource = wholeSourceSpan(source, options.path);
|
|
46
|
+
if (utf8Bytes(source) > WORKFLOW_MAX_SOURCE_BYTES) {
|
|
47
|
+
throw new WorkflowSourceFailure("source-size-limit", "Workflow source exceeds the 1 MiB limit.", documentSource);
|
|
48
|
+
}
|
|
49
|
+
const lineCounter = new LineCounter();
|
|
50
|
+
let doc;
|
|
51
|
+
try {
|
|
52
|
+
doc = parseDocument(source, {
|
|
53
|
+
lineCounter,
|
|
54
|
+
strict: true,
|
|
55
|
+
uniqueKeys: true,
|
|
56
|
+
version: "1.2",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new WorkflowSourceFailure("invalid-yaml", "Workflow YAML could not be parsed.", documentSource);
|
|
61
|
+
}
|
|
62
|
+
if (doc.errors.length > 0) {
|
|
63
|
+
const problem = doc.errors[0];
|
|
64
|
+
if (!problem)
|
|
65
|
+
throw new WorkflowSourceFailure("invalid-yaml", "Invalid YAML.", documentSource);
|
|
66
|
+
const offset = Array.isArray(problem.pos) ? problem.pos[0] : 0;
|
|
67
|
+
const errorSource = spanAtOffset(options.path, lineCounter, offset);
|
|
68
|
+
const code = /Map keys must be unique/i.test(problem.message) ? "duplicate-key" : "invalid-yaml";
|
|
69
|
+
throw new WorkflowSourceFailure(code, cleanYamlMessage(problem.message), errorSource);
|
|
70
|
+
}
|
|
71
|
+
const firstWarning = doc.warnings[0];
|
|
72
|
+
if (firstWarning && /tag/i.test(firstWarning.message)) {
|
|
73
|
+
throw new WorkflowSourceFailure("yaml-custom-tag", cleanYamlMessage(firstWarning.message), documentSource);
|
|
74
|
+
}
|
|
75
|
+
const rootNode = doc.contents;
|
|
76
|
+
if (!isMap(rootNode)) {
|
|
77
|
+
throw new WorkflowSourceFailure("mapping-root-required", "GitHub workflow YAML root must be a mapping.", documentSource);
|
|
78
|
+
}
|
|
79
|
+
const parsedRoot = rootNode;
|
|
80
|
+
const reader = new StrictYamlReader(options.path, lineCounter);
|
|
81
|
+
reader.rejectAliases(parsedRoot);
|
|
82
|
+
reader.checkTree(parsedRoot);
|
|
83
|
+
const root = reader.fields(parsedRoot, ROOT_KEYS, "workflow");
|
|
84
|
+
const name = reader.requiredString(root, "name", "workflow");
|
|
85
|
+
const triggers = parseTriggers(reader, parsedRoot, reader.required(root, "on", "workflow"), options);
|
|
86
|
+
const jobs = parseJobs(reader, reader.required(root, "jobs", "workflow"), options);
|
|
87
|
+
return {
|
|
88
|
+
sourceIrVersion: 1,
|
|
89
|
+
name,
|
|
90
|
+
triggers,
|
|
91
|
+
jobs,
|
|
92
|
+
source: documentSource,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
class StrictYamlReader {
|
|
96
|
+
filePath;
|
|
97
|
+
lineCounter;
|
|
98
|
+
nodes = 0;
|
|
99
|
+
constructor(filePath, lineCounter) {
|
|
100
|
+
this.filePath = filePath;
|
|
101
|
+
this.lineCounter = lineCounter;
|
|
102
|
+
}
|
|
103
|
+
span(node) {
|
|
104
|
+
const range = node?.range;
|
|
105
|
+
if (!range)
|
|
106
|
+
return { path: this.filePath, start: 1, end: 1 };
|
|
107
|
+
const start = this.lineCounter.linePos(range[0]).line;
|
|
108
|
+
const end = this.lineCounter.linePos(Math.max(range[0], range[1] - 1)).line;
|
|
109
|
+
return { path: this.filePath, start, end: Math.max(start, end) };
|
|
110
|
+
}
|
|
111
|
+
rejectAliases(root) {
|
|
112
|
+
const pending = [{ node: root, depth: 0 }];
|
|
113
|
+
let nodes = 0;
|
|
114
|
+
while (pending.length > 0) {
|
|
115
|
+
const current = pending.pop();
|
|
116
|
+
if (!current)
|
|
117
|
+
break;
|
|
118
|
+
nodes++;
|
|
119
|
+
if (nodes > MAX_YAML_NODES)
|
|
120
|
+
this.fail("yaml-node-limit", "YAML exceeds the node limit.", current.node);
|
|
121
|
+
if (current.depth > MAX_YAML_DEPTH) {
|
|
122
|
+
this.fail("yaml-depth-limit", "YAML exceeds the depth limit.", current.node);
|
|
123
|
+
}
|
|
124
|
+
if (isAlias(current.node))
|
|
125
|
+
this.fail("yaml-alias", "YAML aliases are not supported.", current.node);
|
|
126
|
+
if (isSeq(current.node)) {
|
|
127
|
+
for (const item of current.node.items) {
|
|
128
|
+
if (item)
|
|
129
|
+
pending.push({ node: item, depth: current.depth + 1 });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (isMap(current.node)) {
|
|
133
|
+
for (const pair of current.node.items) {
|
|
134
|
+
if (pair.key)
|
|
135
|
+
pending.push({ node: pair.key, depth: current.depth + 1 });
|
|
136
|
+
if (pair.value)
|
|
137
|
+
pending.push({ node: pair.value, depth: current.depth + 1 });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
checkTree(node, depth = 0) {
|
|
143
|
+
this.nodes++;
|
|
144
|
+
if (this.nodes > MAX_YAML_NODES)
|
|
145
|
+
this.fail("yaml-node-limit", "YAML exceeds the node limit.", node);
|
|
146
|
+
if (depth > MAX_YAML_DEPTH)
|
|
147
|
+
this.fail("yaml-depth-limit", "YAML exceeds the depth limit.", node);
|
|
148
|
+
if (isAlias(node))
|
|
149
|
+
this.fail("yaml-alias", "YAML aliases are not supported.", node);
|
|
150
|
+
if ("anchor" in node && typeof node.anchor === "string" && node.anchor !== "") {
|
|
151
|
+
this.fail("yaml-anchor", "YAML anchors are not supported.", node);
|
|
152
|
+
}
|
|
153
|
+
if (node.tag !== undefined)
|
|
154
|
+
this.fail("yaml-custom-tag", "Explicit YAML tags are not supported.", node);
|
|
155
|
+
if (isScalar(node)) {
|
|
156
|
+
if (typeof node.value === "string" && node.value.includes("${{")) {
|
|
157
|
+
this.fail("unsupported-github-expression", "GitHub expressions and contexts are not supported.", node);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (isSeq(node)) {
|
|
162
|
+
for (const item of node.items)
|
|
163
|
+
if (item)
|
|
164
|
+
this.checkTree(item, depth + 1);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (isMap(node)) {
|
|
168
|
+
for (const pair of node.items) {
|
|
169
|
+
if (pair.key)
|
|
170
|
+
this.checkTree(pair.key, depth + 1);
|
|
171
|
+
if (pair.value)
|
|
172
|
+
this.checkTree(pair.value, depth + 1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
fields(node, allowed, context) {
|
|
177
|
+
if (!isMap(node))
|
|
178
|
+
this.fail("mapping-required", `${context} must be a mapping.`, node);
|
|
179
|
+
const accepted = new Set(allowed);
|
|
180
|
+
const out = new Map();
|
|
181
|
+
for (const pair of node.items) {
|
|
182
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== "string" || pair.key.value.trim() === "") {
|
|
183
|
+
this.fail("string-key-required", `${context} keys must be non-empty strings.`, pair.key);
|
|
184
|
+
}
|
|
185
|
+
const key = pair.key.value;
|
|
186
|
+
if (UNSAFE_KEYS.has(key))
|
|
187
|
+
this.fail("unsafe-key", `${context} contains unsafe key ${JSON.stringify(key)}.`, pair.key);
|
|
188
|
+
if (!accepted.has(key))
|
|
189
|
+
this.fail("unknown-key", `${context} contains unknown key ${JSON.stringify(key)}.`, pair.key);
|
|
190
|
+
out.set(key, pair);
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
arbitraryFields(node, context) {
|
|
195
|
+
if (!isMap(node))
|
|
196
|
+
this.fail("mapping-required", `${context} must be a mapping.`, node);
|
|
197
|
+
const out = new Map();
|
|
198
|
+
for (const pair of node.items) {
|
|
199
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== "string" || pair.key.value.trim() === "") {
|
|
200
|
+
this.fail("string-key-required", `${context} keys must be non-empty strings.`, pair.key);
|
|
201
|
+
}
|
|
202
|
+
const key = pair.key.value;
|
|
203
|
+
if (UNSAFE_KEYS.has(key))
|
|
204
|
+
this.fail("unsafe-key", `${context} contains unsafe key ${JSON.stringify(key)}.`, pair.key);
|
|
205
|
+
out.set(key, pair);
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
plain(node, context) {
|
|
210
|
+
if (node === null)
|
|
211
|
+
return null;
|
|
212
|
+
if (isScalar(node))
|
|
213
|
+
return node.value;
|
|
214
|
+
if (isSeq(node)) {
|
|
215
|
+
return node.items.map((item) => {
|
|
216
|
+
if (!item)
|
|
217
|
+
this.fail("sparse-yaml-sequence", `${context} may not contain empty entries.`, node);
|
|
218
|
+
return this.plain(item, context);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (isMap(node)) {
|
|
222
|
+
const out = {};
|
|
223
|
+
for (const [key, pair] of this.arbitraryFields(node, context)) {
|
|
224
|
+
out[key] = this.plain(pair.value, `${context}.${key}`);
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
this.fail("invalid-yaml-node", `${context} contains an unsupported YAML node.`, node);
|
|
229
|
+
}
|
|
230
|
+
lineAt(root, structuralPath) {
|
|
231
|
+
let current = root;
|
|
232
|
+
for (const [index, segment] of structuralPath.entries()) {
|
|
233
|
+
if (typeof segment === "string" && isMap(current)) {
|
|
234
|
+
const pair = current.items.find((candidate) => isScalar(candidate.key) && candidate.key.value === segment);
|
|
235
|
+
if (!pair)
|
|
236
|
+
return undefined;
|
|
237
|
+
if (pair.value === null) {
|
|
238
|
+
return index === structuralPath.length - 1 ? this.span(pair.key).start : undefined;
|
|
239
|
+
}
|
|
240
|
+
current = pair.value;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (typeof segment === "number" && isSeq(current)) {
|
|
244
|
+
current = current.items[segment] ?? null;
|
|
245
|
+
if (current === null)
|
|
246
|
+
return undefined;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
return this.span(current).start;
|
|
252
|
+
}
|
|
253
|
+
required(fields, key, context) {
|
|
254
|
+
const pair = fields.get(key);
|
|
255
|
+
if (!pair)
|
|
256
|
+
this.fail("missing-key", `${context} is missing required key ${JSON.stringify(key)}.`, undefined);
|
|
257
|
+
return pair.value;
|
|
258
|
+
}
|
|
259
|
+
requiredString(fields, key, context) {
|
|
260
|
+
return this.string(this.required(fields, key, context), `${context}.${key}`);
|
|
261
|
+
}
|
|
262
|
+
optionalString(fields, key, context) {
|
|
263
|
+
const pair = fields.get(key);
|
|
264
|
+
return pair ? this.string(pair.value, `${context}.${key}`) : undefined;
|
|
265
|
+
}
|
|
266
|
+
string(node, context) {
|
|
267
|
+
if (!isScalar(node) || typeof node.value !== "string" || node.value.trim() === "") {
|
|
268
|
+
this.fail("string-required", `${context} must be a non-empty string.`, node);
|
|
269
|
+
}
|
|
270
|
+
return node.value;
|
|
271
|
+
}
|
|
272
|
+
scalar(node, context, allowNull) {
|
|
273
|
+
if (node === null || (isScalar(node) && node.value === null)) {
|
|
274
|
+
if (allowNull)
|
|
275
|
+
return null;
|
|
276
|
+
this.fail("scalar-required", `${context} may not be null.`, node);
|
|
277
|
+
}
|
|
278
|
+
if (!isScalar(node))
|
|
279
|
+
this.fail("scalar-required", `${context} must be a scalar.`, node);
|
|
280
|
+
const value = node.value;
|
|
281
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
282
|
+
this.fail("scalar-required", `${context} must be a string, number, or boolean.`, node);
|
|
283
|
+
}
|
|
284
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
285
|
+
this.fail("scalar-required", `${context} must be a finite number.`, node);
|
|
286
|
+
}
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
sequence(node, context, max, allowEmpty = false) {
|
|
290
|
+
if (!isSeq(node) || (!allowEmpty && node.items.length === 0)) {
|
|
291
|
+
this.fail("sequence-required", `${context} must be ${allowEmpty ? "an" : "a non-empty"} list.`, node);
|
|
292
|
+
}
|
|
293
|
+
if (node.items.length > max)
|
|
294
|
+
this.fail("step-count-limit", `${context} exceeds ${max} entries.`, node);
|
|
295
|
+
return node.items.map((item) => {
|
|
296
|
+
if (!item)
|
|
297
|
+
this.fail("sparse-yaml-sequence", `${context} may not contain empty entries.`, node);
|
|
298
|
+
return item;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
fail(code, message, node) {
|
|
302
|
+
throw new WorkflowSourceFailure(code, message, this.span(node));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function parseTriggers(reader, root, node, options) {
|
|
306
|
+
const fields = reader.arbitraryFields(node, "workflow.on");
|
|
307
|
+
for (const [key, pair] of fields) {
|
|
308
|
+
if (!TRIGGER_KEYS.includes(key)) {
|
|
309
|
+
reader.fail("unsupported-service-event", `GitHub service event ${JSON.stringify(key)} is unsupported for local execution.`, pair.key);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (fields.size === 0)
|
|
313
|
+
reader.fail("trigger-required", "workflow.on must declare schedule or workflow_dispatch.", node);
|
|
314
|
+
const triggers = [];
|
|
315
|
+
const schedule = fields.get("schedule");
|
|
316
|
+
if (schedule) {
|
|
317
|
+
const records = reader.sequence(schedule.value, "workflow.on.schedule", 64);
|
|
318
|
+
for (const [ordinal, record] of records.entries()) {
|
|
319
|
+
const scheduleFields = reader.fields(record, SCHEDULE_KEYS, `workflow.on.schedule[${ordinal}]`);
|
|
320
|
+
const cronNode = reader.required(scheduleFields, "cron", `workflow.on.schedule[${ordinal}]`);
|
|
321
|
+
const cron = validateCron(reader, reader.string(cronNode, `workflow.on.schedule[${ordinal}].cron`), cronNode);
|
|
322
|
+
triggers.push({ kind: "schedule", cron, ordinal, source: reader.span(cronNode) });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const manual = fields.get("workflow_dispatch");
|
|
326
|
+
if (manual) {
|
|
327
|
+
const nullValue = manual.value === null || (isScalar(manual.value) && manual.value.value === null);
|
|
328
|
+
if (!nullValue) {
|
|
329
|
+
if (!isMap(manual.value)) {
|
|
330
|
+
reader.fail("workflow-dispatch-inputs-unsupported", "workflow_dispatch must be null or an empty mapping; inputs are unsupported.", manual.value);
|
|
331
|
+
}
|
|
332
|
+
if (manual.value.items.length > 0) {
|
|
333
|
+
reader.fail("workflow-dispatch-inputs-unsupported", "workflow_dispatch inputs are not supported for local execution.", manual.value.items[0]?.key);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
triggers.push({ kind: "workflow_dispatch", source: reader.span(manual.key) });
|
|
337
|
+
}
|
|
338
|
+
if (options.classifyTriggers) {
|
|
339
|
+
verifyOwnerTriggerPlan(reader, root, node, triggers, options);
|
|
340
|
+
}
|
|
341
|
+
return triggers;
|
|
342
|
+
}
|
|
343
|
+
function verifyOwnerTriggerPlan(reader, root, onNode, triggers, options) {
|
|
344
|
+
const classifier = options.classifyTriggers;
|
|
345
|
+
if (!classifier)
|
|
346
|
+
return;
|
|
347
|
+
let lastLine = reader.span(onNode).start;
|
|
348
|
+
let plan;
|
|
349
|
+
try {
|
|
350
|
+
plan = classifier({ on: reader.plain(onNode, "workflow.on") }, {
|
|
351
|
+
filePath: options.path,
|
|
352
|
+
lineAt: (structuralPath) => {
|
|
353
|
+
const line = reader.lineAt(root, structuralPath);
|
|
354
|
+
if (line !== undefined)
|
|
355
|
+
lastLine = line;
|
|
356
|
+
return line;
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
catch (cause) {
|
|
361
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
362
|
+
throw new WorkflowSourceFailure("invalid-trigger", message, {
|
|
363
|
+
path: options.path,
|
|
364
|
+
start: lastLine,
|
|
365
|
+
end: lastLine,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const expectedSchedules = triggers.filter((trigger) => trigger.kind === "schedule");
|
|
369
|
+
const expectedManual = triggers.some((trigger) => trigger.kind === "workflow_dispatch");
|
|
370
|
+
const matches = plan !== null &&
|
|
371
|
+
typeof plan === "object" &&
|
|
372
|
+
plan.manual === expectedManual &&
|
|
373
|
+
Array.isArray(plan.schedules) &&
|
|
374
|
+
plan.schedules.length === expectedSchedules.length &&
|
|
375
|
+
plan.schedules.every((binding, index) => {
|
|
376
|
+
const expected = expectedSchedules[index];
|
|
377
|
+
let canonicalBindingCron;
|
|
378
|
+
try {
|
|
379
|
+
canonicalBindingCron = canonicalizeWorkflowCron(binding.cron);
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
return (expected !== undefined &&
|
|
385
|
+
canonicalBindingCron === expected.cron &&
|
|
386
|
+
binding.ordinal === expected.ordinal &&
|
|
387
|
+
binding.source === `on.schedule[${expected.ordinal}].cron`);
|
|
388
|
+
});
|
|
389
|
+
if (!matches) {
|
|
390
|
+
throw new WorkflowSourceFailure("trigger-classifier-drift", "The workflow trigger parser disagrees with the canonical task-v3 trigger classifier.", reader.span(onNode));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function validateCron(reader, cron, node) {
|
|
394
|
+
try {
|
|
395
|
+
return canonicalizeWorkflowCron(cron);
|
|
396
|
+
}
|
|
397
|
+
catch (cause) {
|
|
398
|
+
semanticReaderFail(reader, cause, node);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function parseJobs(reader, node, options) {
|
|
402
|
+
const fields = reader.arbitraryFields(node, "workflow.jobs");
|
|
403
|
+
if (fields.size === 0 || fields.size > 256) {
|
|
404
|
+
reader.fail("job-count-limit", "workflow.jobs must contain 1 through 256 jobs.", node);
|
|
405
|
+
}
|
|
406
|
+
const jobs = [...fields.entries()].map(([id, pair]) => parseJob(reader, id, pair, options));
|
|
407
|
+
const ordered = canonicalTopologicalJobs(jobs);
|
|
408
|
+
if (ordered.ok)
|
|
409
|
+
return ordered.jobs;
|
|
410
|
+
if (ordered.kind === "missing") {
|
|
411
|
+
throw new WorkflowSourceFailure("missing-job-dependency", `Job ${ordered.job.id} needs missing job ${ordered.dependency}.`, ordered.job.source);
|
|
412
|
+
}
|
|
413
|
+
throw new WorkflowSourceFailure("job-dependency-cycle", "Workflow jobs contain a dependency cycle.", ordered.job.source);
|
|
414
|
+
}
|
|
415
|
+
function parseJob(reader, id, pair, options) {
|
|
416
|
+
const node = pair.value;
|
|
417
|
+
if (!SOURCE_ID.test(id))
|
|
418
|
+
reader.fail("invalid-job-id", `Invalid job id ${JSON.stringify(id)}.`, node);
|
|
419
|
+
const fields = reader.fields(node, JOB_KEYS, `workflow.jobs.${id}`);
|
|
420
|
+
validateRunner(reader, reader.required(fields, "runs-on", `workflow.jobs.${id}`), id);
|
|
421
|
+
const needs = parseNeeds(reader, fields.get("needs"), id);
|
|
422
|
+
const stepNodes = reader.sequence(reader.required(fields, "steps", `workflow.jobs.${id}`), `workflow.jobs.${id}.steps`, MAX_STEPS_PER_JOB);
|
|
423
|
+
const stepIds = new Set();
|
|
424
|
+
const steps = stepNodes.map((step, index) => parseStep(reader, step, id, index, stepIds, options));
|
|
425
|
+
const name = reader.optionalString(fields, "name", `workflow.jobs.${id}`);
|
|
426
|
+
const keySource = reader.span(pair.key);
|
|
427
|
+
const valueSource = reader.span(node);
|
|
428
|
+
return {
|
|
429
|
+
id,
|
|
430
|
+
...(name ? { name } : {}),
|
|
431
|
+
needs,
|
|
432
|
+
steps,
|
|
433
|
+
extensions: { "github.com/actions-workflow": { runsOn: ["self-hosted"] } },
|
|
434
|
+
source: { path: keySource.path, start: keySource.start, end: valueSource.end },
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function validateRunner(reader, node, jobId) {
|
|
438
|
+
if (!isSeq(node) || node.items.length !== 1 || !isScalar(node.items[0]) || node.items[0].value !== "self-hosted") {
|
|
439
|
+
reader.fail("unsupported-runner", `Job ${JSON.stringify(jobId)} must declare exactly runs-on: [self-hosted].`, node);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function parseNeeds(reader, pair, jobId) {
|
|
443
|
+
if (!pair)
|
|
444
|
+
return [];
|
|
445
|
+
const values = isSeq(pair.value)
|
|
446
|
+
? reader
|
|
447
|
+
.sequence(pair.value, `workflow.jobs.${jobId}.needs`, 256)
|
|
448
|
+
.map((item) => reader.string(item, `workflow.jobs.${jobId}.needs`))
|
|
449
|
+
: [reader.string(pair.value, `workflow.jobs.${jobId}.needs`)];
|
|
450
|
+
for (const need of values)
|
|
451
|
+
if (!SOURCE_ID.test(need))
|
|
452
|
+
reader.fail("invalid-job-id", `Invalid needs id ${need}.`, pair.value);
|
|
453
|
+
if (new Set(values).size !== values.length) {
|
|
454
|
+
reader.fail("duplicate-job-dependency", `Job ${jobId} has duplicate needs entries.`, pair.value);
|
|
455
|
+
}
|
|
456
|
+
return values.sort();
|
|
457
|
+
}
|
|
458
|
+
function parseStep(reader, node, jobId, index, stepIds, options) {
|
|
459
|
+
const context = `workflow.jobs.${jobId}.steps[${index}]`;
|
|
460
|
+
const fields = reader.fields(node, STEP_KEYS, context);
|
|
461
|
+
const id = reader.requiredString(fields, "id", context);
|
|
462
|
+
if (!SOURCE_ID.test(id))
|
|
463
|
+
reader.fail("invalid-step-id", `Invalid step id ${JSON.stringify(id)}.`, fields.get("id")?.value);
|
|
464
|
+
if (stepIds.has(id))
|
|
465
|
+
reader.fail("duplicate-step-id", `Job ${jobId} has duplicate step id ${id}.`, fields.get("id")?.value);
|
|
466
|
+
stepIds.add(id);
|
|
467
|
+
const usesPair = fields.get("uses");
|
|
468
|
+
const runPair = fields.get("run");
|
|
469
|
+
if ((usesPair === undefined) === (runPair === undefined)) {
|
|
470
|
+
reader.fail("step-target-xor", `${context} must declare exactly one of uses or run.`, node);
|
|
471
|
+
}
|
|
472
|
+
const common = parseStepCommon(reader, fields, context);
|
|
473
|
+
if (usesPair)
|
|
474
|
+
return parseUsesStep(reader, usesPair, fields, options, { id, ...common, source: reader.span(node) });
|
|
475
|
+
if (!runPair)
|
|
476
|
+
reader.fail("step-target-xor", `${context} must declare exactly one of uses or run.`, node);
|
|
477
|
+
return parseRunStep(reader, runPair, fields, options, { id, ...common, source: reader.span(node) });
|
|
478
|
+
}
|
|
479
|
+
function parseStepCommon(reader, fields, context) {
|
|
480
|
+
const name = reader.optionalString(fields, "name", context);
|
|
481
|
+
const env = parseScalarMap(reader, fields.get("env"), `${context}.env`, ENV_KEY, false);
|
|
482
|
+
return { ...(name ? { name } : {}), ...(env ? { env } : {}) };
|
|
483
|
+
}
|
|
484
|
+
function parseUsesStep(reader, usesPair, fields, options, common) {
|
|
485
|
+
if (fields.has("shell") || fields.has("working-directory")) {
|
|
486
|
+
reader.fail("uses-field-conflict", "shell and working-directory are legal only with run.", usesPair.value);
|
|
487
|
+
}
|
|
488
|
+
const uses = reader.string(usesPair.value, "step.uses");
|
|
489
|
+
const target = classifyUses(reader, uses, usesPair.value, options.classifyUses ?? classifyWorkflowSourceUses);
|
|
490
|
+
const withValues = parseScalarMap(reader, fields.get("with"), "step.with", INPUT_KEY, true);
|
|
491
|
+
const commandMode = target.kind === "builtin-command"
|
|
492
|
+
? validateBuiltinCommand(reader, withValues, fields.get("with")?.value ?? usesPair.value)
|
|
493
|
+
: undefined;
|
|
494
|
+
return {
|
|
495
|
+
...common,
|
|
496
|
+
uses,
|
|
497
|
+
...(commandMode ? { commandMode } : {}),
|
|
498
|
+
...(withValues ? { with: withValues } : {}),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function classifyUses(reader, uses, node, classifier) {
|
|
502
|
+
try {
|
|
503
|
+
return classifyWorkflowStepUses(uses, classifier);
|
|
504
|
+
}
|
|
505
|
+
catch (cause) {
|
|
506
|
+
semanticReaderFail(reader, cause, node);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function validateBuiltinCommand(reader, values, node) {
|
|
510
|
+
try {
|
|
511
|
+
const action = validateWorkflowBuiltinCommand(values);
|
|
512
|
+
if (action.kind === "stored")
|
|
513
|
+
return "stored-ref";
|
|
514
|
+
return action.arguments !== undefined || action.content.includes("$ARGUMENTS") ? "portable-template" : "literal";
|
|
515
|
+
}
|
|
516
|
+
catch (cause) {
|
|
517
|
+
semanticReaderFail(reader, cause, node);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function parseRunStep(reader, runPair, fields, options, common) {
|
|
521
|
+
if (fields.has("with"))
|
|
522
|
+
reader.fail("run-field-conflict", "with is legal only with uses.", fields.get("with")?.value);
|
|
523
|
+
let run;
|
|
524
|
+
try {
|
|
525
|
+
run = canonicalizeWorkflowRun(reader.string(runPair.value, "step.run"));
|
|
526
|
+
}
|
|
527
|
+
catch (cause) {
|
|
528
|
+
semanticReaderFail(reader, cause, runPair.value);
|
|
529
|
+
}
|
|
530
|
+
const shell = fields.has("shell") ? reader.requiredString(fields, "shell", "step") : undefined;
|
|
531
|
+
if (shell !== undefined && !HOST_SHELLS.has(shell))
|
|
532
|
+
reader.fail("unsupported-shell", `Unsupported shell ${shell}.`, fields.get("shell")?.value);
|
|
533
|
+
let workingDirectory = fields.has("working-directory")
|
|
534
|
+
? reader.requiredString(fields, "working-directory", "step")
|
|
535
|
+
: undefined;
|
|
536
|
+
if (workingDirectory !== undefined) {
|
|
537
|
+
try {
|
|
538
|
+
workingDirectory = canonicalizeWorkflowWorkingDirectory(workingDirectory, options.workspaceRoot);
|
|
539
|
+
}
|
|
540
|
+
catch (cause) {
|
|
541
|
+
semanticReaderFail(reader, cause, fields.get("working-directory")?.value);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
...common,
|
|
546
|
+
run,
|
|
547
|
+
...(shell ? { shell: shell } : {}),
|
|
548
|
+
...(workingDirectory ? { workingDirectory } : {}),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function parseScalarMap(reader, pair, context, keyPattern, allowNull) {
|
|
552
|
+
if (!pair)
|
|
553
|
+
return undefined;
|
|
554
|
+
const fields = reader.arbitraryFields(pair.value, context);
|
|
555
|
+
const out = {};
|
|
556
|
+
for (const [key, valuePair] of fields) {
|
|
557
|
+
if (!keyPattern.test(key))
|
|
558
|
+
reader.fail("invalid-mapping-key", `${context} has invalid key ${JSON.stringify(key)}.`, valuePair.key);
|
|
559
|
+
out[key] = reader.scalar(valuePair.value, `${context}.${key}`, allowNull);
|
|
560
|
+
}
|
|
561
|
+
return out;
|
|
562
|
+
}
|
|
563
|
+
function wholeSourceSpan(source, filePath) {
|
|
564
|
+
return { path: filePath, start: 1, end: Math.max(1, source.split(/\r?\n/).length) };
|
|
565
|
+
}
|
|
566
|
+
function spanAtOffset(filePath, counter, offset) {
|
|
567
|
+
const line = counter.linePos(Math.max(0, offset)).line;
|
|
568
|
+
return { path: filePath, start: line, end: line };
|
|
569
|
+
}
|
|
570
|
+
function cleanYamlMessage(message) {
|
|
571
|
+
return message.replace(/\s+at line \d+, column \d+:[\s\S]*$/i, "").trim();
|
|
572
|
+
}
|
|
573
|
+
function semanticReaderFail(reader, cause, node) {
|
|
574
|
+
if (cause instanceof WorkflowSourceSemanticError)
|
|
575
|
+
reader.fail(cause.code, cause.message, node);
|
|
576
|
+
reader.fail("invalid-workflow-source", cause instanceof Error ? cause.message : String(cause), node);
|
|
577
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
/** The one canonical job ordering: emit one lexical ready job, then recompute readiness. */
|
|
5
|
+
export function canonicalTopologicalJobs(jobs) {
|
|
6
|
+
const byId = new Map(jobs.map((job) => [job.id, job]));
|
|
7
|
+
for (const job of jobs) {
|
|
8
|
+
for (const dependency of job.needs) {
|
|
9
|
+
if (!byId.has(dependency)) {
|
|
10
|
+
return { ok: false, kind: "missing", job, dependency };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const ordered = [];
|
|
15
|
+
const emitted = new Set();
|
|
16
|
+
while (ordered.length < jobs.length) {
|
|
17
|
+
const ready = jobs
|
|
18
|
+
.filter((job) => !emitted.has(job.id) && job.needs.every((need) => emitted.has(need)))
|
|
19
|
+
.sort((left, right) => compareCodePoints(left.id, right.id))[0];
|
|
20
|
+
if (!ready) {
|
|
21
|
+
const cyclic = jobs
|
|
22
|
+
.filter((job) => !emitted.has(job.id))
|
|
23
|
+
.sort((left, right) => compareCodePoints(left.id, right.id))[0];
|
|
24
|
+
if (!cyclic)
|
|
25
|
+
throw new Error("Dependency ordering stalled without a remaining job.");
|
|
26
|
+
return { ok: false, kind: "cycle", job: cyclic };
|
|
27
|
+
}
|
|
28
|
+
ordered.push(ready);
|
|
29
|
+
emitted.add(ready.id);
|
|
30
|
+
}
|
|
31
|
+
return { ok: true, jobs: ordered };
|
|
32
|
+
}
|
|
33
|
+
export function compareWorkflowSourceCodePoints(left, right) {
|
|
34
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
35
|
+
}
|
|
36
|
+
function compareCodePoints(left, right) {
|
|
37
|
+
return compareWorkflowSourceCodePoints(left, right);
|
|
38
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
import { parseBuiltinCommandAction } from "../../commands/command/builtin-action.js";
|
|
5
|
+
import { applyPortableCommandArguments } from "../../commands/command/portable-template.js";
|
|
6
|
+
/** Lower one adapter-neutral source-IR step into the shared execution unit. */
|
|
7
|
+
export function sourceStepProgramUnit(source) {
|
|
8
|
+
const unit = {
|
|
9
|
+
...(source.unit ? structuredClone(source.unit) : {}),
|
|
10
|
+
source: sourceStepRef(source),
|
|
11
|
+
};
|
|
12
|
+
if (source.exec)
|
|
13
|
+
unit.exec = { ...source.exec };
|
|
14
|
+
if (source.run !== undefined) {
|
|
15
|
+
unit.exec = {
|
|
16
|
+
command: workflowShellCommand(source.shell ?? "sh", source.run),
|
|
17
|
+
...(source.workingDirectory ? { cwd: source.workingDirectory } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return unit;
|
|
21
|
+
}
|
|
22
|
+
/** Derive the instruction bytes consumed by the one workflow engine. */
|
|
23
|
+
export function sourceStepInstructions(source) {
|
|
24
|
+
if (source.instructions !== undefined)
|
|
25
|
+
return source.instructions;
|
|
26
|
+
if (source.run !== undefined)
|
|
27
|
+
return `Run ${source.run}.`;
|
|
28
|
+
if (source.uses === "akm/command") {
|
|
29
|
+
const action = parseBuiltinCommandAction(source.with);
|
|
30
|
+
if (action.kind === "stored") {
|
|
31
|
+
return `Invoke stored command ${action.ref}${action.arguments === undefined ? "" : " with arguments"}.`;
|
|
32
|
+
}
|
|
33
|
+
if (source.commandMode === "literal")
|
|
34
|
+
return action.content;
|
|
35
|
+
return applyPortableCommandArguments(action.content, action.arguments, "inline workflow command").content;
|
|
36
|
+
}
|
|
37
|
+
if (source.uses !== undefined)
|
|
38
|
+
return `Invoke local target ${source.uses}.`;
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
export function workflowShellCommand(shell, content) {
|
|
42
|
+
if (shell === "cmd")
|
|
43
|
+
return ["cmd", "/d", "/s", "/c", content];
|
|
44
|
+
if (shell === "pwsh" || shell === "powershell")
|
|
45
|
+
return [shell, "-Command", content];
|
|
46
|
+
return [shell, "-c", content];
|
|
47
|
+
}
|
|
48
|
+
export function sourceStepRef(source) {
|
|
49
|
+
return { path: source.source.path, start: source.source.start, end: source.source.end };
|
|
50
|
+
}
|