@decohere/ci-tools 0.0.1-dev.80 → 0.0.1-dev.82
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.
|
|
3
|
+
"version": "0.0.1-dev.82",
|
|
4
4
|
"description": "CI utility scripts for badge generation, coverage reporting, pages artifact assembly, and GitHub API tooling",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"./github/create-or-update-pr": "./src/github/create-or-update-pr.mjs",
|
|
37
37
|
"./github/publish-verify": "./src/github/publish-verify.mjs",
|
|
38
38
|
"./github/credential-nudge": "./src/github/credential-nudge.mjs",
|
|
39
|
-
"./github/oidc-bootstrap-nudge": "./src/github/oidc-bootstrap-nudge.mjs"
|
|
39
|
+
"./github/oidc-bootstrap-nudge": "./src/github/oidc-bootstrap-nudge.mjs",
|
|
40
|
+
"./github/smoke-create-launcher": "./src/github/smoke-create-launcher.mjs"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
43
|
"test": "vitest run"
|
|
@@ -5,8 +5,9 @@ import { listPublishablePackages } from "./publish-verify.mjs";
|
|
|
5
5
|
const LIST_ID = process.env.CLICKUP_LIST_ID ?? "901220171084";
|
|
6
6
|
const API = "https://api.clickup.com/api/v2";
|
|
7
7
|
const GITHUB_API = process.env.GITHUB_API_URL ?? "https://api.github.com";
|
|
8
|
-
const
|
|
9
|
-
const TASK_NAME = "
|
|
8
|
+
const DEPLOYMENT_TYPE = 1005;
|
|
9
|
+
const TASK_NAME = "deploy(publish): OIDC bootstrap unpublished packages";
|
|
10
|
+
const LEGACY_TASK_NAMES = ["chore(publish): OIDC bootstrap unpublished packages"];
|
|
10
11
|
const BOOTSTRAP_ENV = "npm-oidc-bootstrap";
|
|
11
12
|
const DEFAULT_REPO = "typesmiths/decohere";
|
|
12
13
|
|
|
@@ -14,6 +15,10 @@ export function taskName() {
|
|
|
14
15
|
return TASK_NAME;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
export function bootstrapTaskNames() {
|
|
19
|
+
return [TASK_NAME, ...LEGACY_TASK_NAMES];
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
/** @param {string | undefined} raw */
|
|
18
23
|
export function parseUnpublished(raw) {
|
|
19
24
|
return String(raw ?? "")
|
|
@@ -31,15 +36,24 @@ export function parseAssigneeIds(raw) {
|
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
|
-
* @param {
|
|
39
|
+
* @param {string | undefined} version
|
|
35
40
|
*/
|
|
36
|
-
export function
|
|
41
|
+
export function needsPrereleaseTag(version) {
|
|
42
|
+
return String(version ?? "").includes("-");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {{ name: string, dir: string, version?: string }[]} packages
|
|
47
|
+
* @param {string} [channel]
|
|
48
|
+
*/
|
|
49
|
+
export function firstPublishCommands(packages, channel = "dev") {
|
|
37
50
|
if (packages.length === 0) {
|
|
38
|
-
return [
|
|
39
|
-
"npm publish # from the unpublished package directory (do not pass --access public)",
|
|
40
|
-
];
|
|
51
|
+
return [`npm publish ./<package-dir> --tag ${channel}`];
|
|
41
52
|
}
|
|
42
|
-
return packages.map((pkg) =>
|
|
53
|
+
return packages.map((pkg) => {
|
|
54
|
+
const tag = needsPrereleaseTag(pkg.version) ? ` --tag ${channel}` : "";
|
|
55
|
+
return `npm publish ./projects/${pkg.dir}${tag}`;
|
|
56
|
+
});
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
/**
|
|
@@ -94,14 +108,16 @@ export function mapLoginsToClickUpIds(logins, members) {
|
|
|
94
108
|
/**
|
|
95
109
|
* @param {{
|
|
96
110
|
* unpublished: string[],
|
|
97
|
-
* packages: { name: string, dir: string }[],
|
|
111
|
+
* packages: { name: string, dir: string, version?: string }[],
|
|
98
112
|
* repo?: string,
|
|
99
113
|
* runUrl?: string,
|
|
114
|
+
* channel?: string,
|
|
100
115
|
* }} options
|
|
101
116
|
*/
|
|
102
117
|
export function buildDescription(options) {
|
|
103
118
|
const repo = options.repo || DEFAULT_REPO;
|
|
104
119
|
const names = options.unpublished;
|
|
120
|
+
const channel = options.channel || "dev";
|
|
105
121
|
const packages = options.packages.filter((pkg) => names.includes(pkg.name));
|
|
106
122
|
const [owner, repoName] = repo.split("/");
|
|
107
123
|
return [
|
|
@@ -113,10 +129,10 @@ export function buildDescription(options) {
|
|
|
113
129
|
"",
|
|
114
130
|
"## What to do",
|
|
115
131
|
"",
|
|
116
|
-
"1. First-publish each name **restricted** (scoped default — do **not** pass `--access public`). From a trusted machine, `npm login` to the org, then:",
|
|
132
|
+
"1. First-publish each name **restricted** (scoped default — do **not** pass `--access public`). From a trusted machine, `npm login` to the org, then from the **repo root**. Use `./` so npm does not treat the path as a GitHub `owner/repo`. `--tag` is required for prerelease versions (`x.y.z-dev.N`). Missing `bin`/`es6` warnings are OK for this name-only bootstrap; CI `forge publish` ships the built tarball later:",
|
|
117
133
|
"",
|
|
118
134
|
"```bash",
|
|
119
|
-
...firstPublishCommands(packages),
|
|
135
|
+
...firstPublishCommands(packages, channel),
|
|
120
136
|
"```",
|
|
121
137
|
"",
|
|
122
138
|
"2. npm → package settings → Trusted Publisher → GitHub Actions:",
|
|
@@ -162,7 +178,8 @@ async function clickup(method, path, body) {
|
|
|
162
178
|
return text.length === 0 ? undefined : JSON.parse(text);
|
|
163
179
|
}
|
|
164
180
|
|
|
165
|
-
async function findTaskByName(
|
|
181
|
+
async function findTaskByName(names) {
|
|
182
|
+
const wanted = new Set(Array.isArray(names) ? names : [names]);
|
|
166
183
|
for (let page = 0; page < 10; page += 1) {
|
|
167
184
|
const qs = new URLSearchParams({
|
|
168
185
|
include_closed: "true",
|
|
@@ -171,7 +188,7 @@ async function findTaskByName(name) {
|
|
|
171
188
|
});
|
|
172
189
|
const data = await clickup("GET", `/list/${LIST_ID}/task?${qs}`);
|
|
173
190
|
const tasks = Array.isArray(data) ? data : (data?.tasks ?? []);
|
|
174
|
-
const match = tasks.find((task) => task.name
|
|
191
|
+
const match = tasks.find((task) => wanted.has(task.name));
|
|
175
192
|
if (match) {
|
|
176
193
|
return match;
|
|
177
194
|
}
|
|
@@ -248,16 +265,16 @@ export async function clickupWorkspaceMembers() {
|
|
|
248
265
|
}
|
|
249
266
|
|
|
250
267
|
/**
|
|
251
|
-
* @param {{ unpublished: string[], packages: { name: string, dir: string }[], assigneeIds: number[], repo?: string, runUrl?: string }} options
|
|
268
|
+
* @param {{ unpublished: string[], packages: { name: string, dir: string, version?: string }[], assigneeIds: number[], repo?: string, runUrl?: string, channel?: string }} options
|
|
252
269
|
*/
|
|
253
270
|
export async function upsertBootstrapTask(options) {
|
|
254
271
|
const name = taskName();
|
|
255
|
-
const existing = await findTaskByName(
|
|
272
|
+
const existing = await findTaskByName(bootstrapTaskNames());
|
|
256
273
|
const markdown = buildDescription(options);
|
|
257
274
|
const payload = {
|
|
258
275
|
name,
|
|
259
276
|
markdown_description: markdown,
|
|
260
|
-
custom_item_id:
|
|
277
|
+
custom_item_id: DEPLOYMENT_TYPE,
|
|
261
278
|
priority: 1,
|
|
262
279
|
};
|
|
263
280
|
if (options.assigneeIds.length > 0 && !existing) {
|
|
@@ -266,7 +283,9 @@ export async function upsertBootstrapTask(options) {
|
|
|
266
283
|
|
|
267
284
|
if (existing) {
|
|
268
285
|
const update = {
|
|
286
|
+
name,
|
|
269
287
|
markdown_description: markdown,
|
|
288
|
+
custom_item_id: DEPLOYMENT_TYPE,
|
|
270
289
|
priority: 1,
|
|
271
290
|
};
|
|
272
291
|
if (existing.closed || existing.status?.type === "closed") {
|
|
@@ -318,6 +337,7 @@ export async function runNudge(env = process.env, io = {}) {
|
|
|
318
337
|
assigneeIds,
|
|
319
338
|
repo: env.GITHUB_REPOSITORY || DEFAULT_REPO,
|
|
320
339
|
runUrl: io.runUrl ?? runUrl(),
|
|
340
|
+
channel: env.PUBLISH_CHANNEL || env.CHANNEL || "dev",
|
|
321
341
|
});
|
|
322
342
|
const url = result.url ?? "";
|
|
323
343
|
process.stdout.write(
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Run `npx @decohere/create@version --help` from an empty temp directory.
|
|
10
|
+
* Must not use the repo cwd — after `@decohere/cli` is pinned, pnpm installs
|
|
11
|
+
* `@decohere/create` into the workspace tree and npm's npx misses the bin.
|
|
12
|
+
*
|
|
13
|
+
* @param {{
|
|
14
|
+
* createVersion: string,
|
|
15
|
+
* authToken: string,
|
|
16
|
+
* mkdtempSync?: typeof mkdtempSync,
|
|
17
|
+
* writeFileSync?: typeof writeFileSync,
|
|
18
|
+
* spawnSync?: typeof spawnSync,
|
|
19
|
+
* tmpdir?: () => string,
|
|
20
|
+
* stdio?: import("node:child_process").StdioOptions,
|
|
21
|
+
* }} options
|
|
22
|
+
*/
|
|
23
|
+
export function runCreateLauncherSmoke(options) {
|
|
24
|
+
const version = options.createVersion?.trim() ?? "";
|
|
25
|
+
const token = options.authToken?.trim() ?? "";
|
|
26
|
+
if (!version) {
|
|
27
|
+
throw new Error("CREATE_VERSION is required");
|
|
28
|
+
}
|
|
29
|
+
if (!token) {
|
|
30
|
+
throw new Error("NODE_AUTH_TOKEN is required");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const mkdtemp = options.mkdtempSync ?? mkdtempSync;
|
|
34
|
+
const writeFile = options.writeFileSync ?? writeFileSync;
|
|
35
|
+
const spawn = options.spawnSync ?? spawnSync;
|
|
36
|
+
const tmp = options.tmpdir ?? tmpdir;
|
|
37
|
+
|
|
38
|
+
const workDir = mkdtemp(join(tmp(), "decohere-create-smoke-"));
|
|
39
|
+
const npmrc = join(workDir, ".npmrc");
|
|
40
|
+
writeFile(
|
|
41
|
+
npmrc,
|
|
42
|
+
`@decohere:registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=${token}\n`,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const result = spawn("npx", ["--yes", `@decohere/create@${version}`, "--help"], {
|
|
46
|
+
cwd: workDir,
|
|
47
|
+
env: {
|
|
48
|
+
...process.env,
|
|
49
|
+
NPM_CONFIG_USERCONFIG: npmrc,
|
|
50
|
+
NODE_AUTH_TOKEN: token,
|
|
51
|
+
},
|
|
52
|
+
stdio: options.stdio ?? "inherit",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
workDir,
|
|
57
|
+
status: result.status ?? 1,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const isCli =
|
|
62
|
+
process.argv[1] != null && import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
|
|
63
|
+
|
|
64
|
+
if (isCli) {
|
|
65
|
+
const { status } = runCreateLauncherSmoke({
|
|
66
|
+
createVersion: process.env.CREATE_VERSION ?? "",
|
|
67
|
+
authToken: process.env.NODE_AUTH_TOKEN ?? "",
|
|
68
|
+
});
|
|
69
|
+
process.exit(status);
|
|
70
|
+
}
|
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
import { execSync } from "node:child_process";
|
|
8
8
|
|
|
9
9
|
const DRY_RUN = process.env.DRY_RUN === "true";
|
|
10
|
-
const TERMINAL_CLI = process.env.TERMINAL_CLI || "dist/debug/projects/cli/src/cli.js";
|
|
11
10
|
const DECOHERE_CONFIG = process.env.DECOHERE_CONFIG || ".decohere.js";
|
|
12
11
|
|
|
13
|
-
const baseCmd = `
|
|
12
|
+
const baseCmd = `pnpm exec flux run projects/cli/src/forge.ts -- --log-level info --config-file "${DECOHERE_CONFIG}" dependencies --configuration release sync`;
|
|
14
13
|
|
|
15
14
|
try {
|
|
16
15
|
if (DRY_RUN) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
bootstrapTaskNames,
|
|
3
4
|
buildDescription,
|
|
4
5
|
extractGitHubReviewerLogins,
|
|
5
6
|
firstPublishCommands,
|
|
6
7
|
mapLoginsToClickUpIds,
|
|
8
|
+
needsPrereleaseTag,
|
|
7
9
|
parseAssigneeIds,
|
|
8
10
|
parseUnpublished,
|
|
9
11
|
runNudge,
|
|
@@ -18,7 +20,10 @@ describe("oidc-bootstrap-nudge", () => {
|
|
|
18
20
|
]);
|
|
19
21
|
expect(parseAssigneeIds("308432336, 99")).toEqual([308432336, 99]);
|
|
20
22
|
expect(parseAssigneeIds("")).toEqual([]);
|
|
21
|
-
expect(taskName()).toBe("
|
|
23
|
+
expect(taskName()).toBe("deploy(publish): OIDC bootstrap unpublished packages");
|
|
24
|
+
expect(bootstrapTaskNames()).toContain(
|
|
25
|
+
"chore(publish): OIDC bootstrap unpublished packages",
|
|
26
|
+
);
|
|
22
27
|
});
|
|
23
28
|
|
|
24
29
|
it("extracts GitHub user reviewers and maps login to ClickUp email local-part", () => {
|
|
@@ -48,20 +53,26 @@ describe("oidc-bootstrap-nudge", () => {
|
|
|
48
53
|
});
|
|
49
54
|
|
|
50
55
|
it("puts first-publish commands and Trusted Publisher fields in the task body", () => {
|
|
56
|
+
const pkg = { name: "@decohere/scaffold", dir: "scaffold", version: "0.0.1-dev.18" };
|
|
51
57
|
const body = buildDescription({
|
|
52
58
|
unpublished: ["@decohere/scaffold"],
|
|
53
|
-
packages: [
|
|
59
|
+
packages: [pkg],
|
|
54
60
|
repo: "typesmiths/decohere",
|
|
55
61
|
runUrl: "https://github.com/typesmiths/decohere/actions/runs/9",
|
|
62
|
+
channel: "dev",
|
|
56
63
|
});
|
|
57
|
-
expect(
|
|
58
|
-
|
|
64
|
+
expect(needsPrereleaseTag("0.0.1-dev.18")).toBe(true);
|
|
65
|
+
expect(needsPrereleaseTag("1.2.3")).toBe(false);
|
|
66
|
+
expect(firstPublishCommands([pkg], "dev")).toEqual([
|
|
67
|
+
"npm publish ./projects/scaffold --tag dev",
|
|
68
|
+
]);
|
|
69
|
+
expect(firstPublishCommands([{ ...pkg, version: "1.0.0" }], "dev")).toEqual([
|
|
70
|
+
"npm publish ./projects/scaffold",
|
|
59
71
|
]);
|
|
60
|
-
expect(body).toContain("npm publish projects/scaffold");
|
|
72
|
+
expect(body).toContain("npm publish ./projects/scaffold --tag dev");
|
|
73
|
+
expect(body).not.toMatch(/# @decohere\/scaffold/);
|
|
61
74
|
expect(body).toContain("do **not** pass `--access public`");
|
|
62
|
-
expect(
|
|
63
|
-
firstPublishCommands([{ name: "@decohere/scaffold", dir: "scaffold" }]).join("\n"),
|
|
64
|
-
).not.toContain("--access public");
|
|
75
|
+
expect(firstPublishCommands([pkg], "dev").join("\n")).not.toContain("--access public");
|
|
65
76
|
expect(body).toContain("Workflow filename: `publish.yml`");
|
|
66
77
|
expect(body).toContain("Environment: `npm-publish`");
|
|
67
78
|
expect(body).toContain("gh run rerun --failed");
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { runCreateLauncherSmoke } from "../../src/github/smoke-create-launcher.mjs";
|
|
4
|
+
|
|
5
|
+
const fs = await vi.importActual("node:fs");
|
|
6
|
+
const PUBLISH_YML = fileURLToPath(
|
|
7
|
+
new URL("../../../../.github/workflows/publish.yml", import.meta.url),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
describe("runCreateLauncherSmoke", () => {
|
|
11
|
+
it("execs npx from a temp directory, not the repo root", () => {
|
|
12
|
+
/** @type {{ cmd: string, args: string[], opts: { cwd?: string } }[]} */
|
|
13
|
+
const calls = [];
|
|
14
|
+
const written = [];
|
|
15
|
+
|
|
16
|
+
const result = runCreateLauncherSmoke({
|
|
17
|
+
createVersion: "0.0.1-dev.80",
|
|
18
|
+
authToken: "tok",
|
|
19
|
+
tmpdir: () => "/tmp",
|
|
20
|
+
mkdtempSync: (prefix) => `${prefix}abc`,
|
|
21
|
+
writeFileSync: (path, body) => {
|
|
22
|
+
written.push({ path, body });
|
|
23
|
+
},
|
|
24
|
+
spawnSync: (cmd, args, opts) => {
|
|
25
|
+
calls.push({ cmd, args, opts });
|
|
26
|
+
return { status: 0 };
|
|
27
|
+
},
|
|
28
|
+
stdio: "pipe",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(result.workDir).toBe("/tmp/decohere-create-smoke-abc");
|
|
32
|
+
expect(result.workDir).not.toBe(process.cwd());
|
|
33
|
+
expect(calls).toHaveLength(1);
|
|
34
|
+
expect(calls[0].cmd).toBe("npx");
|
|
35
|
+
expect(calls[0].args).toEqual(["--yes", "@decohere/create@0.0.1-dev.80", "--help"]);
|
|
36
|
+
expect(calls[0].opts.cwd).toBe("/tmp/decohere-create-smoke-abc");
|
|
37
|
+
expect(calls[0].opts.cwd).not.toBe(process.cwd());
|
|
38
|
+
expect(written).toEqual([
|
|
39
|
+
{
|
|
40
|
+
path: "/tmp/decohere-create-smoke-abc/.npmrc",
|
|
41
|
+
body: "@decohere:registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=tok\n",
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("fails closed without version or token", () => {
|
|
47
|
+
expect(() => runCreateLauncherSmoke({ createVersion: "", authToken: "tok" })).toThrow(
|
|
48
|
+
"CREATE_VERSION is required",
|
|
49
|
+
);
|
|
50
|
+
expect(() =>
|
|
51
|
+
runCreateLauncherSmoke({ createVersion: "0.0.1-dev.80", authToken: "" }),
|
|
52
|
+
).toThrow("NODE_AUTH_TOKEN is required");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("publish.yml create smoke + tag", () => {
|
|
57
|
+
const yml = fs.readFileSync(PUBLISH_YML, "utf8");
|
|
58
|
+
|
|
59
|
+
it("wires create smoke through the isolated launcher", () => {
|
|
60
|
+
expect(yml).toContain("node projects/ci-tools/src/github/smoke-create-launcher.mjs");
|
|
61
|
+
expect(yml).toContain("CREATE_VERSION:");
|
|
62
|
+
expect(yml).not.toContain("npx --yes");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("grants the tag job actions:read so publish-prepare can download CI artifacts", () => {
|
|
66
|
+
const tag = yml.slice(yml.indexOf("\n tag:"));
|
|
67
|
+
const perms = tag.slice(tag.indexOf("permissions:"), tag.indexOf("steps:"));
|
|
68
|
+
expect(perms).toMatch(/contents:\s*write/);
|
|
69
|
+
expect(perms).toMatch(/actions:\s*read/);
|
|
70
|
+
});
|
|
71
|
+
});
|