feat: release 2.7.0-rc2
This commit is contained in:
+73
-118
@@ -1,5 +1,4 @@
|
||||
import { KUBER_VERSION } from "../shared/version";
|
||||
import { readlinkSync, statSync } from "node:fs";
|
||||
|
||||
type SemVer = {
|
||||
major: bigint;
|
||||
@@ -15,66 +14,6 @@ const INSTALL_TIMEOUT_SECONDS = 30;
|
||||
const gray = (line: string) => `\x1b[90m${line}\x1b[0m\n`;
|
||||
const reportToStderr = (line: string) => process.stderr.write(line);
|
||||
|
||||
// This process owns the install result when the invoking CLI has already exited.
|
||||
// It opens only the original terminal (never the parent's pipe or stdout), and
|
||||
// checks its identity before writing in case the pty path has been recycled.
|
||||
const TTY_HELPER = `
|
||||
const { openSync, closeSync, fstatSync, writeSync, constants } = require('node:fs');
|
||||
const [version, seconds, path, dev, ino, rdev, uid] = process.argv.slice(1);
|
||||
let success = false;
|
||||
try {
|
||||
const child = Bun.spawn(['timeout', '--signal=TERM', '--kill-after=2s', seconds + 's',
|
||||
'bun', 'i', '-g', '--no-cache', '@dmgnr/kuber@' + version],
|
||||
{ stdin: 'ignore', stdout: 'ignore', stderr: 'ignore' });
|
||||
success = (await child.exited) === 0;
|
||||
} catch {}
|
||||
try {
|
||||
const fd = openSync(path, constants.O_WRONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
||||
try {
|
||||
const stat = fstatSync(fd);
|
||||
if (stat.isCharacterDevice() && String(stat.dev) === dev &&
|
||||
String(stat.ino) === ino && String(stat.rdev) === rdev && String(stat.uid) === uid) {
|
||||
writeSync(fd, '\\x1b[90m+ ' + (success ? 'Updated to ' : 'New version available: ') +
|
||||
version + '\\x1b[0m\\n');
|
||||
}
|
||||
} finally { closeSync(fd); }
|
||||
} catch {}
|
||||
`;
|
||||
|
||||
function originalTerminal(): string[] | undefined {
|
||||
if (!process.stderr.isTTY) return;
|
||||
try {
|
||||
const path = readlinkSync("/proc/self/fd/2");
|
||||
if (!/^\/dev\/pts\/[0-9]+$/.test(path)) return;
|
||||
const stat = statSync(path);
|
||||
if (!stat.isCharacterDevice()) return;
|
||||
return [
|
||||
path,
|
||||
String(stat.dev),
|
||||
String(stat.ino),
|
||||
String(stat.rdev),
|
||||
String(stat.uid),
|
||||
];
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function installWithTerminalReport(version: string, tty: string[]): void {
|
||||
const child = Bun.spawn(
|
||||
[
|
||||
process.execPath,
|
||||
"-e",
|
||||
TTY_HELPER,
|
||||
version,
|
||||
String(INSTALL_TIMEOUT_SECONDS),
|
||||
...tty,
|
||||
],
|
||||
{ stdin: "ignore", stdout: "ignore", stderr: "ignore", detached: true },
|
||||
);
|
||||
child.unref();
|
||||
}
|
||||
|
||||
function parseVersion(value: string | null): SemVer | undefined {
|
||||
if (!value || value.length > 128) return;
|
||||
const match = SEMVER.exec(value);
|
||||
@@ -124,7 +63,7 @@ export type VersionInstallRunner = (version: string) => Promise<boolean>;
|
||||
|
||||
type InstallProcess = {
|
||||
exited: Promise<number>;
|
||||
unref?(): void;
|
||||
kill(signal?: NodeJS.Signals): void;
|
||||
};
|
||||
type InstallSpawn = (
|
||||
argv: string[],
|
||||
@@ -132,7 +71,7 @@ type InstallSpawn = (
|
||||
stdin: "ignore";
|
||||
stdout: "ignore";
|
||||
stderr: "ignore";
|
||||
detached: true;
|
||||
env: NodeJS.ProcessEnv;
|
||||
},
|
||||
) => InstallProcess;
|
||||
|
||||
@@ -149,24 +88,51 @@ export async function installVersion(
|
||||
timeoutSeconds > 300
|
||||
)
|
||||
return false;
|
||||
const child = spawn(
|
||||
[
|
||||
"timeout",
|
||||
"--signal=TERM",
|
||||
"--kill-after=2s",
|
||||
`${timeoutSeconds}s`,
|
||||
"bun",
|
||||
"i",
|
||||
"-g",
|
||||
"--no-cache",
|
||||
`@dmgnr/kuber@${version}`,
|
||||
],
|
||||
{ stdin: "ignore", stdout: "ignore", stderr: "ignore", detached: true },
|
||||
);
|
||||
// The detached timeout owns its bounded lifetime; it must not keep a short
|
||||
// CLI invocation alive. Its result can still be observed while the parent lives.
|
||||
child.unref?.();
|
||||
return child.exited.then((code) => code === 0);
|
||||
try {
|
||||
const child = spawn(
|
||||
[process.execPath, "i", "-g", "--no-cache", `@dmgnr/kuber@${version}`],
|
||||
{ stdin: "ignore", stdout: "ignore", stderr: "ignore", env: process.env },
|
||||
);
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
// Allow a short grace period to reap a child that ignores termination.
|
||||
let timedOut = false;
|
||||
const deadline = setTimeout(
|
||||
() => {
|
||||
timedOut = true;
|
||||
try {
|
||||
child.kill();
|
||||
} catch {}
|
||||
const force = setTimeout(() => {
|
||||
try {
|
||||
child.kill("SIGKILL");
|
||||
} catch {}
|
||||
resolve(false);
|
||||
}, 1_000);
|
||||
void child.exited.finally(() => clearTimeout(force)).catch(() => {});
|
||||
},
|
||||
timeoutSeconds * 1_000 - 1_000,
|
||||
);
|
||||
void child.exited.then(
|
||||
(code) => {
|
||||
clearTimeout(deadline);
|
||||
resolve(!timedOut && code === 0);
|
||||
},
|
||||
() => {
|
||||
clearTimeout(deadline);
|
||||
resolve(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const pendingUpdates = new Set<Promise<void>>();
|
||||
|
||||
/** Wait for an update observed during this CLI invocation, including its notice. */
|
||||
export async function waitForVersionUpdate(): Promise<void> {
|
||||
await Promise.all(pendingUpdates);
|
||||
}
|
||||
|
||||
export function createVersionObserver({
|
||||
@@ -184,45 +150,34 @@ export function createVersionObserver({
|
||||
return;
|
||||
attempted = true;
|
||||
// Defer install work beyond the response headers; never block body consumption.
|
||||
setTimeout(() => {
|
||||
// The global observer must not install packages in tests or source-tree
|
||||
// development commands. Explicitly injected runners remain testable.
|
||||
if (
|
||||
runner === installVersion &&
|
||||
(process.env.NODE_ENV === "test" ||
|
||||
process.env.NODE_ENV === "development" ||
|
||||
process.argv[1]?.endsWith(".ts"))
|
||||
)
|
||||
return;
|
||||
if (runner === installVersion && report === reportToStderr) {
|
||||
const tty = originalTerminal();
|
||||
if (tty) {
|
||||
try {
|
||||
installWithTerminalReport(version, tty);
|
||||
} catch {
|
||||
try {
|
||||
report(gray(`+ New version available: ${version}`));
|
||||
} catch {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
void Promise.resolve()
|
||||
.then(() => runner(version))
|
||||
.then(
|
||||
(success) => {
|
||||
report(
|
||||
gray(
|
||||
`+ ${success ? "Updated to " : "New version available: "}${version}`,
|
||||
),
|
||||
);
|
||||
},
|
||||
() => report(gray(`+ New version available: ${version}`)),
|
||||
const pending = new Promise<void>((resolve) => setTimeout(resolve, 0)).then(
|
||||
async () => {
|
||||
// The global observer must not install packages in tests or source-tree
|
||||
// development commands. Explicitly injected runners remain testable.
|
||||
if (
|
||||
runner === installVersion &&
|
||||
(process.env.NODE_ENV === "test" ||
|
||||
process.env.NODE_ENV === "development" ||
|
||||
process.argv[1]?.endsWith(".ts"))
|
||||
)
|
||||
.catch(() => {
|
||||
return;
|
||||
let success = false;
|
||||
try {
|
||||
success = await runner(version);
|
||||
} catch {}
|
||||
try {
|
||||
report(
|
||||
gray(
|
||||
`+ ${success ? "Updated to " : "New version available: "}${version}`,
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
// A broken stderr must never affect an API request or command exit status.
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
);
|
||||
pendingUpdates.add(pending);
|
||||
void pending.finally(() => pendingUpdates.delete(pending));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user