[feature] Add pre-execution JSON output for the resolved `moon ci` action graph
**Is your feature request related to a problem? Please describe.**
CI orchestrators often need to decide which jobs or matrix entries to create *before* launching expensive workers. For a monorepo using Moon, that decision should use the exact same resolution semantics as a real invocation:
```bash
moon ci <explicit-target-roots> \
--base "$BASE_SHA" \
--head "$HEAD_SHA"
```
The distinguishing requirement is **equivalence**: output must come from the same path `moon ci` uses for target allowlisting, affected tracking, CI/`runInCI` checks, graph scopes, execution-plan input, and job partitioning. A separate approximation can drift from real execution.
This is distinct from the input `ExecutionPlan` shipped in v2.1 and consumed through `--plan`. That schema captures execution intent before affected resolution. This request is for the static, resolved action graph after affected and CI resolution, but before any action node executes.
The available interfaces answer related but different questions:
- `moon query projects --affected` is project-path based, not target-input based.
- `moon query affected` tracks workspace-wide affected project/task state without target-root allowlisting or CI `runInCI` filtering.
- `moon action-graph --json` builds a graph without `moon ci` affected selection.
- `moon ci --no-actions` omits sync/setup/install actions but still executes selected task actions.
- `moon ci --plan` consumes execution intent; it does not emit the resolved graph.
- `.moon/cache/ciReport.json` is a runtime outcome report written after a non-empty pipeline completes, which is too late to construct a CI matrix.
As a result, CI users must choose between:
1. launching every worker and letting `moon ci` no-op inside each one;
2. recreating Moon’s affected logic in workflow scripts, which can drift from task inputs and dependency relationships;
3. using a project/query approximation that is not equivalent to `moon ci <roots>`.
A concrete GitHub Actions use case is a planner job that asks Moon for its exact selected primary targets, emits a JSON matrix, and allows a stable required gate to create only the necessary workers. The same output would benefit Buildkite, CircleCI, GitLab CI, local debugging, and external orchestration.
Related prior work:
- #1032 asked for a `moon ci --dry-run`-style listing of tasks triggered by changed files. It was closed after task-level affected querying and documentation were added. In that discussion, upstream/downstream execution closure remained task-runner behavior rather than affected-query output. What is still missing is a pre-execution, machine-readable representation of the full allowlist-respecting `moon ci <roots>` action closure.
- #2378 shipped the input `ExecutionPlan` in v2.1. An AI-generated design comment in that issue also floated an unshipped `--dump-plan` companion. Whether this request reuses that name or needs a distinct resolved-graph output is an interface decision for maintainers.
**Describe the solution you'd like**
Add a structured-output mode to `moon ci` that resolves the same static executable graph as normal CI, validates it, serializes it deterministically, and exits before action-pipeline execution.
The flag and schema names are open to maintainer design. For illustration only:
```bash
moon ci :test :build \
--base "$BASE_SHA" \
--head "$HEAD_SHA" \
--emit-action-graph - > resolved-action-graph.json
```
The semantic contract:
1. Reuse normal changed-file resolution and `ActionGraphBuilder::run_tasks_with_plan`.
2. Apply the same surviving-primary-target empty gate as `moon ci`.
3. For a non-empty graph, apply the same non-executing topological/cycle validation used before pipeline dispatch.
4. Finalize the static planning context, then serialize and return without constructing or running the action pipeline.
5. Represent the same primary roots, action nodes, and dependency edges that would be handed to the pipeline.
6. Do not predict runtime statuses such as cached, passed, skipped, or failed.
When normal affected CI resolution yields no surviving primary targets, emit an empty executable graph and preserve `moon ci`’s existing successful affected-empty status. Do not expose builder-only setup or relation nodes that normal execution discards at its pre-pipeline empty gate. Planning/configuration/revision/cycle errors remain nonzero errors rather than successful empty output.
A useful, versioned JSON document would distinguish:
- Moon/schema version;
- raw CLI target locators and effective execution-plan target input;
- selected primary targets for the current job/partition;
- every task and non-task action in the executable graph;
- explicit dependency edges using stable schema-local IDs;
- affected reasons and effective changed-file/CI metadata where Moon can report them accurately.
All map/set-derived collections should use canonical ordering rather than internal hash-map or node-index order. `--job`/`--job-total` should preserve Moon’s existing behavior: partition expanded affected roots before dependency/dependent closure, which may repeat dependency actions across jobs.
For stdout output, stdout should contain exactly one JSON document; progress, warnings, and errors should use stderr. If direct file output is supported, it should be written only after successful graph validation and should not leave a partial document.
The no-execution guarantee should be scoped to pipeline behavior:
- no `ActionNode` executes;
- no task-output cache entry or CI run report is written;
- no pipeline cleanup runs;
- no project/workspace mutation occurs through sync/setup/install actions.
Normal Moon startup and workspace-graph cache reads/writes may still occur. A strict zero-cache-write startup mode would be a separate feature.
If affected metadata is included, it should report effective—not merely requested—state after environment, CI provider, default, and CLI precedence. For example, `affectedRequested`, `affectedEffective`, and any source-verified disable reason are more useful than echoing raw `--base`/`--head` values.
Replay is not required for this request. The current `ExecutionPlan` is an intent input that Moon resolves again; a resolved action graph is a different contract. Exact replay would require a separate consumer and repository/configuration/toolchain identity guarantees.
Out of scope for the initial feature: human-readable dry-run rendering, cache-hit prediction, CI-provider matrix adapters, plan diffing, and exact graph replay.
**Describe alternatives you've considered**
- **Run `moon ci` inside every always-created CI job.** Correct, but every job still pays runner startup, checkout, toolchain, dependency, and service setup before Moon can no-op.
- **Use `moon query projects --affected`.** Insufficient for relationships represented only by task inputs, generators, migrations, or task dependencies.
- **Use `moon query affected`.** Useful diagnostics, but it has no target-root allowlist or CI `runInCI` filtering and does not expose the same executable closure.
- **Use `moon action-graph --json`.** It does not apply the changed-file/affected filtering performed by `moon ci`.
- **Use `moon ci --no-actions`.** It still executes selected task actions.
- **Read `.moon/cache/ciReport.json`.** Authoritative runtime evidence for completed pipelines, but post-execution and absent for affected-empty early returns.
- **Generate an input `ExecutionPlan` externally.** Requires the external tool to reimplement affected resolution—the behavior this feature should expose.
- **Maintain path classifiers in CI configuration.** Duplicates Moon’s dependency model and drifts as packages, generators, toolchain inputs, and cross-project relationships evolve.
**Additional context**
Implementation appears to have a natural—but slightly stricter than “stop after graph build”—boundary in `ExecWorkflow`:
1. load changed files;
2. build the affected graph and `RunPartition`;
3. apply the existing primary-target empty gate;
4. display affected state;
5. initialize the final static context and validate graph topology/cycles;
6. execute the action pipeline.
The proposed mode would serialize after step 5 and return before step 6. This preserves the graph normal execution would actually hand to the pipeline, rather than serializing builder nodes that the empty-root gate would discard.
Suggested acceptance coverage:
- direct task-input and upstream dependency changes;
- downstream `none`, `direct`, and `deep`;
- `runInCI: false`, `only`, and `always`, including a disallowed primary root with downstream scope;
- explicit, overlapping, and glob target locators;
- input `ExecutionPlan`, including CLI targets supplied alongside non-empty plan targets;
- `--job` / `--job-total`, over-partitioning, and repeated dependency closure;
- stacked/non-default revisions and changed files from stdin;
- VCS disabled, implicit-base shallow, and explicit-base shallow behavior;
- `MOON_BASE`/`MOON_HEAD` and CI-derived revision precedence;
- affected-empty result;
- graph/configuration/revision errors and cycle detection;
- stdout containing byte-for-byte valid JSON under CI’s detailed-summary default;
- verification that no action node ran and no task-output cache/report was written.
For non-empty successful cases, an integration test can compare qualified run-task targets against a freshly generated CI report while validating non-task nodes, edges, and cycle handling directly at the planning workflow boundary. Empty cases should assert an empty dumped executable graph without expecting a CI report.
This proposal intentionally does not prescribe GitHub-specific matrix grouping. Moon should expose its canonical primary targets and static action graph; CI-specific grouping can remain the caller’s responsibility.
2 条评论