@indigoai-us/hq-cli 5.103.0 → 5.103.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.
- package/CHANGELOG.md +15 -0
- package/dist/commands/secrets.d.ts +26 -0
- package/dist/commands/secrets.js +49 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.103.1] — 2026-08-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq secrets env` now terminates deterministically instead of leaking a hung
|
|
10
|
+
process. The prior fix exited inside the stdout write callback
|
|
11
|
+
(`process.stdout.write(payload, () => process.exit(0))`), which only fires
|
|
12
|
+
once the bytes flush to a live reader. When the parent shell running
|
|
13
|
+
`source <(hq secrets env …)` was killed on timeout, the stdout pipe lost its
|
|
14
|
+
reader, the callback never fired, and the process parked forever — reparented
|
|
15
|
+
to PID 1. Hundreds accumulated on shared build hosts, exhausting swap and
|
|
16
|
+
spiking load. `env` now writes synchronously to stdout and exits
|
|
17
|
+
unconditionally, with no dependence on the event loop draining or the reader
|
|
18
|
+
still being attached (a gone reader surfaces as EPIPE and is ignored).
|
|
19
|
+
|
|
5
20
|
## [5.103.0] — 2026-08-18
|
|
6
21
|
|
|
7
22
|
### Added
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { writeSync } from "node:fs";
|
|
2
3
|
import { vaultApiFetch, getCompanyUid, getEntityUid } from "../utils/vault-api.js";
|
|
3
4
|
export type { VaultApiOptions } from "../utils/vault-api.js";
|
|
4
5
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
@@ -51,5 +52,30 @@ export declare function parseDestinationUrl(raw: string): {
|
|
|
51
52
|
export declare function collectSecretNames(value: string, previous?: string[]): string[];
|
|
52
53
|
export declare function scrubSandboxOutput(text: string, secretNames?: string[]): string;
|
|
53
54
|
export declare function loadRevealedSecrets(token: string, companyUid: string, keys: string[], usage?: SecretUsage): Promise<Map<string, string>>;
|
|
55
|
+
/**
|
|
56
|
+
* Emit `hq secrets env` output and terminate deterministically.
|
|
57
|
+
*
|
|
58
|
+
* The process MUST exit even when the consuming pipe has gone away. The
|
|
59
|
+
* orphaned-process leak (personal/projects/hq-secrets-process-leak) was caused
|
|
60
|
+
* by relying on `process.stdout.write(payload, () => process.exit(0))`: when the
|
|
61
|
+
* parent shell reading `source <(hq secrets env …)` is killed on timeout, the
|
|
62
|
+
* stdout pipe loses its reader, the write callback never fires, `process.exit(0)`
|
|
63
|
+
* is never reached, and — because `env` forces no other exit — the process parks
|
|
64
|
+
* in `epoll_pwait(-1)` forever, reparented to PID 1. Hundreds accumulated this
|
|
65
|
+
* way, exhausting swap and driving load into triple digits.
|
|
66
|
+
*
|
|
67
|
+
* Writing synchronously to fd 1 and then exiting unconditionally removes every
|
|
68
|
+
* dependency on the event loop draining and on the reader still being attached:
|
|
69
|
+
* - a live reader receives the export lines (synchronous write reaches the
|
|
70
|
+
* kernel pipe buffer before we exit, so nothing is truncated);
|
|
71
|
+
* - a dead reader surfaces as EPIPE, which we swallow — the secret is already
|
|
72
|
+
* resolved and there is nothing left to deliver, so a clean exit is correct;
|
|
73
|
+
* - EAGAIN (stdout in non-blocking mode with a momentarily full buffer) is
|
|
74
|
+
* retried a bounded number of times. `env` payloads are a few short export
|
|
75
|
+
* lines, far under the 64 KiB pipe buffer, so this effectively never spins.
|
|
76
|
+
*
|
|
77
|
+
* `exit` and `write` are injectable for tests; production uses the real ones.
|
|
78
|
+
*/
|
|
79
|
+
export declare function emitEnvExportsAndExit(payload: string, exit?: (code: number) => never, write?: typeof writeSync): never;
|
|
54
80
|
export declare function registerSecretsCommand(program: Command): void;
|
|
55
81
|
//# sourceMappingURL=secrets.d.ts.map
|
package/dist/commands/secrets.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import * as readline from "node:readline";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
+
import { writeSync } from "node:fs";
|
|
4
5
|
import * as nodePath from "node:path";
|
|
5
6
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
6
7
|
import { DEFAULT_SECRETS_CACHE_TTL_MS, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
|
|
@@ -666,6 +667,50 @@ export async function loadRevealedSecrets(token, companyUid, keys, usage) {
|
|
|
666
667
|
throw err;
|
|
667
668
|
}
|
|
668
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* Emit `hq secrets env` output and terminate deterministically.
|
|
672
|
+
*
|
|
673
|
+
* The process MUST exit even when the consuming pipe has gone away. The
|
|
674
|
+
* orphaned-process leak (personal/projects/hq-secrets-process-leak) was caused
|
|
675
|
+
* by relying on `process.stdout.write(payload, () => process.exit(0))`: when the
|
|
676
|
+
* parent shell reading `source <(hq secrets env …)` is killed on timeout, the
|
|
677
|
+
* stdout pipe loses its reader, the write callback never fires, `process.exit(0)`
|
|
678
|
+
* is never reached, and — because `env` forces no other exit — the process parks
|
|
679
|
+
* in `epoll_pwait(-1)` forever, reparented to PID 1. Hundreds accumulated this
|
|
680
|
+
* way, exhausting swap and driving load into triple digits.
|
|
681
|
+
*
|
|
682
|
+
* Writing synchronously to fd 1 and then exiting unconditionally removes every
|
|
683
|
+
* dependency on the event loop draining and on the reader still being attached:
|
|
684
|
+
* - a live reader receives the export lines (synchronous write reaches the
|
|
685
|
+
* kernel pipe buffer before we exit, so nothing is truncated);
|
|
686
|
+
* - a dead reader surfaces as EPIPE, which we swallow — the secret is already
|
|
687
|
+
* resolved and there is nothing left to deliver, so a clean exit is correct;
|
|
688
|
+
* - EAGAIN (stdout in non-blocking mode with a momentarily full buffer) is
|
|
689
|
+
* retried a bounded number of times. `env` payloads are a few short export
|
|
690
|
+
* lines, far under the 64 KiB pipe buffer, so this effectively never spins.
|
|
691
|
+
*
|
|
692
|
+
* `exit` and `write` are injectable for tests; production uses the real ones.
|
|
693
|
+
*/
|
|
694
|
+
export function emitEnvExportsAndExit(payload, exit = process.exit, write = writeSync) {
|
|
695
|
+
const buf = Buffer.from(payload, "utf8");
|
|
696
|
+
const MAX_EAGAIN_RETRIES = 10_000;
|
|
697
|
+
let offset = 0;
|
|
698
|
+
let eagainRetries = 0;
|
|
699
|
+
while (offset < buf.length) {
|
|
700
|
+
try {
|
|
701
|
+
offset += write(1, buf, offset, buf.length - offset);
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
const code = err.code;
|
|
705
|
+
if (code === "EAGAIN" && eagainRetries++ < MAX_EAGAIN_RETRIES)
|
|
706
|
+
continue;
|
|
707
|
+
// EPIPE (reader gone) or retries exhausted: the output cannot be
|
|
708
|
+
// delivered, but the process must never hang. Stop and exit cleanly.
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return exit(0);
|
|
713
|
+
}
|
|
669
714
|
export function registerSecretsCommand(program) {
|
|
670
715
|
const secrets = program
|
|
671
716
|
.command("secrets")
|
|
@@ -1408,16 +1453,10 @@ export function registerSecretsCommand(program) {
|
|
|
1408
1453
|
const out = redact ? "[REDACTED]" : value;
|
|
1409
1454
|
payload += `export ${key}=${shellSingleQuote(out)}\n`;
|
|
1410
1455
|
}
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1413
|
-
//
|
|
1414
|
-
|
|
1415
|
-
// process alive forever, the mechanism behind the orphaned-process leak.
|
|
1416
|
-
// Exit inside the write callback, which fires only after the bytes reach
|
|
1417
|
-
// the pipe/file, so a `source <(hq secrets env …)` consumer never loses
|
|
1418
|
-
// the export lines — a bare process.exit() would truncate buffered
|
|
1419
|
-
// stdout on a pipe.
|
|
1420
|
-
process.stdout.write(payload, () => process.exit(0));
|
|
1456
|
+
// Write synchronously and exit unconditionally — never depend on the
|
|
1457
|
+
// event loop draining or the stdout write callback firing. See
|
|
1458
|
+
// emitEnvExportsAndExit for the full rationale (orphaned-process leak).
|
|
1459
|
+
emitEnvExportsAndExit(payload);
|
|
1421
1460
|
}
|
|
1422
1461
|
catch (err) {
|
|
1423
1462
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|