nx 23.2.1 → 23.3.0-beta.1

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.
Files changed (149) hide show
  1. package/dist/src/command-line/format/format.js +2 -18
  2. package/dist/src/command-line/graph/graph.js +42 -3
  3. package/dist/src/command-line/init/implementation/format.js +2 -3
  4. package/dist/src/command-line/migrate/agentic/definitions.js +75 -19
  5. package/dist/src/command-line/migrate/agentic/format-command.d.ts +14 -0
  6. package/dist/src/command-line/migrate/agentic/format-command.js +44 -0
  7. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +3 -3
  8. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +3 -3
  9. package/dist/src/command-line/migrate/agentic/handoff.d.ts +27 -17
  10. package/dist/src/command-line/migrate/agentic/handoff.js +107 -24
  11. package/dist/src/command-line/migrate/agentic/instruction-files.d.ts +21 -0
  12. package/dist/src/command-line/migrate/agentic/instruction-files.js +36 -0
  13. package/dist/src/command-line/migrate/agentic/prompts/fragments.d.ts +16 -0
  14. package/dist/src/command-line/migrate/agentic/prompts/fragments.js +70 -0
  15. package/dist/src/command-line/migrate/agentic/prompts/generic-validation.js +4 -3
  16. package/dist/src/command-line/migrate/agentic/prompts/system-prompt.d.ts +25 -1
  17. package/dist/src/command-line/migrate/agentic/prompts/system-prompt.js +57 -34
  18. package/dist/src/command-line/migrate/agentic/run-step.js +28 -4
  19. package/dist/src/command-line/migrate/agentic/runner.d.ts +17 -8
  20. package/dist/src/command-line/migrate/agentic/runner.js +110 -55
  21. package/dist/src/command-line/migrate/agentic/types.d.ts +23 -8
  22. package/dist/src/command-line/migrate/agentic/types.js +10 -7
  23. package/dist/src/command-line/migrate/migrate-commits.d.ts +1 -0
  24. package/dist/src/command-line/migrate/migrate-commits.js +9 -0
  25. package/dist/src/command-line/migrate/migrate-output.d.ts +3 -7
  26. package/dist/src/command-line/migrate/migrate-output.js +3 -7
  27. package/dist/src/command-line/migrate/migrate.js +21 -8
  28. package/dist/src/command-line/migrate/run/agent-output.d.ts +10 -0
  29. package/dist/src/command-line/migrate/run/agent-output.js +16 -2
  30. package/dist/src/command-line/migrate/run/agent-work-payload.d.ts +37 -0
  31. package/dist/src/command-line/migrate/run/agent-work-payload.js +142 -0
  32. package/dist/src/command-line/migrate/run/atomic-write.d.ts +8 -0
  33. package/dist/src/command-line/migrate/run/atomic-write.js +17 -0
  34. package/dist/src/command-line/migrate/run/index.d.ts +1 -1
  35. package/dist/src/command-line/migrate/run/issues.d.ts +106 -0
  36. package/dist/src/command-line/migrate/run/issues.js +980 -0
  37. package/dist/src/command-line/migrate/run/orchestrator.d.ts +1 -0
  38. package/dist/src/command-line/migrate/run/orchestrator.js +662 -105
  39. package/dist/src/command-line/migrate/run/run-state.d.ts +43 -8
  40. package/dist/src/command-line/migrate/run/run-state.js +184 -20
  41. package/dist/src/command-line/migrate/run/runbook.d.ts +17 -0
  42. package/dist/src/command-line/migrate/run/runbook.js +97 -0
  43. package/dist/src/command-line/migrate/run/state-lock.d.ts +2 -2
  44. package/dist/src/command-line/migrate/run/state-lock.js +2 -2
  45. package/dist/src/command-line/migrate/run/state-machine.d.ts +5 -1
  46. package/dist/src/command-line/migrate/run/state-machine.js +27 -1
  47. package/dist/src/command-line/migrate/run/worker.js +206 -56
  48. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +5 -1
  49. package/dist/src/command-line/release/publish.js +11 -4
  50. package/dist/src/command-line/release/utils/remote-release-clients/github.js +3 -3
  51. package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +3 -3
  52. package/dist/src/command-line/release/utils/remote-release-clients/remote-release-client.d.ts +2 -0
  53. package/dist/src/command-line/release/utils/remote-release-clients/remote-release-client.js +35 -0
  54. package/dist/src/command-line/yargs-utils/shared-options.d.ts +1 -1
  55. package/dist/src/command-line/yargs-utils/shared-options.js +22 -4
  56. package/dist/src/config/workspace-json-project-json.d.ts +2 -0
  57. package/dist/src/core/graph/main.js +1 -1
  58. package/dist/src/daemon/client/client.d.ts +4 -2
  59. package/dist/src/daemon/client/client.js +20 -11
  60. package/dist/src/daemon/message-types/run-tasks-execution-hooks.d.ts +3 -2
  61. package/dist/src/daemon/server/handle-hash-tasks.d.ts +8 -2
  62. package/dist/src/daemon/server/handle-hash-tasks.js +16 -3
  63. package/dist/src/daemon/server/handle-outputs-changes.d.ts +2 -2
  64. package/dist/src/daemon/server/handle-outputs-changes.js +21 -6
  65. package/dist/src/daemon/server/handle-tasks-execution-hooks.d.ts +3 -2
  66. package/dist/src/daemon/server/outputs-tracking.d.ts +6 -0
  67. package/dist/src/daemon/server/outputs-tracking.js +18 -0
  68. package/dist/src/daemon/server/project-graph-incremental-recomputation.d.ts +22 -7
  69. package/dist/src/daemon/server/project-graph-incremental-recomputation.js +99 -69
  70. package/dist/src/daemon/server/restart-checks.d.ts +22 -0
  71. package/dist/src/daemon/server/restart-checks.js +77 -0
  72. package/dist/src/daemon/server/server.js +28 -18
  73. package/dist/src/daemon/server/shutdown-utils.d.ts +0 -5
  74. package/dist/src/daemon/server/shutdown-utils.js +3 -26
  75. package/dist/src/hasher/hash-task.js +20 -16
  76. package/dist/src/hasher/native-task-hasher-impl.d.ts +8 -0
  77. package/dist/src/hasher/native-task-hasher-impl.js +46 -3
  78. package/dist/src/hasher/task-hasher.d.ts +9 -0
  79. package/dist/src/hasher/task-hasher.js +14 -0
  80. package/dist/src/native/index.d.ts +202 -21
  81. package/dist/src/native/index.js +3 -3
  82. package/dist/src/native/native-bindings.js +3 -1
  83. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  84. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  85. package/dist/src/plugins/js/lock-file/pnpm-parser.js +1 -1
  86. package/dist/src/plugins/js/lock-file/utils/pnpm-normalizer.d.ts +2 -0
  87. package/dist/src/plugins/js/lock-file/utils/pnpm-normalizer.js +22 -0
  88. package/dist/src/plugins/js/utils/register.js +2 -1
  89. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +2 -1
  90. package/dist/src/project-graph/plugins/isolation/messaging.d.ts +3 -2
  91. package/dist/src/project-graph/plugins/isolation/plugin-worker.js +5 -1
  92. package/dist/src/project-graph/plugins/loaded-nx-plugin.d.ts +3 -2
  93. package/dist/src/project-graph/plugins/loaded-nx-plugin.js +5 -1
  94. package/dist/src/project-graph/plugins/task-results-stub.d.ts +50 -0
  95. package/dist/src/project-graph/plugins/task-results-stub.js +111 -0
  96. package/dist/src/project-graph/plugins/tasks-execution-hooks.d.ts +3 -2
  97. package/dist/src/project-graph/plugins/tasks-execution-hooks.js +2 -1
  98. package/dist/src/project-graph/project-graph.js +2 -0
  99. package/dist/src/tasks-runner/batch/run-batch.js +15 -0
  100. package/dist/src/tasks-runner/cache.d.ts +35 -0
  101. package/dist/src/tasks-runner/cache.js +40 -0
  102. package/dist/src/tasks-runner/create-task-graph.js +1 -0
  103. package/dist/src/tasks-runner/default-tasks-runner.js +1 -1
  104. package/dist/src/tasks-runner/forked-process-task-runner.d.ts +1 -1
  105. package/dist/src/tasks-runner/forked-process-task-runner.js +8 -7
  106. package/dist/src/tasks-runner/init-tasks-runner.js +5 -1
  107. package/dist/src/tasks-runner/is-tui-enabled.js +4 -3
  108. package/dist/src/tasks-runner/life-cycle.d.ts +7 -0
  109. package/dist/src/tasks-runner/life-cycle.js +7 -0
  110. package/dist/src/tasks-runner/life-cycles/summary-terminal-output-life-cycle.d.ts +35 -0
  111. package/dist/src/tasks-runner/life-cycles/summary-terminal-output-life-cycle.js +146 -0
  112. package/dist/src/tasks-runner/prune-task-graph.d.ts +17 -0
  113. package/dist/src/tasks-runner/prune-task-graph.js +45 -0
  114. package/dist/src/tasks-runner/run-command.js +14 -3
  115. package/dist/src/tasks-runner/running-tasks/batch-process.d.ts +93 -19
  116. package/dist/src/tasks-runner/running-tasks/batch-process.js +184 -88
  117. package/dist/src/tasks-runner/task-orchestrator.d.ts +64 -2
  118. package/dist/src/tasks-runner/task-orchestrator.js +232 -46
  119. package/dist/src/tasks-runner/terminal-output-path.d.ts +11 -0
  120. package/dist/src/tasks-runner/terminal-output-path.js +18 -0
  121. package/dist/src/tasks-runner/utils.js +5 -2
  122. package/dist/src/utils/ab-testing.d.ts +3 -0
  123. package/dist/src/utils/ab-testing.js +11 -3
  124. package/dist/src/utils/chunkify.d.ts +1 -7
  125. package/dist/src/utils/chunkify.js +2 -8
  126. package/dist/src/utils/command-line-utils.d.ts +19 -0
  127. package/dist/src/utils/command-line-utils.js +10 -3
  128. package/dist/src/utils/formatters/index.d.ts +6 -0
  129. package/dist/src/utils/formatters/index.js +5 -0
  130. package/dist/src/utils/formatters/prettier.d.ts +0 -8
  131. package/dist/src/utils/formatters/prettier.js +4 -26
  132. package/dist/src/utils/installed-nx-version.js +2 -48
  133. package/dist/src/utils/min-release-age/packument.js +1 -1
  134. package/dist/src/utils/output.d.ts +22 -3
  135. package/dist/src/utils/output.js +28 -5
  136. package/dist/src/utils/package-json.d.ts +25 -0
  137. package/dist/src/utils/package-json.js +41 -6
  138. package/dist/src/utils/package-manager.d.ts +6 -0
  139. package/dist/src/utils/package-manager.js +10 -0
  140. package/dist/src/utils/provenance.js +20 -4
  141. package/dist/src/utils/workspace-context.d.ts +47 -3
  142. package/dist/src/utils/workspace-context.js +162 -10
  143. package/package.json +12 -12
  144. package/schemas/nx-schema.json +4 -0
  145. package/schemas/project-schema.json +4 -0
  146. package/dist/src/daemon/server/file-watching/route-workspace-changes.d.ts +0 -9
  147. package/dist/src/daemon/server/file-watching/route-workspace-changes.js +0 -76
  148. package/dist/src/daemon/server/watcher.d.ts +0 -18
  149. package/dist/src/daemon/server/watcher.js +0 -126
