impel-cli 0.20.63-canary.6 → 0.20.63
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 +1 -1
- package/RELEASE_NOTES.md +14 -0
- package/package.json +1 -1
- package/src/cli.js +1 -2
- package/src/cliProfiles.js +9 -6
- package/src/codexSetup.js +14 -5
- package/src/commands/auth.js +6 -22
- package/src/commands/update.js +2 -4
- package/src/config.js +5 -48
- package/src/extension/index.js +2 -2
- package/src/gateway/index.js +1 -0
- package/src/managedProfileVersion.js +6 -1
- package/src/posthog.js +2 -2
- package/src/runtimeBrand.js +7 -66
- package/src/updates.js +3 -12
- package/scripts/bake-canary-brand.mjs +0 -92
- package/scripts/canary-version.mjs +0 -173
- package/scripts/npm-dist-tags.mjs +0 -134
- package/scripts/pack-canary.mjs +0 -32
- package/scripts/pack-release.mjs +0 -27
- package/scripts/prepare-canary.mjs +0 -28
- package/scripts/release-gate.mjs +0 -133
- package/scripts/release-metadata.mjs +0 -18
- package/scripts/required-checks.mjs +0 -126
- package/scripts/verify-canary-install.mjs +0 -62
|
@@ -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
|
-
}
|
package/scripts/pack-canary.mjs
DELETED
|
@@ -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
|
-
}
|
package/scripts/pack-release.mjs
DELETED
|
@@ -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
|
-
}
|
package/scripts/release-gate.mjs
DELETED
|
@@ -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
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
|
-
|
|
6
|
-
function main() {
|
|
7
|
-
if (!process.env.GITHUB_OUTPUT) throw new Error("GITHUB_OUTPUT is not set");
|
|
8
|
-
const { version } = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
|
9
|
-
const sourceCommit = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
|
10
|
-
fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\nsource_commit=${sourceCommit}\n`);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
main();
|
|
15
|
-
} catch (error) {
|
|
16
|
-
console.error(`release-metadata: ${error?.message || error}`);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// The CI check names a publishable commit must carry (canary release cycle, U4).
|
|
3
|
-
//
|
|
4
|
-
// Both release workflows consume this list through checked-in gate scripts.
|
|
5
|
-
// A renamed CI job that is not renamed here fails the suite, not a release.
|
|
6
|
-
// Every push to `dev` and `main` runs these via `ci.yml`, so they must exist on
|
|
7
|
-
// a publishable SHA; names must match the job names exactly.
|
|
8
|
-
//
|
|
9
|
-
// Usage:
|
|
10
|
-
// node scripts/required-checks.mjs <check-runs.ndjson>
|
|
11
|
-
// Evaluate the Checks API output (one `{name, status, conclusion}` JSON
|
|
12
|
-
// object per line). Prints the markdown summary on stdout and exits 0
|
|
13
|
-
// when every required check is green, 2 while any is still queued,
|
|
14
|
-
// running, or not yet reported (the caller polls), and 1 when a required
|
|
15
|
-
// check, or a present Windows vendor check, has a non-success conclusion.
|
|
16
|
-
// node scripts/required-checks.mjs --list
|
|
17
|
-
// Print the required names, one per line.
|
|
18
|
-
|
|
19
|
-
import fs from "node:fs";
|
|
20
|
-
import path from "node:path";
|
|
21
|
-
import { fileURLToPath } from "node:url";
|
|
22
|
-
|
|
23
|
-
export const REQUIRED_CHECKS = Object.freeze([
|
|
24
|
-
"Node 18 on ubuntu-latest",
|
|
25
|
-
"Node 18 on macos-latest",
|
|
26
|
-
"Node 18 on windows-latest",
|
|
27
|
-
"Node 24 on ubuntu-latest",
|
|
28
|
-
"Node 24 on macos-latest",
|
|
29
|
-
"Node 24 on windows-latest",
|
|
30
|
-
"Pinned macOS CLI installer contract",
|
|
31
|
-
"Pinned ChatGPT/Codex config contract",
|
|
32
|
-
"Pinned Claude config contract",
|
|
33
|
-
"Windows standard-user Codex config contract",
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
// The Windows vendor contract workflow is path-filtered, so its jobs may
|
|
37
|
-
// legitimately be absent. Present => must be green. Absent => loud warning
|
|
38
|
-
// telling the operator when a manual dispatch is owed.
|
|
39
|
-
export const CONDITIONAL_CHECKS = Object.freeze([
|
|
40
|
-
"Pinned Claude MSIX install and replacement",
|
|
41
|
-
"Pinned ChatGPT/Codex Store install",
|
|
42
|
-
]);
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Judge one commit's check runs. A name with any completed/success run is
|
|
46
|
-
* green (a re-run supersedes an earlier failure); a name with no run yet, or
|
|
47
|
-
* whose runs are all still queued or in progress, is pending; a name whose
|
|
48
|
-
* runs have all completed without a success has failed. Failure outranks
|
|
49
|
-
* pending so a superseded or broken SHA is refused as soon as it is known.
|
|
50
|
-
*/
|
|
51
|
-
export function evaluateCheckRuns(runs, { required = REQUIRED_CHECKS, conditional = CONDITIONAL_CHECKS } = {}) {
|
|
52
|
-
const byName = new Map();
|
|
53
|
-
for (const run of runs) {
|
|
54
|
-
if (!byName.has(run.name)) byName.set(run.name, []);
|
|
55
|
-
byName.get(run.name).push(run);
|
|
56
|
-
}
|
|
57
|
-
const runsFor = (name) => byName.get(name) ?? [];
|
|
58
|
-
const green = (name) => runsFor(name).some((run) => run.status === "completed" && run.conclusion === "success");
|
|
59
|
-
const unfinished = (name) => runsFor(name).some((run) => run.status !== "completed");
|
|
60
|
-
const describe = (name) => {
|
|
61
|
-
const found = runsFor(name);
|
|
62
|
-
if (found.length === 0) return "MISSING";
|
|
63
|
-
return found.map((run) => `${run.status}/${run.conclusion ?? "none"}`).join(", ");
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const failures = [];
|
|
67
|
-
const pending = [];
|
|
68
|
-
const absentConditional = [];
|
|
69
|
-
for (const name of required) {
|
|
70
|
-
if (green(name)) continue;
|
|
71
|
-
if (runsFor(name).length === 0 || unfinished(name)) pending.push(name);
|
|
72
|
-
else failures.push(`required check "${name}": ${describe(name)}`);
|
|
73
|
-
}
|
|
74
|
-
for (const name of conditional) {
|
|
75
|
-
if (runsFor(name).length === 0) absentConditional.push(name);
|
|
76
|
-
else if (green(name)) continue;
|
|
77
|
-
else if (unfinished(name)) pending.push(name);
|
|
78
|
-
else failures.push(`Windows vendor check "${name}": ${describe(name)}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const state = failures.length > 0 ? "failure" : pending.length > 0 ? "pending" : "success";
|
|
82
|
-
const summary = ["## Release gate: checks on the commit", ""];
|
|
83
|
-
summary.push("| Check | Result |", "| --- | --- |");
|
|
84
|
-
for (const name of [...required, ...conditional]) summary.push(`| ${name} | ${describe(name)} |`);
|
|
85
|
-
if (absentConditional.length > 0) {
|
|
86
|
-
summary.push(
|
|
87
|
-
"",
|
|
88
|
-
"> **Warning:** " + absentConditional.map((name) => `\`${name}\``).join(" and ")
|
|
89
|
-
+ " never ran on this commit. If this change touches bundle-affecting files"
|
|
90
|
-
+ " (src/apps.js, src/windowsApps.js, src/desktopTasks.js, src/codesign.js,"
|
|
91
|
-
+ " src/selfInvocation.js, or the Windows contract tests), dispatch the"
|
|
92
|
-
+ " \"Pinned Windows vendor app contract\" workflow against this commit.",
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
if (failures.length > 0) summary.push("", "**Gate failed:**", ...failures.map((failure) => `- ${failure}`));
|
|
96
|
-
if (state === "pending") summary.push("", "**Still waiting on:** " + pending.map((name) => `\`${name}\``).join(", "));
|
|
97
|
-
summary.push("", `Total check runs found on this commit: ${runs.length}`);
|
|
98
|
-
|
|
99
|
-
return { state, failures, pending, absentConditional, summary: summary.join("\n") + "\n" };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function main(argv) {
|
|
103
|
-
if (argv[0] === "--list") {
|
|
104
|
-
process.stdout.write(REQUIRED_CHECKS.join("\n") + "\n");
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (argv.length !== 1) {
|
|
108
|
-
process.stderr.write("usage: node scripts/required-checks.mjs <check-runs.ndjson> | --list\n");
|
|
109
|
-
process.exit(1);
|
|
110
|
-
}
|
|
111
|
-
const lines = fs.readFileSync(path.resolve(argv[0]), "utf8").split("\n").filter((line) => line.trim() !== "");
|
|
112
|
-
const runs = lines.map((line) => JSON.parse(line));
|
|
113
|
-
const result = evaluateCheckRuns(runs);
|
|
114
|
-
process.stdout.write(result.summary);
|
|
115
|
-
for (const failure of result.failures) process.stderr.write(`${failure}\n`);
|
|
116
|
-
if (result.state === "failure") process.exit(1);
|
|
117
|
-
if (result.state === "pending") {
|
|
118
|
-
process.stderr.write(`waiting for ${result.pending.map((name) => `"${name}": ${runs.some((run) => run.name === name) ? "pending" : "MISSING"}`).join(", ")}\n`);
|
|
119
|
-
process.exit(2);
|
|
120
|
-
}
|
|
121
|
-
process.stderr.write("All required checks are green on the pushed commit.\n");
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
125
|
-
main(process.argv.slice(2));
|
|
126
|
-
}
|