@olivervea/pipelines 2.0.0-beta.33 → 2.0.0-beta.34

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/README.md CHANGED
@@ -241,7 +241,7 @@ export const deployBeta = implement(step({ name: "deploy-beta", /* … */ }), as
241
241
  | `ctx.` | |
242
242
  |---|---|
243
243
  | `sh(cmd \| script, { env, cwd, args })`, `sh.text(…)` | Runs a command string, or an embedded script, through the image's shell. The script is copied to `$OLVE_DIR/tmp/scripts/` first. The shell is the first of `/bin/sh`, `/busybox/sh`, `/bin/bash` that exists; a shell-less image gets a clear `ShellNotFoundError`. A script's shebang is honoured when its interpreter exists. A string counts as an embedded script when it is a `/$bunfs/…` path, or an absolute path to an existing `*.sh` or `*.bash` file. Pass `{ script }` to force script mode. `args` are the script's arguments, or `$1`, `$2`… of a command string. A non-zero exit throws a `CommandError` that carries `exitCode`. |
244
- | `exec([argv…], { env, cwd, idleTimeoutSeconds })` | Runs a binary directly, with no shell (works on distroless). `idleTimeoutSeconds` (also on `sh`) is an inactivity watchdog: no stdout/stderr output for that long → SIGTERM, SIGKILL 10 s later, and a `CommandStalledError`. |
244
+ | `exec([argv…], { env, cwd, idleTimeoutSeconds })` | Runs a binary directly, with no shell (works on distroless). `idleTimeoutSeconds` (also on `sh`) is an inactivity watchdog: no stdout/stderr output for that long → SIGTERM, SIGKILL 10 s later, and a `CommandStalledError`. Once a command exits, its output is read for at most 5 s more, so a leftover background child holding the pipes doesn't hang the step. |
245
245
  | `readText(path)`, `readJson(path)` | Paths are relative to the cwd. |
246
246
  | `files.fetch(ref)` | Downloads a file reference and verifies its sha256. Directory refs are extracted. Returns a local path. |
247
247
  | `output.write(name, content)`, `output.file(path, { name })`, `output.dir(path, { name })` | Adds a file to the step's output and returns its `FileRef`. A directory becomes a deterministic tar. |
@@ -343,7 +343,7 @@ This section is everything the C# side needs in order to run the binary.
343
343
  | `OLVE_FILES_BASE_URL` (+ `OLVE_FILES_TOKEN`) | optional | Fallback: `GET <base>/<run>/<step>/<attempt>/<object path>` with an optional `Authorization: Bearer <token>`. |
344
344
  | `OLVE_FILES_DIR` | dev | Local pool laid out as `<run>/<step>/<attempt>/files/<object path>`. |
345
345
 
346
- `OLVE_RESULT_URL`, `OLVE_FILE_URLS`, `OLVE_FILES_TOKEN` and `OLVE_CALLBACK_URLS` are removed from the environment of child processes. URLs are never logged past `?`.
346
+ The variables in this table (the `OLVE_*` contract, credentials included) are removed from the environment of child processes `ctx.sh` and `ctx.exec` start; declared secrets are not. URLs are never logged past `?`.
347
347
 
348
348
  **Secret masking** (`run`): with `OLVE_SECRETS` set, the binary runs the step as its own child and replaces every value of those secrets with `***` in the child's stdout and stderr, a line at a time, from the pipeline's first import to its exit. That covers every way of printing: logs, `console`, direct writes, commands and the processes they start. The envelope's failure message, skip reason and links are masked too; outputs and files are data and aren't. A multi-line value is masked line by line, and values (or lines) shorter than 4 characters are left alone. Not masked: a value the step transforms before printing (escaped, encoded, cut short) and a process writing to the pod's stdout directly (`/proc/1/fd/1`).
349
349
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olivervea/pipelines",
3
- "version": "2.0.0-beta.33",
3
+ "version": "2.0.0-beta.34",
4
4
  "description": "Olve.Pipelines v2 SDK: typed pipeline authoring, the step harness, and the build tool.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/ctx/sh.ts CHANGED
@@ -52,6 +52,9 @@ export class CommandStalledError extends CommandError {
52
52
  /** Grace between the watchdog's SIGTERM and SIGKILL (a process blocked in a socket read may ignore TERM). */
53
53
  export const STALL_KILL_GRACE_MS = 10_000;
54
54
 
55
+ /** How long output is still read after a command exits; a leftover child (an MSBuild node) may hold the pipes. */
56
+ export const PIPE_GRACE_MS = 5_000;
57
+
55
58
  export function detectShell(candidates: readonly string[] = SHELL_CANDIDATES): string {
56
59
  for (const c of candidates) if (existsSync(c)) return c;
57
60
  throw new ShellNotFoundError();
@@ -80,6 +83,7 @@ export interface ShDeps {
80
83
  tmpDir: string;
81
84
  signal: AbortSignal;
82
85
  shellCandidates?: readonly string[];
86
+ pipeGraceMs?: number;
83
87
  }
84
88
 
85
89
  export function createSh(deps: ShDeps): {
@@ -208,9 +212,16 @@ export function createSh(deps: ShDeps): {
208
212
 
209
213
  try {
210
214
  const code = await proc.exited;
211
- // A killed command's children may still hold the pipes open: don't wait for them after a stall.
212
- if (stalled) await Promise.all(readers.map((r) => r.cancel().catch(() => {})));
213
- else await Promise.all(pumps);
215
+ // A command's leftover children may still hold the pipes open: don't wait for them long.
216
+ if (!stalled) {
217
+ let grace: ReturnType<typeof setTimeout> | undefined;
218
+ await Promise.race([
219
+ Promise.all(pumps),
220
+ new Promise((r) => (grace = setTimeout(r, deps.pipeGraceMs ?? PIPE_GRACE_MS))),
221
+ ]);
222
+ clearTimeout(grace);
223
+ }
224
+ await Promise.all(readers.map((r) => r.cancel().catch(() => {})));
214
225
  if (stalled)
215
226
  throw new CommandStalledError(
216
227
  what,
@@ -25,12 +25,12 @@ export const ENV = {
25
25
  filesDir: 'OLVE_FILES_DIR',
26
26
  } as const;
27
27
 
28
- /** Env vars never passed to child processes (they carry credentials for the harness only). */
28
+ /** Env vars never passed to child processes: the harness's own contract, credentials included. */
29
29
  export const PRIVATE_ENV: readonly string[] = [
30
- ENV.resultUrl,
31
- ENV.fileUrls,
32
- ENV.filesToken,
33
- ENV.callbackUrls,
30
+ ...Object.values(ENV),
31
+ 'OLVE_CASES',
32
+ 'OLVE_SECRETS',
33
+ 'OLVE_SUPERVISED',
34
34
  ];
35
35
 
36
36
  /** `OLVE_CALLBACK_URLS`, parsed; empty when unset or malformed. */