@@ -0,0 +1,17 @@
1
+ import type { TaskGraph } from '../config/task-graph';
2
+ /**
3
+ * Everything written onto a task as a run progresses: hashes and hash
4
+ * details by the hasher, timings by the orchestrator, terminal output by the
5
+ * Nx Cloud life cycle. Nothing that receives a task graph over a socket or
6
+ * a fork needs any of it. The daemon recomputes hashes, and a worker learns
7
+ * its own from `NX_TASK_HASH`; hash details are read only by Nx Cloud on the
8
+ * runner side.
9
+ */
10
+ export declare const TASK_RESULT_FIELDS: readonly ['hash', 'hashDetails', 'startTime', 'endTime', 'terminalOutput'];
11
+ /**
12
+ * A copy of the graph with `TASK_RESULT_FIELDS` removed from every task, made
13
+ * once per graph object. Structure and the remaining fields are fixed once a
14
+ * graph is built and only result fields change during a run, so the copy
15
+ * never goes stale, and the source graph is untouched.
16
+ */
17
+ export declare function pruneTaskGraph(taskGraph: TaskGraph): TaskGraph;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TASK_RESULT_FIELDS = void 0;
4
+ exports.pruneTaskGraph = pruneTaskGraph;
5
+ /**
6
+ * Everything written onto a task as a run progresses: hashes and hash
7
+ * details by the hasher, timings by the orchestrator, terminal output by the
8
+ * Nx Cloud life cycle. Nothing that receives a task graph over a socket or
9
+ * a fork needs any of it. The daemon recomputes hashes, and a worker learns
10
+ * its own from `NX_TASK_HASH`; hash details are read only by Nx Cloud on the
11
+ * runner side.
12
+ */
13
+ exports.TASK_RESULT_FIELDS = [
14
+ 'hash',
15
+ 'hashDetails',
16
+ 'startTime',
17
+ 'endTime',
18
+ 'terminalOutput',
19
+ ];
20
+ const prunedGraphs = new WeakMap();
21
+ /**
22
+ * A copy of the graph with `TASK_RESULT_FIELDS` removed from every task, made
23
+ * once per graph object. Structure and the remaining fields are fixed once a
24
+ * graph is built and only result fields change during a run, so the copy
25
+ * never goes stale, and the source graph is untouched.
26
+ */
27
+ function pruneTaskGraph(taskGraph) {
28
+ let pruned = prunedGraphs.get(taskGraph);
29
+ if (pruned)
30
+ return pruned;
31
+ const tasks = {};
32
+ for (const id in taskGraph.tasks) {
33
+ const task = taskGraph.tasks[id];
34
+ const copy = {};
35
+ for (const key in task) {
36
+ if (!exports.TASK_RESULT_FIELDS.includes(key)) {
37
+ copy[key] = task[key];
38
+ }
39
+ }
40
+ tasks[id] = copy;
41
+ }
42
+ pruned = { ...taskGraph, tasks };
43
+ prunedGraphs.set(taskGraph, pruned);
44
+ return pruned;
45
+ }
@@ -35,6 +35,7 @@ const dynamic_run_many_terminal_output_life_cycle_1 = require("./life-cycles/dyn
35
35
  const dynamic_run_one_terminal_output_life_cycle_1 = require("./life-cycles/dynamic-run-one-terminal-output-life-cycle");
36
36
  const static_run_many_terminal_output_life_cycle_1 = require("./life-cycles/static-run-many-terminal-output-life-cycle");
37
37
  const static_run_one_terminal_output_life_cycle_1 = require("./life-cycles/static-run-one-terminal-output-life-cycle");
38
+ const summary_terminal_output_life_cycle_1 = require("./life-cycles/summary-terminal-output-life-cycle");
38
39
  const store_run_information_life_cycle_1 = require("./life-cycles/store-run-information-life-cycle");
39
40
  const task_history_life_cycle_1 = require("./life-cycles/task-history-life-cycle");
40
41
  const task_profiling_life_cycle_1 = require("./life-cycles/task-profiling-life-cycle");
@@ -265,8 +266,14 @@ async function getTerminalOutputLifeCycle(initiatingProject, initiatingTasks, pr
265
266
  renderIsDone,
266
267
  };
267
268
  }
