@decohere/ci-tools 0.0.1-dev.127 → 0.0.1-dev.129

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decohere/ci-tools",
3
- "version": "0.0.1-dev.127",
3
+ "version": "0.0.1-dev.129",
4
4
  "description": "CI utility scripts for badge generation, coverage reporting, pages artifact assembly, and GitHub API tooling",
5
5
  "keywords": [
6
6
  "typescript",
@@ -10,9 +10,14 @@ import { pathToFileURL } from "node:url";
10
10
  * Must not use the repo cwd — after `@decohere/cli` is pinned, pnpm installs
11
11
  * `@decohere/create` into the workspace tree and npm's npx misses the bin.
12
12
  * No token: the packages are public, so this installs the launcher the way a user does.
13
+ * Anonymous reads go through npm's cache, which can lag minutes behind a publish, so it
14
+ * first waits until npm serves the version (`attempts` tries, `intervalMs` apart).
13
15
  *
14
16
  * @param {{
15
17
  * createVersion: string,
18
+ * attempts?: number,
19
+ * intervalMs?: number,
20
+ * sleep?: (ms: number) => void,
16
21
  * mkdtempSync?: typeof mkdtempSync,
17
22
  * writeFileSync?: typeof writeFileSync,
18
23
  * spawnSync?: typeof spawnSync,
@@ -30,14 +35,31 @@ export function runCreateLauncherSmoke(options) {
30
35
  const writeFile = options.writeFileSync ?? writeFileSync;
31
36
  const spawn = options.spawnSync ?? spawnSync;
32
37
  const tmp = options.tmpdir ?? tmpdir;
38
+ const sleep = options.sleep ?? sleepSync;
39
+ const attempts = options.attempts ?? 20;
40
+ const intervalMs = options.intervalMs ?? 15_000;
33
41
 
34
42
  const workDir = mkdtemp(join(tmp(), "decohere-create-smoke-"));
35
43
  const npmrc = join(workDir, ".npmrc");
36
44
  writeFile(npmrc, "@decohere:registry=https://registry.npmjs.org/\n");
37
45
 
38
- const result = spawn("npx", ["--yes", `@decohere/create@${version}`, "--help"], {
46
+ const env = { ...process.env, NPM_CONFIG_USERCONFIG: npmrc };
47
+ const spec = `@decohere/create@${version}`;
48
+
49
+ for (let attempt = 1; ; attempt++) {
50
+ const view = spawn("npm", ["view", spec, "version"], { cwd: workDir, env, stdio: "pipe" });
51
+ if (view.status === 0) break;
52
+ if (attempt >= attempts) {
53
+ console.error(`${spec} is still not served by npm after ${attempts} tries`);
54
+ return { workDir, status: 1 };
55
+ }
56
+ console.log(`${spec} not served by npm yet (try ${attempt} of ${attempts}); waiting`);
57
+ sleep(intervalMs);
58
+ }
59
+
60
+ const result = spawn("npx", ["--yes", spec, "--help"], {
39
61
  cwd: workDir,
40
- env: { ...process.env, NPM_CONFIG_USERCONFIG: npmrc },
62
+ env,
41
63
  stdio: options.stdio ?? "inherit",
42
64
  });
43
65
 
@@ -47,6 +69,11 @@ export function runCreateLauncherSmoke(options) {
47
69
  };
48
70
  }
49
71
 
72
+ /** @param {number} ms */
73
+ function sleepSync(ms) {
74
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
75
+ }
76
+
50
77
  const isCli =
51
78
  process.argv[1] != null && import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
52
79
 
@@ -0,0 +1,19 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ const fs = await vi.importActual("node:fs");
5
+ const CI_YML = fileURLToPath(new URL("../../../../.github/workflows/ci.yml", import.meta.url));
6
+
7
+ describe("ci.yml and the merge queue", () => {
8
+ const yml = fs.readFileSync(CI_YML, "utf8");
9
+ const triggers = yml.slice(yml.indexOf("\non:"), yml.indexOf("\nconcurrency:"));
10
+ const gate = yml.slice(yml.indexOf("- id: gate"), yml.indexOf("# Static gates"));
11
+
12
+ it("runs for queued pull requests, or the queue waits forever for the required check", () => {
13
+ expect(triggers).toMatch(/^\s+merge_group:/m);
14
+ });
15
+
16
+ it("runs the whole suite in the queue: the docs-only shortcut is only for pull requests", () => {
17
+ expect(gate).toContain('"${{ github.event_name }}" == "merge_group"');
18
+ });
19
+ });
@@ -29,11 +29,11 @@ describe("runCreateLauncherSmoke", () => {
29
29
 
30
30
  expect(result.workDir).toBe("/tmp/decohere-create-smoke-abc");
31
31
  expect(result.workDir).not.toBe(process.cwd());
32
- expect(calls).toHaveLength(1);
33
- expect(calls[0].cmd).toBe("npx");
34
- expect(calls[0].args).toEqual(["--yes", "@decohere/create@0.0.1-dev.80", "--help"]);
35
- expect(calls[0].opts.cwd).toBe("/tmp/decohere-create-smoke-abc");
36
- expect(calls[0].opts.cwd).not.toBe(process.cwd());
32
+ const npx = calls.at(-1);
33
+ expect(npx.cmd).toBe("npx");
34
+ expect(npx.args).toEqual(["--yes", "@decohere/create@0.0.1-dev.80", "--help"]);
35
+ expect(npx.opts.cwd).toBe("/tmp/decohere-create-smoke-abc");
36
+ expect(npx.opts.cwd).not.toBe(process.cwd());
37
37
  expect(written).toEqual([
38
38
  {
39
39
  path: "/tmp/decohere-create-smoke-abc/.npmrc",
@@ -41,7 +41,57 @@ describe("runCreateLauncherSmoke", () => {
41
41
  },
42
42
  ]);
43
43
  // The packages are public: the smoke installs them the way a user does, with no token.
44
- expect(calls[0].opts.env.NODE_AUTH_TOKEN).toBeUndefined();
44
+ expect(npx.opts.env.NODE_AUTH_TOKEN).toBeUndefined();
45
+ });
46
+
47
+ // npm's public cache can lag minutes behind a publish: the first token-free Publish ran npx
48
+ // 90 seconds after publishing and got ETARGET for a version the registry already had.
49
+ it("waits until npm serves the new version before running npx", () => {
50
+ /** @type {{ cmd: string, args: string[] }[]} */
51
+ const calls = [];
52
+ const sleeps = [];
53
+ let views = 0;
54
+
55
+ const result = runCreateLauncherSmoke({
56
+ createVersion: "0.0.1-dev.127",
57
+ tmpdir: () => "/tmp",
58
+ mkdtempSync: (prefix) => `${prefix}abc`,
59
+ writeFileSync: () => {},
60
+ spawnSync: (cmd, args) => {
61
+ calls.push({ cmd, args });
62
+ if (cmd === "npm") return { status: ++views < 3 ? 1 : 0 };
63
+ return { status: 0 };
64
+ },
65
+ sleep: (ms) => sleeps.push(ms),
66
+ stdio: "pipe",
67
+ });
68
+
69
+ expect(result.status).toBe(0);
70
+ expect(calls.map((call) => call.cmd)).toEqual(["npm", "npm", "npm", "npx"]);
71
+ expect(calls[0].args).toEqual(["view", "@decohere/create@0.0.1-dev.127", "version"]);
72
+ expect(sleeps).toHaveLength(2);
73
+ });
74
+
75
+ it("fails without running npx when npm never serves the version", () => {
76
+ /** @type {string[]} */
77
+ const commands = [];
78
+
79
+ const result = runCreateLauncherSmoke({
80
+ createVersion: "0.0.1-dev.127",
81
+ tmpdir: () => "/tmp",
82
+ mkdtempSync: (prefix) => `${prefix}abc`,
83
+ writeFileSync: () => {},
84
+ spawnSync: (cmd) => {
85
+ commands.push(cmd);
86
+ return { status: 1 };
87
+ },
88
+ sleep: () => {},
89
+ attempts: 3,
90
+ stdio: "pipe",
91
+ });
92
+
93
+ expect(result.status).toBe(1);
94
+ expect(commands).toEqual(["npm", "npm", "npm"]);
45
95
  });
46
96
 
47
97
  it("fails closed without a version", () => {