@decohere/ci-tools 0.0.1-dev.20261105.70 → 0.0.1-dev.20261105.79
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 +2 -2
- package/src/github/first-publish-check.mjs +70 -0
- package/src/github/npm-bootstrap.mjs +1 -1
- package/src/github/publish-verify.mjs +1 -3
- package/tests/github/first-publish-check.test.mjs +51 -0
- package/src/github/oidc-bootstrap-nudge.mjs +0 -214
- package/tests/github/oidc-bootstrap-nudge.test.mjs +0 -89
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decohere/ci-tools",
|
|
3
|
-
"version": "0.0.1-dev.20261105.
|
|
3
|
+
"version": "0.0.1-dev.20261105.79",
|
|
4
4
|
"description": "CI utility scripts for badge generation, coverage reporting, pages artifact assembly, and GitHub API tooling",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"./github/ensure-metadata": "./src/github/ensure-metadata.mjs",
|
|
44
44
|
"./github/create-or-update-pr": "./src/github/create-or-update-pr.mjs",
|
|
45
45
|
"./github/publish-verify": "./src/github/publish-verify.mjs",
|
|
46
|
-
"./github/
|
|
46
|
+
"./github/first-publish-check": "./src/github/first-publish-check.mjs",
|
|
47
47
|
"./github/npm-bootstrap": "./src/github/npm-bootstrap.mjs",
|
|
48
48
|
"./github/smoke-create-launcher": "./src/github/smoke-create-launcher.mjs"
|
|
49
49
|
},
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Finds packages in this workspace that aren't on npm yet. A first publish needs an owner's npm
|
|
4
|
+
* login, so CI can't do it. On a PR that's only a notice, so a contributor can propose a package
|
|
5
|
+
* with no npm access. In the merge queue it fails, because Publish could not upload that package.
|
|
6
|
+
*/
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { listPublishablePackages } from "./publish-verify.mjs";
|
|
10
|
+
|
|
11
|
+
const REGISTRY = "https://registry.npmjs.org";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {{
|
|
15
|
+
* packages: string[],
|
|
16
|
+
* blocking: boolean,
|
|
17
|
+
* fetch: typeof globalThis.fetch,
|
|
18
|
+
* log?: (message: string) => void,
|
|
19
|
+
* }} options
|
|
20
|
+
* @returns {Promise<number>} exit code
|
|
21
|
+
*/
|
|
22
|
+
export async function checkFirstPublish(options) {
|
|
23
|
+
const { packages, blocking, fetch } = options;
|
|
24
|
+
const log = options.log ?? console.log;
|
|
25
|
+
|
|
26
|
+
const results = await Promise.all(
|
|
27
|
+
packages.map(async (name) => {
|
|
28
|
+
// Not /-/package/<name>/dist-tags: for a name that does not exist it answers 401, not 404.
|
|
29
|
+
const url = `${REGISTRY}/${name.replace("/", "%2f")}`;
|
|
30
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
|
31
|
+
// Anything else, like an outage, says nothing about whether the name exists.
|
|
32
|
+
if (!response.ok && response.status !== 404) {
|
|
33
|
+
throw new Error(`Could not check ${name} on npm: ${response.status}`);
|
|
34
|
+
}
|
|
35
|
+
return { name, missing: response.status === 404 };
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const missing = results.filter((result) => result.missing).map((result) => result.name);
|
|
40
|
+
if (missing.length === 0) {
|
|
41
|
+
log(`All ${packages.length} packages are on npm.`);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
const names = missing.join(" ");
|
|
45
|
+
if (blocking) {
|
|
46
|
+
log(
|
|
47
|
+
`::error::Not on npm yet: ${names}. An owner runs \`pnpm npm:bootstrap --otp <code> ${names}\`, ` +
|
|
48
|
+
"then adds this PR to the merge queue again.",
|
|
49
|
+
);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
log(
|
|
53
|
+
`::notice::New package, not on npm yet: ${names}. Contributors have nothing to do: a maintainer ` +
|
|
54
|
+
`runs \`pnpm npm:bootstrap --otp <code> ${names}\` before this merges.`,
|
|
55
|
+
);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
process.argv[1] != null &&
|
|
61
|
+
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
|
|
62
|
+
) {
|
|
63
|
+
process.exit(
|
|
64
|
+
await checkFirstPublish({
|
|
65
|
+
packages: listPublishablePackages(process.cwd()).map((pkg) => pkg.name),
|
|
66
|
+
blocking: process.env.GITHUB_EVENT_NAME === "merge_group",
|
|
67
|
+
fetch: globalThis.fetch,
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -169,7 +169,7 @@ export async function runBootstrap(options = {}) {
|
|
|
169
169
|
"\nFor each new package, also run `pnpm npm:trust-promote --otp <code>`, so Promote to latest can move its tag.",
|
|
170
170
|
);
|
|
171
171
|
log(
|
|
172
|
-
"\nDone.
|
|
172
|
+
"\nDone. Add the PR to the merge queue. If Publish already failed on this, rerun it: `gh run rerun --failed <run-id>`.",
|
|
173
173
|
);
|
|
174
174
|
return 0;
|
|
175
175
|
}
|
|
@@ -199,8 +199,6 @@ if (isCli) {
|
|
|
199
199
|
appendFileSync(process.env.GITHUB_OUTPUT, `${body}\n`);
|
|
200
200
|
}
|
|
201
201
|
if (result.unpublished.length) {
|
|
202
|
-
process.stderr.write(
|
|
203
|
-
`Never-published packages (bootstrap OIDC before PUT): ${result.unpublished.join(", ")}\n`,
|
|
204
|
-
);
|
|
202
|
+
process.stderr.write(`Not on npm yet: ${result.unpublished.join(", ")}\n`);
|
|
205
203
|
}
|
|
206
204
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { checkFirstPublish } from "../../src/github/first-publish-check.mjs";
|
|
3
|
+
|
|
4
|
+
function fakeRegistry(statusByName) {
|
|
5
|
+
return async (url) => {
|
|
6
|
+
const name = decodeURIComponent(new URL(url).pathname.slice(1));
|
|
7
|
+
return new Response("{}", { status: statusByName[name] ?? 200 });
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function check(statusByName, blocking) {
|
|
12
|
+
const messages = [];
|
|
13
|
+
const code = await checkFirstPublish({
|
|
14
|
+
packages: ["@decohere/cli", "@decohere/shiny"],
|
|
15
|
+
blocking,
|
|
16
|
+
fetch: fakeRegistry(statusByName),
|
|
17
|
+
log: (message) => messages.push(message),
|
|
18
|
+
});
|
|
19
|
+
return { code, output: messages.join("\n") };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("checkFirstPublish", () => {
|
|
23
|
+
it("passes when every package is on npm", async () => {
|
|
24
|
+
const { code, output } = await check({}, true);
|
|
25
|
+
|
|
26
|
+
expect(code).toBe(0);
|
|
27
|
+
expect(output).toContain("All 2 packages are on npm");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("only leaves a notice on a PR, so a contributor can propose a package", async () => {
|
|
31
|
+
const { code, output } = await check({ "@decohere/shiny": 404 }, false);
|
|
32
|
+
|
|
33
|
+
expect(code).toBe(0);
|
|
34
|
+
expect(output).toContain("::notice::");
|
|
35
|
+
expect(output).toContain("pnpm npm:bootstrap --otp <code> @decohere/shiny");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("fails in the merge queue, naming the package and the command", async () => {
|
|
39
|
+
const { code, output } = await check({ "@decohere/shiny": 404 }, true);
|
|
40
|
+
|
|
41
|
+
expect(code).toBe(1);
|
|
42
|
+
expect(output).toContain("::error::");
|
|
43
|
+
expect(output).toContain("pnpm npm:bootstrap --otp <code> @decohere/shiny");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("doesn't mistake an npm outage for a new package", async () => {
|
|
47
|
+
await expect(check({ "@decohere/shiny": 503 }, false)).rejects.toThrow(
|
|
48
|
+
"Could not check @decohere/shiny on npm: 503",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { upsertIssue } from "../lib/github-issue.mjs";
|
|
4
|
-
import { needsPrereleaseTag, TRUSTED_PUBLISHER } from "./npm-bootstrap.mjs";
|
|
5
|
-
import { listPublishablePackages } from "./publish-verify.mjs";
|
|
6
|
-
|
|
7
|
-
export { needsPrereleaseTag } from "./npm-bootstrap.mjs";
|
|
8
|
-
|
|
9
|
-
const GITHUB_API = process.env.GITHUB_API_URL ?? "https://api.github.com";
|
|
10
|
-
const ISSUE_TITLE = "deploy(publish): OIDC bootstrap unpublished packages";
|
|
11
|
-
const BOOTSTRAP_ENV = "npm-oidc-bootstrap";
|
|
12
|
-
const DEFAULT_REPO = "typesmiths/decohere";
|
|
13
|
-
|
|
14
|
-
export function issueTitle() {
|
|
15
|
-
return ISSUE_TITLE;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** @param {string | undefined} raw */
|
|
19
|
-
export function parseUnpublished(raw) {
|
|
20
|
-
return String(raw ?? "")
|
|
21
|
-
.split(/\s+/)
|
|
22
|
-
.map((name) => name.trim())
|
|
23
|
-
.filter(Boolean);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @param {{ name: string, dir: string, version?: string }[]} packages
|
|
28
|
-
* @param {string} [channel]
|
|
29
|
-
*/
|
|
30
|
-
export function firstPublishCommands(packages, channel = "dev") {
|
|
31
|
-
if (packages.length === 0) {
|
|
32
|
-
return [`npm publish ./<package-dir> --tag ${channel}`];
|
|
33
|
-
}
|
|
34
|
-
return packages.map((pkg) => {
|
|
35
|
-
const tag = needsPrereleaseTag(pkg.version) ? ` --tag ${channel}` : "";
|
|
36
|
-
return `npm publish ./projects/${pkg.dir} --access public${tag}`;
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {unknown} environment
|
|
42
|
-
* @returns {string[]}
|
|
43
|
-
*/
|
|
44
|
-
export function extractGitHubReviewerLogins(environment) {
|
|
45
|
-
const rules =
|
|
46
|
-
environment && typeof environment === "object" ? environment.protection_rules : null;
|
|
47
|
-
if (!Array.isArray(rules)) {
|
|
48
|
-
return [];
|
|
49
|
-
}
|
|
50
|
-
const logins = [];
|
|
51
|
-
for (const rule of rules) {
|
|
52
|
-
const reviewers = rule?.reviewers;
|
|
53
|
-
if (!Array.isArray(reviewers)) {
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
for (const entry of reviewers) {
|
|
57
|
-
const login = entry?.reviewer?.login;
|
|
58
|
-
if (typeof login === "string" && login.length > 0 && entry?.type !== "Team") {
|
|
59
|
-
logins.push(login);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return [...new Set(logins)];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* @param {{
|
|
68
|
-
* unpublished: string[],
|
|
69
|
-
* packages: { name: string, dir: string, version?: string }[],
|
|
70
|
-
* repo?: string,
|
|
71
|
-
* runUrl?: string,
|
|
72
|
-
* channel?: string,
|
|
73
|
-
* }} options
|
|
74
|
-
*/
|
|
75
|
-
export function buildDescription(options) {
|
|
76
|
-
const repo = options.repo || DEFAULT_REPO;
|
|
77
|
-
const names = options.unpublished;
|
|
78
|
-
const channel = options.channel || "dev";
|
|
79
|
-
const packages = options.packages.filter((pkg) => names.includes(pkg.name));
|
|
80
|
-
const [owner, repoName] = repo.split("/");
|
|
81
|
-
return [
|
|
82
|
-
"## Summary",
|
|
83
|
-
"",
|
|
84
|
-
"These package names are not on npm yet, so OIDC `npm publish` cannot run until each name exists and has a Trusted Publisher:",
|
|
85
|
-
"",
|
|
86
|
-
...names.map((name) => `- \`${name}\``),
|
|
87
|
-
"",
|
|
88
|
-
"## What to do",
|
|
89
|
-
"",
|
|
90
|
-
"From a trusted machine, `npm login` to the org, then from the **repo root** (`--otp` is optional: npm asks for a code, or the browser login, whenever it needs one):",
|
|
91
|
-
"",
|
|
92
|
-
"```bash",
|
|
93
|
-
`pnpm npm:bootstrap --otp <code> ${names.join(" ")}`,
|
|
94
|
-
"```",
|
|
95
|
-
"",
|
|
96
|
-
"That does steps 1 and 2 below, skipping what is already done. To do them by hand instead:",
|
|
97
|
-
"",
|
|
98
|
-
"1. First-publish each name **public** (`--access public`: scoped names default to restricted). 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:",
|
|
99
|
-
"",
|
|
100
|
-
"```bash",
|
|
101
|
-
...firstPublishCommands(packages, channel),
|
|
102
|
-
"```",
|
|
103
|
-
"",
|
|
104
|
-
"2. npm → package settings → Trusted Publisher → GitHub Actions:",
|
|
105
|
-
"",
|
|
106
|
-
`- Organization: \`${owner ?? "typesmiths"}\``,
|
|
107
|
-
`- Repository: \`${repoName ?? "decohere"}\``,
|
|
108
|
-
`- Workflow filename: \`${TRUSTED_PUBLISHER.file}\``,
|
|
109
|
-
`- Environment: \`${TRUSTED_PUBLISHER.environment}\``,
|
|
110
|
-
"- Allowed action: `npm publish` (not staged-only)",
|
|
111
|
-
"",
|
|
112
|
-
"3. Approve the waiting **npm-oidc-bootstrap** deployment on this Publish run. The job re-checks that the names exist, then npm + Docker + tag continue. If reviewers were not configured and the job already failed:",
|
|
113
|
-
"",
|
|
114
|
-
"```bash",
|
|
115
|
-
`gh run list --repo ${repo} --workflow Publish --limit 5`,
|
|
116
|
-
`gh run rerun --failed <run-id> --repo ${repo}`,
|
|
117
|
-
"```",
|
|
118
|
-
"",
|
|
119
|
-
options.runUrl ? `- Workflow run: ${options.runUrl}` : null,
|
|
120
|
-
]
|
|
121
|
-
.filter((line) => line != null)
|
|
122
|
-
.join("\n");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function runUrl() {
|
|
126
|
-
if (
|
|
127
|
-
process.env.GITHUB_SERVER_URL &&
|
|
128
|
-
process.env.GITHUB_REPOSITORY &&
|
|
129
|
-
process.env.GITHUB_RUN_ID
|
|
130
|
-
) {
|
|
131
|
-
return `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
132
|
-
}
|
|
133
|
-
return "";
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* @param {{ fetch?: typeof fetch, token?: string, repo?: string, environment?: string }} options
|
|
138
|
-
*/
|
|
139
|
-
export async function githubEnvironmentReviewerLogins(options = {}) {
|
|
140
|
-
const token = options.token ?? process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? "";
|
|
141
|
-
const repo = options.repo ?? process.env.GITHUB_REPOSITORY ?? "";
|
|
142
|
-
const environment = options.environment ?? BOOTSTRAP_ENV;
|
|
143
|
-
const fetchFn = options.fetch ?? fetch;
|
|
144
|
-
if (!token || !repo) {
|
|
145
|
-
return [];
|
|
146
|
-
}
|
|
147
|
-
const response = await fetchFn(
|
|
148
|
-
`${GITHUB_API}/repos/${repo}/environments/${encodeURIComponent(environment)}`,
|
|
149
|
-
{
|
|
150
|
-
headers: {
|
|
151
|
-
Authorization: `Bearer ${token}`,
|
|
152
|
-
Accept: "application/vnd.github+json",
|
|
153
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
);
|
|
157
|
-
if (!response.ok) {
|
|
158
|
-
process.stderr.write(
|
|
159
|
-
`Could not list ${environment} reviewers (${response.status}); the issue will have no assignees\n`,
|
|
160
|
-
);
|
|
161
|
-
return [];
|
|
162
|
-
}
|
|
163
|
-
return extractGitHubReviewerLogins(await response.json());
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export async function runNudge(env = process.env, io = {}) {
|
|
167
|
-
const unpublished = parseUnpublished(env.UNPUBLISHED);
|
|
168
|
-
if (unpublished.length === 0) {
|
|
169
|
-
process.stdout.write("No unpublished package names; skipping OIDC bootstrap issue\n");
|
|
170
|
-
return 0;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const root = io.root ?? process.cwd();
|
|
174
|
-
const packages = (io.listPackages ?? listPublishablePackages)(root);
|
|
175
|
-
const assignees = await (io.githubReviewers ?? githubEnvironmentReviewerLogins)({
|
|
176
|
-
token: env.GITHUB_TOKEN ?? env.GH_TOKEN,
|
|
177
|
-
repo: env.GITHUB_REPOSITORY,
|
|
178
|
-
});
|
|
179
|
-
const repo = env.GITHUB_REPOSITORY || DEFAULT_REPO;
|
|
180
|
-
|
|
181
|
-
const result = (io.upsert ?? upsertIssue)({
|
|
182
|
-
title: issueTitle(),
|
|
183
|
-
body: buildDescription({
|
|
184
|
-
unpublished,
|
|
185
|
-
packages,
|
|
186
|
-
repo,
|
|
187
|
-
runUrl: io.runUrl ?? runUrl(),
|
|
188
|
-
channel: env.PUBLISH_CHANNEL || env.CHANNEL || "dev",
|
|
189
|
-
}),
|
|
190
|
-
assignees,
|
|
191
|
-
});
|
|
192
|
-
process.stdout.write(
|
|
193
|
-
`${result.created ? "Opened" : "Updated"} issue #${result.number} ${result.url}\n`,
|
|
194
|
-
);
|
|
195
|
-
if (assignees.length > 0) {
|
|
196
|
-
process.stdout.write(`Assigned ${assignees.join(", ")}\n`);
|
|
197
|
-
} else {
|
|
198
|
-
process.stdout.write(
|
|
199
|
-
`No assignees (add required reviewers on the ${BOOTSTRAP_ENV} environment)\n`,
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
if (env.GITHUB_OUTPUT) {
|
|
203
|
-
const { appendFileSync } = await import("node:fs");
|
|
204
|
-
appendFileSync(env.GITHUB_OUTPUT, `issue_url=${result.url}\n`);
|
|
205
|
-
}
|
|
206
|
-
return 0;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const isCli =
|
|
210
|
-
process.argv[1] != null && import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
|
|
211
|
-
|
|
212
|
-
if (isCli) {
|
|
213
|
-
process.exit(await runNudge());
|
|
214
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
buildDescription,
|
|
4
|
-
extractGitHubReviewerLogins,
|
|
5
|
-
firstPublishCommands,
|
|
6
|
-
issueTitle,
|
|
7
|
-
needsPrereleaseTag,
|
|
8
|
-
parseUnpublished,
|
|
9
|
-
runNudge,
|
|
10
|
-
} from "../../src/github/oidc-bootstrap-nudge.mjs";
|
|
11
|
-
|
|
12
|
-
describe("oidc-bootstrap-nudge", () => {
|
|
13
|
-
it("parses unpublished names", () => {
|
|
14
|
-
expect(parseUnpublished(" @decohere/scaffold @decohere/cli ")).toEqual([
|
|
15
|
-
"@decohere/scaffold",
|
|
16
|
-
"@decohere/cli",
|
|
17
|
-
]);
|
|
18
|
-
expect(issueTitle()).toBe("deploy(publish): OIDC bootstrap unpublished packages");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("extracts GitHub user reviewers and skips teams", () => {
|
|
22
|
-
expect(
|
|
23
|
-
extractGitHubReviewerLogins({
|
|
24
|
-
protection_rules: [
|
|
25
|
-
{ type: "branch_policy", reviewers: [] },
|
|
26
|
-
{
|
|
27
|
-
type: "required_reviewers",
|
|
28
|
-
reviewers: [
|
|
29
|
-
{ type: "User", reviewer: { login: "das974" } },
|
|
30
|
-
{ type: "Team", reviewer: { slug: "publishers", login: "should-skip" } },
|
|
31
|
-
],
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
}),
|
|
35
|
-
).toEqual(["das974"]);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("puts first-publish commands and Trusted Publisher fields in the task body", () => {
|
|
39
|
-
const pkg = { name: "@decohere/scaffold", dir: "scaffold", version: "0.0.1-dev.18" };
|
|
40
|
-
const body = buildDescription({
|
|
41
|
-
unpublished: ["@decohere/scaffold"],
|
|
42
|
-
packages: [pkg],
|
|
43
|
-
repo: "typesmiths/decohere",
|
|
44
|
-
runUrl: "https://github.com/typesmiths/decohere/actions/runs/9",
|
|
45
|
-
channel: "dev",
|
|
46
|
-
});
|
|
47
|
-
expect(needsPrereleaseTag("0.0.1-dev.18")).toBe(true);
|
|
48
|
-
expect(needsPrereleaseTag("1.2.3")).toBe(false);
|
|
49
|
-
expect(firstPublishCommands([pkg], "dev")).toEqual([
|
|
50
|
-
"npm publish ./projects/scaffold --access public --tag dev",
|
|
51
|
-
]);
|
|
52
|
-
expect(firstPublishCommands([{ ...pkg, version: "1.0.0" }], "dev")).toEqual([
|
|
53
|
-
"npm publish ./projects/scaffold --access public",
|
|
54
|
-
]);
|
|
55
|
-
expect(body).toContain("npm publish ./projects/scaffold --access public --tag dev");
|
|
56
|
-
expect(body).toContain("pnpm npm:bootstrap --otp <code> @decohere/scaffold");
|
|
57
|
-
expect(body).not.toMatch(/# @decohere\/scaffold/);
|
|
58
|
-
expect(body).toContain("First-publish each name **public**");
|
|
59
|
-
expect(body).toContain("Workflow filename: `publish.yml`");
|
|
60
|
-
expect(body).toContain("Environment: `npm-publish`");
|
|
61
|
-
expect(body).toContain("gh run rerun --failed");
|
|
62
|
-
expect(body).toContain("https://github.com/typesmiths/decohere/actions/runs/9");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("assigns the bootstrap environment's reviewers to the issue", async () => {
|
|
66
|
-
/** @type {object[]} */
|
|
67
|
-
const upserts = [];
|
|
68
|
-
const code = await runNudge(
|
|
69
|
-
{ UNPUBLISHED: "@decohere/scaffold", GITHUB_REPOSITORY: "typesmiths/decohere" },
|
|
70
|
-
{
|
|
71
|
-
listPackages: () => [{ name: "@decohere/scaffold", dir: "scaffold" }],
|
|
72
|
-
githubReviewers: async () => ["das974"],
|
|
73
|
-
upsert: (options) => {
|
|
74
|
-
upserts.push(options);
|
|
75
|
-
return {
|
|
76
|
-
number: 5,
|
|
77
|
-
url: "https://github.com/typesmiths/decohere/issues/5",
|
|
78
|
-
created: true,
|
|
79
|
-
};
|
|
80
|
-
},
|
|
81
|
-
runUrl: "https://github.com/typesmiths/decohere/actions/runs/1",
|
|
82
|
-
},
|
|
83
|
-
);
|
|
84
|
-
expect(code).toBe(0);
|
|
85
|
-
expect(upserts[0].title).toBe(issueTitle());
|
|
86
|
-
expect(upserts[0].assignees).toEqual(["das974"]);
|
|
87
|
-
expect(upserts[0].body).toContain("`@decohere/scaffold`");
|
|
88
|
-
});
|
|
89
|
-
});
|