269
+ if (nxArgs.resolvedOutputStyle === 'summary') {
270
+ return {
271
+ lifeCycle: new summary_terminal_output_life_cycle_1.SummaryTerminalOutputLifeCycle(tasks),
272
+ renderIsDone: Promise.resolve(),
273
+ };
274
+ }
268
275
  const { runnerOptions } = getRunner(nxArgs, nxJson);
269
- const useDynamicOutput = shouldUseDynamicLifeCycle(tasks, runnerOptions, nxArgs.outputStyle);
276
+ const useDynamicOutput = shouldUseDynamicLifeCycle(tasks, runnerOptions, nxArgs.specifiedOutputStyle);
270
277
  if (isRunOne) {
271
278
  if (useDynamicOutput) {
272
279
  return await (0, dynamic_run_one_terminal_output_life_cycle_1.createRunOneDynamicOutputRenderer)({
@@ -641,12 +648,12 @@ function setEnvVarsBasedOnArgs(nxArgs, loadDotEnvFiles) {
641
648
  // grouping applies, let it win over the implicit request; an output style the
642
649
  // user asked for explicitly still wins over grouping.
643
650
  const batchMode = process.env.NX_BATCH_MODE === 'true' || nxArgs.batch;
644
- if (nxArgs.outputStyle == 'stream' ||
651
+ if (nxArgs.specifiedOutputStyle == 'stream' ||
645
652
  (batchMode && !(0, output_1.isLogGroupingEnabled)())) {
646
653
  process.env.NX_STREAM_OUTPUT = 'true';
647
654
  process.env.NX_PREFIX_OUTPUT = 'true';
648
655
  }
649
- if (nxArgs.outputStyle == 'stream-without-prefixes') {
656
+ if (nxArgs.specifiedOutputStyle == 'stream-without-prefixes') {
650
657
  process.env.NX_STREAM_OUTPUT = 'true';
651
658
  process.env.NX_PREFIX_OUTPUT = 'false';
652
659
  }
@@ -733,6 +740,9 @@ async function invokeTasksRunner({ tasks, projectGraph, taskGraph, lifeCycle, nx
733
740
  // per-task-env shape and normalizes internally.
734
741
  return hasher.hashTasks(tasks, taskGraph_, envOrPerTaskEnvs);
735
742
  },
743
+ hashTasksUpfront(tasks, taskGraph_, perTaskEnvs) {
744
+ return hasher.hashTasksUpfront(tasks, taskGraph_, perTaskEnvs);
745
+ },
736
746
  },
737
747
  daemon: client_1.daemonClient,
738
748
  });
@@ -789,6 +799,7 @@ function shouldUseDynamicLifeCycle(tasks, options, outputStyle) {
789
799
  if ((0, is_ci_1.isCI)())
790
800
  return false;
791
801
  if ((0, output_1.isStaticOutputStyle)(outputStyle) ||
802
+ outputStyle === 'summary' ||
792
803
  outputStyle === 'stream' ||
793
804
  outputStyle === 'stream-without-prefixes')
794
805
  return false;
@@ -4,6 +4,20 @@ import { BatchResults } from '../batch/batch-messages';
4
4
  export declare class BatchProcess {
5
5
  private childProcess;
6
6
  private executorName;
7
+ /**
8
+ * Whether the active output style puts task bytes on the terminal at all.
9
+ * `summary` does not, and its life cycle cannot enforce that here: this
10
+ * class writes to `output` directly from a stream handler, so without being
11
+ * told it would print a whole batch into a run that asked for log paths.
12
+ *
13
+ * False makes this capture rather than print. Suppressing the write alone
14
+ * would drop the bytes entirely off GitHub Actions, where nothing else
15
+ * captures them - and the worker's own crash output is exactly what no task
16
+ * ever claims, so it would exist nowhere.
17
+ */
18
+ private readonly printsOutput;
19
+ /** Labels the capture file so it is identifiable in `batchOutputs/`. */
20
+ private readonly batchId;
7
21
  private exitCallbacks;
8
22
  private batchResultsCallbacks;
9
23
  private taskResultsCallbacks;
@@ -24,49 +38,109 @@ export declare class BatchProcess {
24
38
  * length of a JS string.
25
39
  */
26
40
  private capturedOutputPath;
27
- private capturedOutputFd;
41
+ private capturedOutputStream;
42
+ /** Sources paused for backpressure, awaiting a 'drain' that may never come. */
43
+ private readonly pausedSources;
28
44
  /**
29
45
  * Set once the capture is released. A chunk can still arrive after that —
30
- * stdout delivers past the exit event — and reopening then would mint a
31
- * second numbered file that nothing ever cleans up.
46
+ * stdout delivers past the exit event — and writing then would reach a file
47
+ * the renderer has already read.
32
48
  */
33
49
  private capturedOutputDiscarded;
34
50
  /**
35
- * Set when writing the capture failed. Distinct from discarded: the bytes
36
- * already on disk are still the best record of the batch, so the file is kept
37
- * and rendered, while everything after the failure goes live to the terminal.
51
+ * Set when the capture file could not be written. Whatever reached it is kept
52
+ * and still rendered: that is the head of the batch's log, where a compiler's
53
+ * first non-cascading errors are. Everything after it goes to the terminal -
54
+ * see `capture`.
38
55
  */
39
56
  private capturedOutputFailed;
40
- private static capturedOutputCount;
41
- constructor(childProcess: ChildProcess, executorName: string);
57
+ /** Set once the failure has been reported, so it is said once per batch. */
58
+ private capturedOutputWarned;
59
+ /** Set before `end()`: a write after it would destroy the unflushed tail. */
60
+ private capturedOutputClosing;
61
+ private static captureSeq;
62
+ /** Turns `flushCapturedOutput` waits for a resumed source to empty. */
63
+ private static readonly MAX_DRAIN_TURNS;
64
+ /**
65
+ * Makes the capture file unique across every batch in the process, which
66
+ * `batchId` alone does not: `TasksSchedule.batchCounters` is per instance, so
67
+ * a second orchestrator in the same process - `runDiscreteTasks` and
68
+ * `runContinuousTasks`, not the CLI - re-mints `<executor> 1`, and the pid
69
+ * cannot separate them.
70
+ */
71
+ private readonly captureSeq;
72
+ /**
73
+ * Separates this capture from one minted by a DIFFERENT process, which
74
+ * `captureSeq` cannot: it is a per-process static that restarts at 1. These
75
+ * files now outlive their batch and `cacheDir` can be shared across checkouts
76
+ * of a repo, so without this a recycled pid truncates a log an earlier run's
77
+ * summary is still pointing at.
78
+ */
79
+ private readonly captureNonce;
80
+ constructor(childProcess: ChildProcess, executorName: string,
81
+ /**
82
+ * Whether the active output style puts task bytes on the terminal at all.
83
+ * `summary` does not, and its life cycle cannot enforce that here: this
84
+ * class writes to `output` directly from a stream handler, so without being
85
+ * told it would print a whole batch into a run that asked for log paths.
86
+ *
87
+ * False makes this capture rather than print. Suppressing the write alone
88
+ * would drop the bytes entirely off GitHub Actions, where nothing else
89
+ * captures them - and the worker's own crash output is exactly what no task
90
+ * ever claims, so it would exist nowhere.
91
+ */
92
+ printsOutput?: boolean,
93
+ /** Labels the capture file so it is identifiable in `batchOutputs/`. */
94
+ batchId?: string);
42
95
  onExit(cb: (code: number) => void): void;
43
96
  onBatchResults(cb: (results: BatchResults) => void): void;
44
97
  onTaskResults(cb: (task: string, result: TaskResult) => void): void;
45
98
  onOutput(cb: (output: string) => void): void;
46
99
  private capture;
100
+ /**
101
+ * Puts bytes the capture could not take back on the terminal, on the stream
102
+ * they arrived on.
103
+ */
104
+ private forwardUncaptured;
105
+ /**
106
+ * Names the failure once. A stream reports it asynchronously, so the chunk
107
+ * that tripped it may or may not have reached disk - unlike a `writeSync`,
108
+ * there is no byte count to tell. Every chunk after it is forwarded.
109
+ */
110
+ private warnCaptureFailed;
111
+ /**
112
+ * Lets the worker write again after the capture has stopped.
113
+ *
114
+ * Without this a failed capture wedges the whole run: the source stays
115
+ * paused, so the worker blocks once its stdout pipe fills, never sends its
116
+ * results, and `getResults` waits on a batch that can no longer finish.
117
+ */
118
+ private resumeCapturedSources;
119
+ private openCapturedOutput;
120
+ /**
121
+ * Flushes and closes the capture so the file is complete before a caller
122
+ * reads it.
123
+ */
124
+ flushCapturedOutput(): Promise<void>;
47
125
  /**
48
126
  * Path to the file holding everything held back from the live stream under
49
127
  * log grouping, or undefined if nothing was captured. Used to render the whole
50
128
  * batch as one fold, so output no task claimed is not lost.
51
129
  *
52
- * The file is deliberately left open rather than closed here. A worker's
53
- * stdout can deliver after its exit event — which is what `getResults()`
54
- * settles on — and leaving the fd open keeps such a chunk appending to this
55
- * same file instead of minting a second numbered one that nothing cleans up.
56
- * The caller reads the file once, synchronously, while rendering the fold, so
57
- * anything arriving after that read is not shown; writes are unbuffered, so
58
- * the read always sees a complete prefix of what has arrived by then.
130
+ * Not valid to read before `flushCapturedOutput`; that flush ends the stream,
131
+ * so a chunk arriving afterwards - a worker's stdout can deliver past the
132
+ * exit event `getResults()` settles on - is dropped rather than appended.
59
133
  */
60
134
  getCapturedOutputPath(): string | undefined;
61
135
  /** Releases the capture file. Safe to call more than once. */
62
136
  discardCapturedOutput(): void;
63
137
  /**
64
- * Closes the fd and unlinks the file, leaving no path behind for a caller to
65
- * read. Tolerates a partially-initialized capture, since it also runs when
66
- * opening or writing the file is what failed.
138
+ * Destroys the stream and unlinks the file, leaving no path behind for a
139
+ * caller to read. Tolerates a partially-initialized capture, since it also
140
+ * runs when opening or writing the file is what failed.
67
141
  */
68
142
  private releaseCapturedOutput;
69
- /** Closes the fd, keeping the file and its path readable. */
143
+ /** Destroys the stream, dropping anything still buffered. The file stays. */
70
144
  private closeCapturedOutput;
71
145
  getResults(): Promise<BatchResults>;
72
146
  send(message: Serializable): void;
@@ -2,32 +2,72 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BatchProcess = void 0;
4
4
  const fs_1 = require("fs");
5
+ const crypto_1 = require("crypto");
5
6
  const path_1 = require("path");
6
7
  const native_1 = require("../../native");
7
- const cache_directory_1 = require("../../utils/cache-directory");
8
+ const cache_1 = require("../cache");
8
9
  const exit_codes_1 = require("../../utils/exit-codes");
9
10
  const output_1 = require("../../utils/output");
10
11
  const batch_messages_1 = require("../batch/batch-messages");
11
12
  class BatchProcess {
12
- constructor(childProcess, executorName) {
13
+ constructor(childProcess, executorName,
14
+ /**
15
+ * Whether the active output style puts task bytes on the terminal at all.
16
+ * `summary` does not, and its life cycle cannot enforce that here: this
17
+ * class writes to `output` directly from a stream handler, so without being
18
+ * told it would print a whole batch into a run that asked for log paths.
19
+ *
20
+ * False makes this capture rather than print. Suppressing the write alone
21
+ * would drop the bytes entirely off GitHub Actions, where nothing else
22
+ * captures them - and the worker's own crash output is exactly what no task
23
+ * ever claims, so it would exist nowhere.
24
+ */
25
+ printsOutput = true,
26
+ /** Labels the capture file so it is identifiable in `batchOutputs/`. */
27
+ batchId = executorName) {
13
28
  this.childProcess = childProcess;
14
29
  this.executorName = executorName;
30
+ this.printsOutput = printsOutput;
31
+ this.batchId = batchId;
15
32
  this.exitCallbacks = [];
16
33
  this.batchResultsCallbacks = [];
17
34
  this.taskResultsCallbacks = [];
18
35
  this.outputCallbacks = [];
36
+ /** Sources paused for backpressure, awaiting a 'drain' that may never come. */
37
+ this.pausedSources = new Set();
19
38
  /**
20
39
  * Set once the capture is released. A chunk can still arrive after that —
21
- * stdout delivers past the exit event — and reopening then would mint a
22
- * second numbered file that nothing ever cleans up.
40
+ * stdout delivers past the exit event — and writing then would reach a file
41
+ * the renderer has already read.
23
42
  */
24
43
  this.capturedOutputDiscarded = false;
25
44
  /**
26
- * Set when writing the capture failed. Distinct from discarded: the bytes
27
- * already on disk are still the best record of the batch, so the file is kept
28
- * and rendered, while everything after the failure goes live to the terminal.
45
+ * Set when the capture file could not be written. Whatever reached it is kept
46
+ * and still rendered: that is the head of the batch's log, where a compiler's
47
+ * first non-cascading errors are. Everything after it goes to the terminal -
48
+ * see `capture`.
29
49
  */
30
50
  this.capturedOutputFailed = false;
51
+ /** Set once the failure has been reported, so it is said once per batch. */
52
+ this.capturedOutputWarned = false;
53
+ /** Set before `end()`: a write after it would destroy the unflushed tail. */
54
+ this.capturedOutputClosing = false;
55
+ /**
56
+ * Makes the capture file unique across every batch in the process, which
57
+ * `batchId` alone does not: `TasksSchedule.batchCounters` is per instance, so
58
+ * a second orchestrator in the same process - `runDiscreteTasks` and
59
+ * `runContinuousTasks`, not the CLI - re-mints `<executor> 1`, and the pid
60
+ * cannot separate them.
61
+ */
62
+ this.captureSeq = ++BatchProcess.captureSeq;
63
+ /**
64
+ * Separates this capture from one minted by a DIFFERENT process, which
65
+ * `captureSeq` cannot: it is a per-process static that restarts at 1. These
66
+ * files now outlive their batch and `cacheDir` can be shared across checkouts
67
+ * of a repo, so without this a recycled pid truncates a log an earlier run's
68
+ * summary is still pointing at.
69
+ */
70
+ this.captureNonce = (0, crypto_1.randomBytes)(4).toString('hex');
31
71
  this.childProcess.on('message', (message) => {
32
72
  switch (message.type) {
33
73
  case batch_messages_1.BatchMessageType.CompleteTask: {
@@ -70,8 +110,8 @@ class BatchProcess {
70
110
  // current terminal output behavior. These chunks are forwarded raw and
71
111
  // routinely end mid-line, so they go through `output` to keep its line
72
112
  // tracking accurate for whatever prints next.
73
- if ((0, output_1.shouldGroupBatchOutput)()) {
74
- this.capture(chunk);
113
+ if ((0, output_1.shouldGroupBatchOutput)() || !this.printsOutput) {
114
+ this.capture(chunk, this.childProcess.stdout);
75
115
  }
76
116
  else {
77
117
  output_1.output.writeTaskOutputChunk(chunk);
@@ -86,8 +126,8 @@ class BatchProcess {
86
126
  if (this.childProcess.stderr) {
87
127
  this.childProcess.stderr.on('data', (chunk) => {
88
128
  const text = chunk.toString();
89
- if ((0, output_1.shouldGroupBatchOutput)()) {
90
- this.capture(chunk, process.stderr);
129
+ if ((0, output_1.shouldGroupBatchOutput)() || !this.printsOutput) {
130
+ this.capture(chunk, this.childProcess.stderr);
91
131
  }
92
132
  else {
93
133
  // Maintain current terminal output behavior
@@ -112,88 +152,144 @@ class BatchProcess {
112
152
  onOutput(cb) {
113
153
  this.outputCallbacks.push(cb);
114
154
  }
115
- capture(chunk, stream = process.stdout) {
116
- // Only a released capture stops recording; a handed-over one keeps
117
- // appending until the fold is rendered.
118
- if (this.capturedOutputDiscarded) {
155
+ capture(chunk, source) {
156
+ // A closed or released capture drops the chunk; see `getCapturedOutputPath`.
157
+ if (this.capturedOutputDiscarded || this.capturedOutputClosing) {
119
158
  return;
120
159
  }
160
+ // A failed one does not. The style withheld these bytes on the promise that
161
+ // they are readable elsewhere; once the file is unwritable that promise
162
+ // cannot be kept, so they go to the terminal even under a style that prints
163
+ // nothing. Losing the format is survivable, losing task output is what this
164
+ // path exists to prevent.
121
165
  if (this.capturedOutputFailed) {
122
- // Capture is over, but the output is not optional. Everything from here
123
- // goes straight to the terminal, on the stream it arrived on.
124
- output_1.output.writeTaskOutputChunk(chunk, stream);
166
+ this.forwardUncaptured(chunk, source);
167
+ return;
168
+ }
169
+ const stream = this.openCapturedOutput();
170
+ if (!stream) {
171
+ // Opening is what failed, so nothing of this chunk reached disk.
172
+ this.forwardUncaptured(chunk, source);
173
+ return;
174
+ }
175
+ // A full write buffer pauses the worker rather than growing in this
176
+ // process. Tracked, because only 'drain' resumes it and a stream that
177
+ // errors never emits one - see `resumeCapturedSources`.
178
+ if (!stream.write(chunk) && source) {
179
+ this.pausedSources.add(source);
180
+ source.pause();
181
+ stream.once('drain', () => {
182
+ this.pausedSources.delete(source);
183
+ source.resume();
184
+ });
185
+ }
186
+ }
187
+ /**
188
+ * Puts bytes the capture could not take back on the terminal, on the stream
189
+ * they arrived on.
190
+ */
191
+ forwardUncaptured(chunk, source) {
192
+ output_1.output.writeTaskOutputChunk(chunk, source === this.childProcess.stderr ? process.stderr : process.stdout);
193
+ }
194
+ /**
195
+ * Names the failure once. A stream reports it asynchronously, so the chunk
196
+ * that tripped it may or may not have reached disk - unlike a `writeSync`,
197
+ * there is no byte count to tell. Every chunk after it is forwarded.
198
+ */
199
+ warnCaptureFailed(reason) {
200
+ if (this.capturedOutputWarned) {
125
201
  return;
126
202
  }
127
- // Hoisted so the catch can tell how much of this chunk reached the file: a
128
- // `writeSync` that fails partway leaves a prefix on disk, and replaying the
129
- // whole chunk live would print that prefix twice - once now, once when the
130
- // fold reads the file.
131
- const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
132
- let written = 0;
203
+ this.capturedOutputWarned = true;
204
+ output_1.output.warn({
205
+ title: `Could not capture batch output for ${this.executorName}`,
206
+ bodyLines: [reason, 'Streaming the rest of it live instead.'],
207
+ });
208
+ }
209
+ /**
210
+ * Lets the worker write again after the capture has stopped.
211
+ *
212
+ * Without this a failed capture wedges the whole run: the source stays
213
+ * paused, so the worker blocks once its stdout pipe fills, never sends its
214
+ * results, and `getResults` waits on a batch that can no longer finish.
215
+ */
216
+ resumeCapturedSources() {
217
+ const resumed = [...this.pausedSources];
218
+ for (const source of resumed) {
219
+ source.resume();
220
+ }
221
+ this.pausedSources.clear();
222
+ return resumed;
223
+ }
224
+ openCapturedOutput() {
225
+ if (this.capturedOutputStream) {
226
+ return this.capturedOutputStream;
227
+ }
133
228
  try {
134
- if (this.capturedOutputFd === undefined) {
135
- const dir = (0, path_1.join)(cache_directory_1.workspaceDataDirectory, 'batch-outputs');
136
- (0, fs_1.mkdirSync)(dir, { recursive: true });
137
- const name = `${this.executorName.replace(/[^a-zA-Z0-9]+/g, '-')}-${process.pid}-${++BatchProcess.capturedOutputCount}.log`;
138
- const path = (0, path_1.join)(dir, name);
139
- // Assigned only once the file is actually open, so a path always names
140
- // an fd rather than a file that failed to open.
141
- const fd = (0, fs_1.openSync)(path, 'w');
142
- this.capturedOutputPath = path;
143
- this.capturedOutputFd = fd;
144
- }
145
- // Written synchronously so the file is complete the moment the batch ends,
146
- // with no flush to sequence against the read that renders the fold.
147
- while (written < bytes.length) {
148
- written += (0, fs_1.writeSync)(this.capturedOutputFd, bytes, written);
149
- }
229
+ // `batchId` names the file for a human reading the directory;
230
+ // `captureSeq` separates batches within this process and `captureNonce`
231
+ // separates it from every other process - see both fields.
232
+ const key = `${this.batchId.replace(/[^a-zA-Z0-9]+/g, '-')}-${process.pid}-${this.captureSeq}-${this.captureNonce}`;
233
+ const path = (0, cache_1.batchOutputPathForKey)(key);
234
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true });
235
+ const stream = (0, fs_1.createWriteStream)(path);
236
+ // Mandatory, not defensive: a write error reaches a stream as an 'error'
237
+ // event, and an unhandled one is an uncaught exception that would take
238
+ // down a run with nothing else wrong with it. Stop capturing, keep what
239
+ // already reached the file — the head of the batch's log, where a
240
+ // compiler's first non-cascading errors are — and put everything after
241
+ // it on the terminal.
242
+ stream.on('error', (e) => {
243
+ this.capturedOutputFailed = true;
244
+ this.resumeCapturedSources();
245
+ this.warnCaptureFailed(e.message);
246
+ });
247
+ this.capturedOutputPath = path;
248
+ this.capturedOutputStream = stream;
150
249
  }
151
250
  catch (e) {
152
- // This runs inside a stream 'data' handler, where a throw is an uncaught
153
- // exception rather than something the orchestrator's try can see - so a
154
- // full disk or a read-only data directory would take down a run that
155
- // otherwise had nothing wrong with it. The capture is an optimization
156
- // that the common path discards anyway, so give it up and put the bytes
157
- // back on the terminal instead of losing both them and the run.
158
- // Latch on a flag of its own rather than reusing the discard flag: that
159
- // one makes `capture` return early, which would drop every later chunk
160
- // on the floor - captured nowhere and printed nowhere.
251
+ // mkdir is the only synchronous throw here; the stream reports its own
252
+ // failures through the handler above.
161
253
  this.capturedOutputFailed = true;
162
- // Close the fd but keep the file. Whatever was written before the failure
163
- // is still the head of the batch's log, which is where a compiler's first
164
- // non-cascading errors live; unlinking it here would throw away the most
165
- // useful bytes precisely when the disk is in trouble.
166
- this.closeCapturedOutput();
167
- // Forward only what did not reach the file, and do it before warning.
168
- // `output.warn` writes to the terminal as well, so if that is the thing
169
- // failing, the bytes this handler was handed must already be out - losing
170
- // the warning is survivable, losing task output is what this path exists
171
- // to prevent.
172
- const unwritten = bytes.subarray(written);
173
- if (unwritten.length > 0) {
174
- output_1.output.writeTaskOutputChunk(unwritten, stream);
175
- }
176
- output_1.output.warn({
177
- title: `Could not capture batch output for ${this.executorName}`,
178
- bodyLines: [
179
- e.message,
180
- 'Streaming the rest of it live instead; the fold holds what was captured first.',
181
- ],
182
- });
254
+ this.warnCaptureFailed(e.message);
255
+ }
256
+ return this.capturedOutputStream;
257
+ }
258
+ /**
259
+ * Flushes and closes the capture so the file is complete before a caller
260
+ * reads it.
261
+ */
262
+ async flushCapturedOutput() {
263
+ // Before the early return, and unconditionally: `end()` means a pending
264
+ // 'drain' will never arrive, so a source paused for backpressure has
265
+ // nothing left to resume it. Measured: 20/20 runs reach 'finish' with the
266
+ // source still paused.
267
+ const resumed = this.resumeCapturedSources();
268
+ // `resume()` delivers on the next tick while `end()` takes effect in this
269
+ // one, so ending here would strand everything the source still holds - the
270
+ // tail of the log, which is where a build tool puts what went wrong.
271
+ // Bounded, because a source that never empties must not hold the run open.
272
+ for (let turn = 0; turn < BatchProcess.MAX_DRAIN_TURNS &&
273
+ resumed.some((source) => source.readableLength > 0); turn++) {
274
+ await new Promise((resolve) => setImmediate(resolve));
275
+ }
276
+ this.capturedOutputClosing = true;
277
+ // The loop's own writes can re-pause a source.
278
+ this.resumeCapturedSources();
279
+ const stream = this.capturedOutputStream;
280
+ if (!stream || stream.destroyed || stream.writableEnded) {
281
+ return;
183
282
  }
283
+ await new Promise((resolve) => stream.end(resolve));
184
284
  }
185
285
  /**
186
286
  * Path to the file holding everything held back from the live stream under
187
287
  * log grouping, or undefined if nothing was captured. Used to render the whole
188
288
  * batch as one fold, so output no task claimed is not lost.
189
289
  *
190
- * The file is deliberately left open rather than closed here. A worker's
191
- * stdout can deliver after its exit event — which is what `getResults()`
192
- * settles on — and leaving the fd open keeps such a chunk appending to this
193
- * same file instead of minting a second numbered one that nothing cleans up.
194
- * The caller reads the file once, synchronously, while rendering the fold, so
195
- * anything arriving after that read is not shown; writes are unbuffered, so
196
- * the read always sees a complete prefix of what has arrived by then.
290
+ * Not valid to read before `flushCapturedOutput`; that flush ends the stream,
291
+ * so a chunk arriving afterwards - a worker's stdout can deliver past the
292
+ * exit event `getResults()` settles on - is dropped rather than appended.
197
293
  */
198
294
  getCapturedOutputPath() {
199
295
  return this.capturedOutputPath;
@@ -204,9 +300,9 @@ class BatchProcess {
204
300
  this.releaseCapturedOutput();
205
301
  }
206
302
  /**
207
- * Closes the fd and unlinks the file, leaving no path behind for a caller to
208
- * read. Tolerates a partially-initialized capture, since it also runs when
209
- * opening or writing the file is what failed.
303
+ * Destroys the stream and unlinks the file, leaving no path behind for a
304
+ * caller to read. Tolerates a partially-initialized capture, since it also
305
+ * runs when opening or writing the file is what failed.
210
306
  */
211
307
  releaseCapturedOutput() {
212
308
  this.closeCapturedOutput();
@@ -223,14 +319,12 @@ class BatchProcess {
223
319
  this.capturedOutputPath = undefined;
224
320
  }
225
321
  }
226
- /** Closes the fd, keeping the file and its path readable. */
322
+ /** Destroys the stream, dropping anything still buffered. The file stays. */
227
323
  closeCapturedOutput() {
228
- if (this.capturedOutputFd !== undefined) {
229
- try {
230
- (0, fs_1.closeSync)(this.capturedOutputFd);
231
- }
232
- catch { }
233
- this.capturedOutputFd = undefined;
324
+ this.resumeCapturedSources();
325
+ if (this.capturedOutputStream) {
326
+ this.capturedOutputStream.destroy();
327
+ this.capturedOutputStream = undefined;
234
328
  }
235
329
  }
236
330
  // Resolves on the CompleteBatchExecution message, not process exit: the
@@ -263,4 +357,6 @@ class BatchProcess {
263
357
  }
264
358
  }
265
359
  exports.BatchProcess = BatchProcess;
266
- BatchProcess.capturedOutputCount = 0;
360
+ BatchProcess.captureSeq = 0;
361
+ /** Turns `flushCapturedOutput` waits for a resumed source to empty. */
362
+ BatchProcess.MAX_DRAIN_TURNS = 100;