impel-cli 0.20.63-canary.7 → 0.20.64

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/src/updates.js CHANGED
@@ -36,22 +36,13 @@ export function updateRegistry() {
36
36
  ).replace(/\/+$/u, "");
37
37
  }
38
38
 
39
- /** The closed set of npm dist-tags the CLI can follow. */
40
- export function normalizeUpdateTag(tag) {
41
- if (tag === "latest" || tag === "next" || tag === "canary") return tag;
39
+ function normalizeUpdateTag(tag) {
40
+ if (tag === "latest" || tag === "next") return tag;
42
41
  throw new Error(`unsupported npm update tag "${tag}"`);
43
42
  }
44
43
 
45
- /**
46
- * Keep prerelease installs on npm's prerelease channel.
47
- *
48
- * A canary build is keyed on the brand channel rather than its version
49
- * string: `<patch+1>-canary.<N>` is a prerelease, but it must follow the
50
- * `canary` dist-tag, not `next` — and under the stable brand that same
51
- * version is only ever a prerelease that follows `next`.
52
- */
44
+ /** Keep prerelease installs on npm's prerelease channel. */
53
45
  export function updateTagForVersion(version) {
54
- if (RUNTIME_BRAND.channel === "canary") return "canary";
55
46
  const parsed = validVersion(version) ? version.match(VERSION_RE) : null;
56
47
  return parsed?.[4] ? "next" : "latest";
57
48
  }
package/src/windowsGit.js CHANGED
@@ -15,7 +15,6 @@ import path from "node:path";
15
15
 
16
16
  import { nativeCommandInvocation, nativeSpawnInvocation } from "./nativeProcess.js";
17
17
  import { renameWithWindowsRetry } from "./windowsFs.js";
18
- import { RUNTIME_BRAND } from "./runtimeBrand.js";
19
18
 
20
19
  /**
21
20
  * The exact MinGit release the CLI may install. Never use `latest` or an
@@ -53,7 +52,7 @@ const POWERSHELL_ARGS = [
53
52
 
54
53
  /** Root of the Impel-managed MinGit install. */
55
54
  export function managedGitRoot(homeDir = os.homedir()) {
56
- return path.join(homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "tools", "git");
55
+ return path.join(homeDir, ".config", "impel", "tools", "git");
57
56
  }
58
57
 
59
58
  /** The PATH-safe git.exe inside a MinGit root (MinGit unpacks `cmd/` at top level). */
@@ -1,92 +0,0 @@
1
- #!/usr/bin/env node
2
- // Bake the canary channel into the runtime brand (canary release cycle, KTD7).
3
- //
4
- // `publish-canary.yml` runs this in the working tree immediately before
5
- // `pnpm pack`, after the test suite has run on the unmodified tree. It
6
- // rewrites the brand DEFAULT in `src/runtimeBrand.js` in place: the channel
7
- // literal becomes `canary`, the three production `defaultOrigin` literals
8
- // become the canary row of CHANNEL_ORIGINS (read from the very module being
9
- // baked so the row has one source of truth), and the CLI config namespace
10
- // becomes `impel-canary` so stable and canary state never share files.
11
- //
12
- // It is a one-shot publish step, not a formatter: every literal it replaces
13
- // must occur exactly once in its shipped form, a file that is already baked
14
- // is refused, and the result is re-imported and validated before the script
15
- // exits 0 — a half-baked or doubly-baked tree can never be packed.
16
- //
17
- // Usage: node scripts/bake-canary-brand.mjs [path/to/runtimeBrand.js]
18
-
19
- import fs from "node:fs";
20
- import path from "node:path";
21
- import { fileURLToPath, pathToFileURL } from "node:url";
22
-
23
- const DEFAULT_TARGET = fileURLToPath(new URL("../src/runtimeBrand.js", import.meta.url));
24
- const CANARY_MARKER = 'channel: "canary",';
25
-
26
- function fail(message) {
27
- process.stderr.write(`bake-canary-brand: ${message}\n`);
28
- process.exit(1);
29
- }
30
-
31
- /** Import `target` fresh (never from this process's module cache) with no brand in the environment. */
32
- async function importBrandModule(target, cacheBuster) {
33
- // The module reads IMPEL_CLI_RUNTIME_BRAND at import time; the bake is about
34
- // the DEFAULT, so an inherited brand must not stand in for it.
35
- delete process.env.IMPEL_CLI_RUNTIME_BRAND;
36
- return import(`${pathToFileURL(target).href}?bake=${cacheBuster}`);
37
- }
38
-
39
- function replaceExactlyOnce(source, from, to) {
40
- const occurrences = source.split(from).length - 1;
41
- if (occurrences !== 1) {
42
- fail(`expected exactly one occurrence of ${JSON.stringify(from)} in the brand source, found ${occurrences}`);
43
- }
44
- return source.replace(from, to);
45
- }
46
-
47
- async function main() {
48
- const target = path.resolve(process.argv[2] || DEFAULT_TARGET);
49
- const original = fs.readFileSync(target, "utf8");
50
- if (original.includes(CANARY_MARKER)) fail(`${target} is already baked for the canary channel`);
51
-
52
- const { CHANNEL_ORIGINS } = await importBrandModule(target, "before");
53
- const rows = [
54
- ['channel: "stable",', CANARY_MARKER],
55
- ['configNamespace: "impel",', 'configNamespace: "impel-canary",'],
56
- ...["gateway", "sessions", "controlPlane"].map((column) => [
57
- `defaultOrigin: ${JSON.stringify(CHANNEL_ORIGINS.stable[column])}`,
58
- `defaultOrigin: ${JSON.stringify(CHANNEL_ORIGINS.canary[column])}`,
59
- ]),
60
- ];
61
- let baked = original;
62
- for (const [from, to] of rows) baked = replaceExactlyOnce(baked, from, to);
63
-
64
- // Atomic replace, then prove the result by importing it: the DEFAULT must
65
- // now be the canary row and must pass the validator's own channel check.
66
- const temporary = `${target}.bake-${process.pid}`;
67
- fs.writeFileSync(temporary, baked, { mode: fs.statSync(target).mode & 0o777 });
68
- fs.renameSync(temporary, target);
69
- try {
70
- const { RUNTIME_BRAND, validateRuntimeBrand } = await importBrandModule(target, "after");
71
- validateRuntimeBrand(RUNTIME_BRAND);
72
- const mismatch = ["gateway", "sessions", "controlPlane"].find((column) => (
73
- RUNTIME_BRAND[column].defaultOrigin !== CHANNEL_ORIGINS.canary[column]
74
- ));
75
- if (RUNTIME_BRAND.channel !== "canary" || mismatch) {
76
- throw new Error(`baked DEFAULT is not the canary row (${mismatch || "channel"})`);
77
- }
78
- if (RUNTIME_BRAND.cli.configNamespace !== "impel-canary") {
79
- throw new Error(`baked CLI config namespace is not isolated (${RUNTIME_BRAND.cli.configNamespace})`);
80
- }
81
- process.stdout.write(
82
- `bake-canary-brand: ${target} now carries channel canary `
83
- + `(gateway ${RUNTIME_BRAND.gateway.defaultOrigin}, sessions ${RUNTIME_BRAND.sessions.defaultOrigin}, `
84
- + `controlPlane ${RUNTIME_BRAND.controlPlane.defaultOrigin}, config ${RUNTIME_BRAND.cli.configNamespace})\n`,
85
- );
86
- } catch (error) {
87
- fs.writeFileSync(target, original);
88
- fail(`baked brand failed verification and was restored: ${error?.message || error}`);
89
- }
90
- }
91
-
92
- await main();
@@ -1,173 +0,0 @@
1
- #!/usr/bin/env node
2
- // Canary version computation and successor gate (canary release cycle, U4).
3
- //
4
- // `publish-canary.yml` publishes `<major>.<minor>.<patch + 1>-canary.<N>` on
5
- // every push to `dev` (R15), where `N` is `GITHUB_RUN_NUMBER` (KTD6): it is
6
- // monotonic across publishes and unique on a retry without a registry round
7
- // trip, and a GitHub re-run keeps its number, so the same version comes back
8
- // and the successor gate below refuses it. The script only reads
9
- // `package.json`; the publish job applies the version to its working tree
10
- // with `npm version --no-git-tag-version`, and nothing on `dev` is edited.
11
- //
12
- // Usage: node scripts/canary-version.mjs [package.json] [--dist-tags <file>]
13
- // Prints the computed version on stdout. With `--dist-tags`, the file is
14
- // the registry's dist-tags document and the version must strictly succeed
15
- // its `canary` entry; an absent `canary` tag is the bootstrap case. Every
16
- // refusal prints nothing on stdout and exits 1, so a caller that captures
17
- // stdout can never pick up a half-computed version.
18
-
19
- import fs from "node:fs";
20
- import path from "node:path";
21
- import { fileURLToPath } from "node:url";
22
-
23
- const RELEASE_VERSION = /^(\d+)\.(\d+)\.(\d+)$/u;
24
- const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u;
25
-
26
- /** `<major>.<minor>.<patch + 1>-canary.<runNumber>` from a bare release version. */
27
- export function canaryVersion(packageVersion, runNumber) {
28
- if (typeof packageVersion !== "string" || packageVersion === "") {
29
- throw new Error(`package.json version must be a non-empty string, got ${JSON.stringify(packageVersion)}`);
30
- }
31
- if (/-/u.test(packageVersion) && SEMVER.test(packageVersion)) {
32
- throw new Error(
33
- `package.json version ${packageVersion} already carries a prerelease suffix; `
34
- + "the canary base must be the bare release version committed on dev",
35
- );
36
- }
37
- const match = RELEASE_VERSION.exec(packageVersion);
38
- if (!match) {
39
- throw new Error(`package.json version ${JSON.stringify(packageVersion)} is not a bare <major>.<minor>.<patch> release version`);
40
- }
41
- const text = typeof runNumber === "number" ? String(runNumber) : runNumber;
42
- if (typeof text !== "string" || !/^\d+$/u.test(text) || Number(text) < 1 || !Number.isSafeInteger(Number(text))) {
43
- throw new Error(`run number must be a positive integer, got ${JSON.stringify(runNumber)}`);
44
- }
45
- const [major, minor, patch] = match.slice(1).map(Number);
46
- return `${major}.${minor}.${patch + 1}-canary.${Number(text)}`;
47
- }
48
-
49
- function parseSemver(text) {
50
- const match = SEMVER.exec(text);
51
- if (!match) throw new Error(`unparsable semver: ${text}`);
52
- return {
53
- release: [Number(match[1]), Number(match[2]), Number(match[3])],
54
- prerelease: match[4] ? match[4].split(".") : [],
55
- };
56
- }
57
-
58
- function compareIdentifiers(a, b) {
59
- const aNumeric = /^\d+$/u.test(a);
60
- const bNumeric = /^\d+$/u.test(b);
61
- if (aNumeric && bNumeric) return Math.sign(Number(a) - Number(b));
62
- if (aNumeric) return -1; // numeric identifiers sort before alphanumeric
63
- if (bNumeric) return 1;
64
- return a < b ? -1 : a > b ? 1 : 0;
65
- }
66
-
67
- /** SemVer 2.0.0 precedence, the same ordering `publish-npm.yml`'s successor gate uses. */
68
- export function compareSemver(leftText, rightText) {
69
- const a = parseSemver(leftText);
70
- const b = parseSemver(rightText);
71
- for (let index = 0; index < 3; index += 1) {
72
- if (a.release[index] !== b.release[index]) return Math.sign(a.release[index] - b.release[index]);
73
- }
74
- if (a.prerelease.length === 0 && b.prerelease.length === 0) return 0;
75
- if (a.prerelease.length === 0) return 1; // a release outranks its prereleases
76
- if (b.prerelease.length === 0) return -1;
77
- const length = Math.max(a.prerelease.length, b.prerelease.length);
78
- for (let index = 0; index < length; index += 1) {
79
- if (a.prerelease[index] === undefined) return -1; // shorter prerelease sorts first
80
- if (b.prerelease[index] === undefined) return 1;
81
- const result = compareIdentifiers(a.prerelease[index], b.prerelease[index]);
82
- if (result !== 0) return result;
83
- }
84
- return 0;
85
- }
86
-
87
- /**
88
- * Require `version` to strictly succeed `distTags.canary`. Only the `canary`
89
- * tag is the predecessor: `latest` and `next` belong to the stable lane and
90
- * never stand in for it. Returns a description for the step summary.
91
- */
92
- export function assertCanarySuccessor(version, distTags) {
93
- const current = distTags?.canary;
94
- if (current === undefined) {
95
- return {
96
- bootstrap: true,
97
- current,
98
- message: `No existing \`canary\` dist-tag; ${version} is the canary channel bootstrap.`,
99
- };
100
- }
101
- if (compareSemver(version, current) <= 0) {
102
- throw new Error(
103
- `version ${version} is not a strict semver successor of canary=${current}. `
104
- + "A GitHub re-run keeps its run number and is refused by design; push a new commit "
105
- + "to dev instead, or merge the released version back into dev if the base has moved.",
106
- );
107
- }
108
- return {
109
- bootstrap: false,
110
- current,
111
- message: `${version} strictly succeeds \`canary\`=${current}. Current tags: ${JSON.stringify(distTags)}`,
112
- };
113
- }
114
-
115
- function fail(message) {
116
- process.stderr.write(`canary-version: ${message}\n`);
117
- process.exit(1);
118
- }
119
-
120
- function main(argv) {
121
- let packageJson = "package.json";
122
- let distTagsFile = null;
123
- for (let index = 0; index < argv.length; index += 1) {
124
- const argument = argv[index];
125
- if (argument === "--dist-tags") {
126
- distTagsFile = argv[index + 1];
127
- index += 1;
128
- if (!distTagsFile) fail("--dist-tags requires a file path");
129
- } else if (argument.startsWith("--")) {
130
- fail(`unknown option ${argument}`);
131
- } else {
132
- packageJson = argument;
133
- }
134
- }
135
-
136
- let version;
137
- try {
138
- const manifest = JSON.parse(fs.readFileSync(path.resolve(packageJson), "utf8"));
139
- const runNumber = process.env.GITHUB_RUN_NUMBER;
140
- if (runNumber === undefined || runNumber === "") fail("GITHUB_RUN_NUMBER is not set; the canary counter is the workflow run number");
141
- version = canaryVersion(manifest.version, runNumber);
142
- } catch (error) {
143
- fail(error?.message || String(error));
144
- }
145
-
146
- if (distTagsFile) {
147
- let distTags;
148
- try {
149
- distTags = JSON.parse(fs.readFileSync(path.resolve(distTagsFile), "utf8"));
150
- } catch (error) {
151
- fail(`cannot read dist-tags from ${distTagsFile}: ${error?.message || error}`);
152
- }
153
- if (!distTags || typeof distTags !== "object" || Array.isArray(distTags)) {
154
- fail(`dist-tags document ${distTagsFile} is not a JSON object`);
155
- }
156
- let result;
157
- try {
158
- result = assertCanarySuccessor(version, distTags);
159
- } catch (error) {
160
- fail(error?.message || String(error));
161
- }
162
- process.stderr.write(`canary-version: ${result.message}\n`);
163
- if (process.env.GITHUB_STEP_SUMMARY) {
164
- fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `## Canary gate: dist-tag successor\n\n${result.message}\n`);
165
- }
166
- }
167
-
168
- process.stdout.write(`${version}\n`);
169
- }
170
-
171
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
172
- main(process.argv.slice(2));
173
- }
@@ -1,134 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const DIST_TAGS_URL = "https://registry.npmjs.org/-/package/impel-cli/dist-tags";
7
-
8
- // npm accepts a version and returns success before every registry edge exposes
9
- // its new dist-tag. The post-publish verifier therefore treats a stale canary
10
- // tag as propagation, not proof that npm publish failed. Five minutes stays
11
- // comfortably inside the publish job's 15-minute timeout.
12
- const CANARY_TAG_VERIFY_ATTEMPTS = 20;
13
- const CANARY_TAG_VERIFY_DELAY_MS = 15_000;
14
-
15
- class CanaryDistTagMismatchError extends Error {}
16
-
17
- export function assertCanaryDistTags({ beforeLatest, expectedCanary, tags }) {
18
- const latest = tags.latest ?? "";
19
- const canary = tags.canary ?? "";
20
- if (latest !== beforeLatest) {
21
- throw new Error(
22
- `dist-tags.latest moved during the canary publish: before=${beforeLatest || "<none>"} `
23
- + `after=${latest || "<none>"}. The canary lane must never move latest; investigate before the next publish.`,
24
- );
25
- }
26
- if (canary !== expectedCanary) {
27
- throw new CanaryDistTagMismatchError(
28
- `dist-tags.canary is ${canary || "<none>"}, expected ${expectedCanary}.`,
29
- );
30
- }
31
- return { latest, canary };
32
- }
33
-
34
- /**
35
- * Verify npm's public dist-tags after publish while allowing the canary tag to
36
- * propagate. A moved `latest` remains an immediate failure: that is a release
37
- * invariant violation, not ordinary registry propagation.
38
- */
39
- export async function verifyCanaryDistTags({
40
- beforeLatest,
41
- expectedCanary,
42
- readTags = readDistTags,
43
- attempts = CANARY_TAG_VERIFY_ATTEMPTS,
44
- delayMs = CANARY_TAG_VERIFY_DELAY_MS,
45
- sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
46
- }) {
47
- let lastObservedCanary = "";
48
- let lastError;
49
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
50
- const tags = await readTags({ bustCache: true });
51
- try {
52
- return assertCanaryDistTags({ beforeLatest, expectedCanary, tags });
53
- } catch (error) {
54
- if (!(error instanceof CanaryDistTagMismatchError)) throw error;
55
- lastObservedCanary = tags.canary ?? "";
56
- lastError = error;
57
- if (attempt < attempts) await sleep(delayMs);
58
- }
59
- }
60
- throw new Error(
61
- `npm accepted the canary publish but did not expose it after ${attempts} dist-tag checks: `
62
- + `${lastError?.message || `last dist-tags.canary was ${lastObservedCanary || "<none>"}.`} `
63
- + "This is registry propagation lag, not an unpublished tarball.",
64
- );
65
- }
66
-
67
- async function readDistTags({ allowMissing, bustCache = false } = {}) {
68
- const url = bustCache ? `${DIST_TAGS_URL}?t=${Date.now()}` : DIST_TAGS_URL;
69
- let lastError;
70
- for (let attempt = 1; attempt <= 3; attempt += 1) {
71
- try {
72
- const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
73
- if (response.status === 404 && allowMissing) return {};
74
- if (!response.ok) throw new Error(`npm registry dist-tags lookup failed: HTTP ${response.status}`);
75
- const tags = await response.json();
76
- if (!tags || typeof tags !== "object" || Array.isArray(tags)) {
77
- throw new Error("npm registry returned an invalid dist-tags document");
78
- }
79
- return tags;
80
- } catch (error) {
81
- lastError = error;
82
- if (attempt < 3) await new Promise((resolve) => setTimeout(resolve, 1_000));
83
- }
84
- }
85
- throw lastError;
86
- }
87
-
88
- function appendEnvironment(name, value) {
89
- if (!process.env.GITHUB_ENV) throw new Error("GITHUB_ENV is not set");
90
- fs.appendFileSync(process.env.GITHUB_ENV, `${name}=${value}\n`);
91
- }
92
-
93
- function appendSummary(lines) {
94
- if (!process.env.GITHUB_STEP_SUMMARY) throw new Error("GITHUB_STEP_SUMMARY is not set");
95
- fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${lines.join("\n")}\n`);
96
- }
97
-
98
- async function main([command, ...args]) {
99
- if (command === "fetch" && args.length === 1) {
100
- const tags = await readDistTags({ allowMissing: true });
101
- fs.writeFileSync(path.resolve(args[0]), `${JSON.stringify(tags)}\n`);
102
- console.log(Object.keys(tags).length === 0 ? "No npm dist-tags found; using channel bootstrap." : "Fetched npm dist-tags.");
103
- return;
104
- }
105
- if (command === "snapshot-latest" && args.length === 0) {
106
- const tags = await readDistTags({ allowMissing: true });
107
- const latest = tags.latest ?? "";
108
- appendEnvironment("LATEST_BEFORE", latest);
109
- console.log(`dist-tags.latest before publish: ${latest || "<none>"}`);
110
- return;
111
- }
112
- if (command === "assert-canary" && args.length === 2) {
113
- const [expectedCanary, beforeLatest] = args;
114
- const { latest } = await verifyCanaryDistTags({ expectedCanary, beforeLatest });
115
- appendSummary([
116
- "## Canary published",
117
- "",
118
- `- \`impel-cli@${expectedCanary}\` is \`canary\``,
119
- `- \`latest\` is unchanged at \`${latest || "<none>"}\``,
120
- ]);
121
- console.log(`Published impel-cli@${expectedCanary} under canary; latest is still ${latest || "<none>"}.`);
122
- return;
123
- }
124
- throw new Error(
125
- "usage: node scripts/npm-dist-tags.mjs fetch <file> | snapshot-latest | assert-canary <version> <latest-before>",
126
- );
127
- }
128
-
129
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
130
- main(process.argv.slice(2)).catch((error) => {
131
- console.error(`npm-dist-tags: ${error?.message || error}`);
132
- process.exit(1);
133
- });
134
- }
@@ -1,32 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "node:fs";
4
- import { execFileSync } from "node:child_process";
5
- import path from "node:path";
6
- import { fileURLToPath } from "node:url";
7
-
8
- function appendEnvironment(name, value) {
9
- if (!process.env.GITHUB_ENV) throw new Error("GITHUB_ENV is not set");
10
- fs.appendFileSync(process.env.GITHUB_ENV, `${name}=${value}\n`);
11
- }
12
-
13
- function main([version]) {
14
- const destination = process.env.RUNNER_TEMP;
15
- if (!destination || !version) throw new Error("RUNNER_TEMP and a canary version are required");
16
- execFileSync("pnpm", ["pack", "--pack-destination", destination], { stdio: "inherit" });
17
- const tarball = path.join(destination, `impel-cli-${version}.tgz`);
18
- if (!fs.existsSync(tarball)) throw new Error(`pnpm pack did not create ${tarball}`);
19
- const prefix = path.join(destination, "impel-prefix");
20
- execFileSync("npm", ["install", "--global", "--prefix", prefix, tarball], { stdio: "inherit" });
21
- execFileSync(process.execPath, ["scripts/verify-canary-install.mjs", prefix, version], { stdio: "inherit" });
22
- appendEnvironment("CANARY_TARBALL", tarball);
23
- }
24
-
25
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
26
- try {
27
- main(process.argv.slice(2));
28
- } catch (error) {
29
- console.error(`pack-canary: ${error?.message || error}`);
30
- process.exit(1);
31
- }
32
- }
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { execFileSync } from "node:child_process";
4
- import fs from "node:fs";
5
- import path from "node:path";
6
-
7
- function main() {
8
- const destination = process.env.RUNNER_TEMP;
9
- if (!destination) throw new Error("RUNNER_TEMP is required");
10
- const { version } = JSON.parse(fs.readFileSync("package.json", "utf8"));
11
- execFileSync("pnpm", ["pack", "--pack-destination", destination], { stdio: "inherit" });
12
- const tarball = path.join(destination, `impel-cli-${version}.tgz`);
13
- if (!fs.existsSync(tarball)) throw new Error(`pnpm pack did not create ${tarball}`);
14
- const prefix = path.join(destination, "impel-prefix");
15
- execFileSync("npm", ["install", "--global", "--prefix", prefix, tarball], { stdio: "inherit" });
16
- const executable = process.platform === "win32" ? path.join(prefix, "impel.cmd") : path.join(prefix, "bin", "impel");
17
- const actual = execFileSync(executable, ["--version"], { encoding: "utf8" }).trim();
18
- if (actual !== version) throw new Error(`packed CLI reports ${actual}, expected ${version}`);
19
- console.log(`Verified packed impel-cli ${version}.`);
20
- }
21
-
22
- try {
23
- main();
24
- } catch (error) {
25
- console.error(`pack-release: ${error?.message || error}`);
26
- process.exit(1);
27
- }
@@ -1,28 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { execFileSync } from "node:child_process";
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import { fileURLToPath } from "node:url";
7
-
8
- const CANARY_VERSION = /^\d+\.\d+\.\d+-canary\.\d+$/u;
9
-
10
- function main([version]) {
11
- if (!CANARY_VERSION.test(version || "")) {
12
- throw new Error(`the gate produced an invalid canary version: ${version || "<empty>"}`);
13
- }
14
- execFileSync("npm", ["version", version, "--no-git-tag-version"], { stdio: "inherit" });
15
- execFileSync(process.execPath, ["scripts/bake-canary-brand.mjs"], { stdio: "inherit" });
16
- const actual = JSON.parse(fs.readFileSync("package.json", "utf8")).version;
17
- if (actual !== version) throw new Error(`package.json version ${actual} does not match ${version}`);
18
- console.log(`Prepared ${version} canary package.`);
19
- }
20
-
21
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
22
- try {
23
- main(process.argv.slice(2));
24
- } catch (error) {
25
- console.error(`prepare-canary: ${error?.message || error}`);
26
- process.exit(1);
27
- }
28
- }
@@ -1,133 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "node:fs";
4
- import { execFileSync } from "node:child_process";
5
- import path from "node:path";
6
- import { fileURLToPath } from "node:url";
7
-
8
- import { compareSemver } from "./canary-version.mjs";
9
- import { evaluateCheckRuns } from "./required-checks.mjs";
10
-
11
- const DIST_TAGS_URL = "https://registry.npmjs.org/-/package/impel-cli/dist-tags";
12
-
13
- function appendSummary(text) {
14
- if (!process.env.GITHUB_STEP_SUMMARY) throw new Error("GITHUB_STEP_SUMMARY is not set");
15
- fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, text.endsWith("\n") ? text : `${text}\n`);
16
- }
17
-
18
- async function fetchJson(url, { token, allowMissing = false } = {}) {
19
- const response = await fetch(url, {
20
- headers: {
21
- Accept: "application/vnd.github+json",
22
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
23
- "User-Agent": "UseImpel/impel-cli release gate",
24
- "X-GitHub-Api-Version": "2022-11-28",
25
- },
26
- signal: AbortSignal.timeout(30_000),
27
- });
28
- if (response.status === 404 && allowMissing) return {};
29
- if (!response.ok) throw new Error(`request failed: HTTP ${response.status}`);
30
- return response.json();
31
- }
32
-
33
- async function checkRunsForCommit(repository, sha, token) {
34
- const runs = [];
35
- for (let page = 1; ; page += 1) {
36
- const body = await fetchJson(
37
- `https://api.github.com/repos/${repository}/commits/${sha}/check-runs?per_page=100&page=${page}`,
38
- { token },
39
- );
40
- runs.push(...body.check_runs.map(({ name, status, conclusion }) => ({ name, status, conclusion })));
41
- if (body.check_runs.length < 100) return runs;
42
- }
43
- }
44
-
45
- function manifest() {
46
- return JSON.parse(fs.readFileSync("package.json", "utf8"));
47
- }
48
-
49
- export function verifyTag({ tag, version }) {
50
- const expected = `v${version}`;
51
- if (tag !== expected) throw new Error(`release tag ${tag} does not match ${expected}`);
52
- return expected;
53
- }
54
-
55
- export function verifyMain({ sha, mainRef = "origin/main" }) {
56
- try {
57
- execFileSync("git", ["merge-base", "--is-ancestor", sha, mainRef], { stdio: "ignore" });
58
- } catch {
59
- throw new Error(`tagged commit ${sha} is not on ${mainRef}; merge to main before tagging`);
60
- }
61
- }
62
-
63
- export async function verifyChecks({ repository, sha, token }) {
64
- const result = evaluateCheckRuns(await checkRunsForCommit(repository, sha, token));
65
- const summary = result.summary;
66
- appendSummary(summary);
67
- if (result.state !== "success") {
68
- throw new Error(`required checks on ${sha} are not all green: ${result.failures.join("; ") || result.pending.join(", ")}`);
69
- }
70
- return result;
71
- }
72
-
73
- export async function verifySuccessor({ version, channel = version.includes("-") ? "next" : "latest" }) {
74
- const tags = await fetchJson(DIST_TAGS_URL, { allowMissing: true });
75
- const current = tags[channel] ?? (channel === "next" ? tags.latest : undefined);
76
- const message = current === undefined
77
- ? `No existing \`${channel}\` dist-tag; ${version} is the channel bootstrap.`
78
- : `${version} strictly succeeds \`${channel}\`=${current}. Current tags: ${JSON.stringify(tags)}`;
79
- if (current !== undefined && compareSemver(version, current) <= 0) {
80
- throw new Error(`version ${version} is not a strict semver successor of ${channel}=${current}`);
81
- }
82
- appendSummary(`## Release gate: dist-tag successor\n\n${message}\n`);
83
- console.log(message);
84
- return { channel, current, tags };
85
- }
86
-
87
- export function selectDistTag(version) {
88
- const tag = version.includes("-") ? "next" : "latest";
89
- if (!process.env.GITHUB_ENV) throw new Error("GITHUB_ENV is not set");
90
- fs.appendFileSync(process.env.GITHUB_ENV, `NPM_DIST_TAG=${tag}\n`);
91
- console.log(`Publishing ${version} under npm dist-tag ${tag}.`);
92
- return tag;
93
- }
94
-
95
- async function main([command]) {
96
- const pkg = manifest();
97
- if (command === "verify-tag") {
98
- verifyTag({ tag: process.env.RELEASE_TAG, version: pkg.version });
99
- return;
100
- }
101
- if (command === "verify-main") {
102
- const sha = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
103
- verifyMain({ sha });
104
- console.log(`Tagged commit ${sha} is on origin/main.`);
105
- return;
106
- }
107
- if (command === "verify-checks") {
108
- const sha = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
109
- await verifyChecks({
110
- repository: process.env.GITHUB_REPOSITORY,
111
- sha,
112
- token: process.env.GH_TOKEN,
113
- });
114
- console.log(`All required checks are green on ${sha}.`);
115
- return;
116
- }
117
- if (command === "verify-successor") {
118
- await verifySuccessor({ version: pkg.version });
119
- return;
120
- }
121
- if (command === "select-dist-tag") {
122
- selectDistTag(pkg.version);
123
- return;
124
- }
125
- throw new Error("usage: node scripts/release-gate.mjs verify-tag|verify-main|verify-checks|verify-successor|select-dist-tag");
126
- }
127
-
128
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
129
- main(process.argv.slice(2)).catch((error) => {
130
- console.error(`release-gate: ${error?.message || error}`);
131
- process.exit(1);
132
- });
133
- }