@moku-labs/ci 1.2.2 → 1.2.4
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/dist/release.mjs +128 -14
- package/package.json +1 -1
package/dist/release.mjs
CHANGED
|
@@ -788,6 +788,60 @@ function isAsciiOnly(text) {
|
|
|
788
788
|
return !NON_ASCII.test(text);
|
|
789
789
|
}
|
|
790
790
|
/**
|
|
791
|
+
* The one-line form a top-level value had in the source, e.g. `["dist", "LICENSE"]`.
|
|
792
|
+
*
|
|
793
|
+
* @param source - The original file contents.
|
|
794
|
+
* @param key - The top-level key to look up.
|
|
795
|
+
* @returns The inline text, or `undefined` when the value spanned several lines or is absent.
|
|
796
|
+
* @example
|
|
797
|
+
* inlineValueOf('{\n "files": ["dist"],\n}', "files"); // '["dist"]'
|
|
798
|
+
*/
|
|
799
|
+
function inlineValueOf(source, key) {
|
|
800
|
+
const prefix = ` ${JSON.stringify(key)}: `;
|
|
801
|
+
const line = source.split("\n").find((candidate) => candidate.startsWith(prefix));
|
|
802
|
+
if (line === void 0) return void 0;
|
|
803
|
+
const value = line.slice(prefix.length).replace(/,\s*$/, "");
|
|
804
|
+
return /^[[{].*[\]}]$/.test(value) ? value : void 0;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Put back the one-line objects and arrays the source had. Formatters such as biome keep a
|
|
808
|
+
* short `"files": ["dist"]` on one line; `JSON.stringify` expands it, and an untouched value
|
|
809
|
+
* then shows up as changed lines in the diff. Only top-level, unchanged values are restored.
|
|
810
|
+
*
|
|
811
|
+
* @param manifest - The manifest being written.
|
|
812
|
+
* @param expanded - Its `JSON.stringify` rendering.
|
|
813
|
+
* @param source - The original file contents.
|
|
814
|
+
* @returns The rendering with the untouched inline values restored.
|
|
815
|
+
* @example
|
|
816
|
+
* keepInlineValues({ files: ["dist"] }, '{\n "files": [\n "dist"\n ]\n}\n', source);
|
|
817
|
+
*/
|
|
818
|
+
function keepInlineValues(manifest, expanded, source) {
|
|
819
|
+
let text = expanded;
|
|
820
|
+
for (const [key, value] of Object.entries(manifest)) {
|
|
821
|
+
const inline = inlineValueOf(source, key);
|
|
822
|
+
if (inline === void 0 || parseManifestValue(inline) !== JSON.stringify(value)) continue;
|
|
823
|
+
const block = JSON.stringify(value, void 0, 2).replaceAll("\n", "\n ");
|
|
824
|
+
const label = ` ${JSON.stringify(key)}: `;
|
|
825
|
+
text = text.replace(`${label}${block}`, () => `${label}${inline}`);
|
|
826
|
+
}
|
|
827
|
+
return text;
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* The canonical JSON of an inline value, for comparing it with the value being written.
|
|
831
|
+
*
|
|
832
|
+
* @param inline - The one-line JSON text.
|
|
833
|
+
* @returns Its compact re-serialization, or `undefined` when it does not parse.
|
|
834
|
+
* @example
|
|
835
|
+
* parseManifestValue('["dist", "LICENSE"]'); // '["dist","LICENSE"]'
|
|
836
|
+
*/
|
|
837
|
+
function parseManifestValue(inline) {
|
|
838
|
+
try {
|
|
839
|
+
return JSON.stringify(JSON.parse(inline));
|
|
840
|
+
} catch {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
791
845
|
* Render a manifest the way npm itself writes one: two-space JSON with a trailing newline.
|
|
792
846
|
* A source file that was ASCII-only stays ASCII-only, so an escaped dash the package chose
|
|
793
847
|
* does not show up as a changed line in the diff.
|
|
@@ -799,8 +853,10 @@ function isAsciiOnly(text) {
|
|
|
799
853
|
* await files.write("package.json", formatManifest(manifest, source));
|
|
800
854
|
*/
|
|
801
855
|
function formatManifest(manifest, source) {
|
|
802
|
-
const
|
|
803
|
-
if (source === void 0
|
|
856
|
+
const expanded = `${JSON.stringify(manifest, void 0, 2)}\n`;
|
|
857
|
+
if (source === void 0) return expanded;
|
|
858
|
+
const text = keepInlineValues(manifest, expanded, source);
|
|
859
|
+
if (!isAsciiOnly(source)) return text;
|
|
804
860
|
return text.replaceAll(new RegExp(NON_ASCII, "g"), (character) => String.raw`\u${character.codePointAt(0)?.toString(16).padStart(4, "0")}`);
|
|
805
861
|
}
|
|
806
862
|
/**
|
|
@@ -1596,6 +1652,8 @@ const tagSyncCheck = {
|
|
|
1596
1652
|
*/
|
|
1597
1653
|
/** Basename npm registers the publisher against — the workflow file's name, not its path. */
|
|
1598
1654
|
const PUBLISH_WORKFLOW_FILE$1 = ".github/workflows/publish.yml".split("/").pop() ?? "publish.yml";
|
|
1655
|
+
/** Detail of the result when the listing sits behind 2FA; `setup` offers to register anyway. */
|
|
1656
|
+
const TRUST_NEEDS_OTP = "npm asks for an OTP, cannot verify from here";
|
|
1599
1657
|
/**
|
|
1600
1658
|
* Whether npm's output says the `trust` command itself does not exist.
|
|
1601
1659
|
*
|
|
@@ -1667,7 +1725,7 @@ const trustedPublisherCheck = {
|
|
|
1667
1725
|
]);
|
|
1668
1726
|
if (isUnknownCommand(`${listing.stdout}${listing.stderr}`)) return warn("this npm has no `trust` command", "upgrade npm");
|
|
1669
1727
|
if (isUnauthorized(`${listing.stdout}${listing.stderr}`)) return skip("cannot list trusted publishers without `npm login`");
|
|
1670
|
-
if (isOtpRequired(`${listing.stdout}${listing.stderr}`)) return warn(
|
|
1728
|
+
if (isOtpRequired(`${listing.stdout}${listing.stderr}`)) return warn(TRUST_NEEDS_OTP, `npm trust list ${manifest.name}`);
|
|
1671
1729
|
if (listing.code !== 0 || !listing.stdout.includes(PUBLISH_WORKFLOW_FILE$1)) return fail("no trusted publisher registered", trustCommand(manifest.name, ownerRepo));
|
|
1672
1730
|
return pass(`${ownerRepo} · ${PUBLISH_WORKFLOW_FILE$1}`);
|
|
1673
1731
|
}
|
|
@@ -2094,6 +2152,22 @@ function deferred(setup, description) {
|
|
|
2094
2152
|
return true;
|
|
2095
2153
|
}
|
|
2096
2154
|
/**
|
|
2155
|
+
* Hand a step that needs the owner's OTP back to the owner. npm asks for the OTP in a
|
|
2156
|
+
* browser; with nobody at the keyboard (`--yes`) that would hang, so the command is printed.
|
|
2157
|
+
*
|
|
2158
|
+
* @param setup - The wizard state.
|
|
2159
|
+
* @param step - What the step does, as a noun phrase ("first publish").
|
|
2160
|
+
* @param command - The command the owner runs themselves.
|
|
2161
|
+
* @returns `true` when the run is unattended and the caller must not act.
|
|
2162
|
+
* @example
|
|
2163
|
+
* if (needsOwner(setup, "first publish", "npm publish --access public")) return false;
|
|
2164
|
+
*/
|
|
2165
|
+
function needsOwner(setup, step, command) {
|
|
2166
|
+
if (!setup.unattended) return false;
|
|
2167
|
+
setup.ui.check(false, `${step} needs your OTP, run it yourself`, command);
|
|
2168
|
+
return true;
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2097
2171
|
* Verify the two human-only prerequisites. Nothing else in the wizard can run without
|
|
2098
2172
|
* them, and neither can be automated — the wizard prints the command and stops.
|
|
2099
2173
|
*
|
|
@@ -2222,7 +2296,7 @@ async function normalizeContract(setup, manifest) {
|
|
|
2222
2296
|
* @param setup - The wizard state.
|
|
2223
2297
|
* @param name - The package name.
|
|
2224
2298
|
* @param version - The version about to be published.
|
|
2225
|
-
* @returns `true` when
|
|
2299
|
+
* @returns `true` when this run performed the first publish.
|
|
2226
2300
|
* @example
|
|
2227
2301
|
* await firstPublish(setup, "@moku-labs/common", "0.2.0");
|
|
2228
2302
|
*/
|
|
@@ -2235,13 +2309,14 @@ async function firstPublish(setup, name, version) {
|
|
|
2235
2309
|
]);
|
|
2236
2310
|
if (view.code === 0) {
|
|
2237
2311
|
setup.ui.check(true, `${name}@${view.stdout.trim()} already on npm`);
|
|
2238
|
-
return
|
|
2312
|
+
return false;
|
|
2239
2313
|
}
|
|
2240
2314
|
if (!await setup.prompts.confirm(`Publish ${name}@${version} to npm now?`)) {
|
|
2241
2315
|
setup.ui.check(false, "first publish skipped");
|
|
2242
2316
|
return false;
|
|
2243
2317
|
}
|
|
2244
2318
|
if (deferred(setup, `run bun run build && npm publish --access public`)) return false;
|
|
2319
|
+
if (needsOwner(setup, "first publish", "bun run build && npm publish --access public")) return false;
|
|
2245
2320
|
if (await setup.ctx.exec.inherit("bun", ["run", "build"]) !== 0) {
|
|
2246
2321
|
setup.ui.error("build failed — not publishing");
|
|
2247
2322
|
return false;
|
|
@@ -2307,14 +2382,49 @@ async function pushVersionTag(setup, version) {
|
|
|
2307
2382
|
async function registerTrustedPublisher(setup) {
|
|
2308
2383
|
setup.ui.heading("Trusted publisher");
|
|
2309
2384
|
const result = await trustedPublisherCheck.run(setup.ctx);
|
|
2310
|
-
if (result.status
|
|
2311
|
-
setup.ui.check(
|
|
2385
|
+
if (result.status === "pass") {
|
|
2386
|
+
setup.ui.check(true, result.detail);
|
|
2312
2387
|
return;
|
|
2313
2388
|
}
|
|
2314
|
-
|
|
2315
|
-
|
|
2389
|
+
const registration = result.detail === "npm asks for an OTP, cannot verify from here" ? await registrationBehindOtp(setup) : failFix(result);
|
|
2390
|
+
if (registration === void 0) {
|
|
2391
|
+
setup.ui.check(false, result.detail, result.fix);
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
if (deferred(setup, registration)) return;
|
|
2395
|
+
if (needsOwner(setup, "trusted publisher registration", registration)) return;
|
|
2396
|
+
const [command = "npm", ...args] = registration.split(" ");
|
|
2316
2397
|
const code = await setup.ctx.exec.inherit(command, args);
|
|
2317
|
-
|
|
2398
|
+
const hint = code === 0 ? void 0 : `verify: ${result.fix}`;
|
|
2399
|
+
setup.ui.check(code === 0, "trusted publisher registered", hint);
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* The fix of a failing result. A warn or a skip is not something the wizard may act on.
|
|
2403
|
+
*
|
|
2404
|
+
* @param result - The check result.
|
|
2405
|
+
* @returns The fix command when the result is a failure.
|
|
2406
|
+
* @example
|
|
2407
|
+
* failFix({ status: "fail", detail: "…", fix: "npm trust github …" });
|
|
2408
|
+
*/
|
|
2409
|
+
function failFix(result) {
|
|
2410
|
+
return result.status === "fail" ? result.fix : void 0;
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* The registration command for an account behind 2FA. There the listing needs an OTP, so
|
|
2414
|
+
* the check cannot tell "missing" from "registered"; the wizard asks, and npm — with stdio
|
|
2415
|
+
* inherited — prompts for the OTP itself and refuses a duplicate.
|
|
2416
|
+
*
|
|
2417
|
+
* @param setup - The wizard state.
|
|
2418
|
+
* @returns The command to run, or `undefined` when it cannot be built or was declined.
|
|
2419
|
+
* @example
|
|
2420
|
+
* const registration = await registrationBehindOtp(setup);
|
|
2421
|
+
*/
|
|
2422
|
+
async function registrationBehindOtp(setup) {
|
|
2423
|
+
const manifest = await readManifest(setup.ctx.files);
|
|
2424
|
+
const declared = manifest === void 0 ? void 0 : repositoryUrlOf(manifest);
|
|
2425
|
+
const ownerRepo = declared === void 0 ? void 0 : ownerRepoFrom(declared);
|
|
2426
|
+
if (!manifest?.name || ownerRepo === void 0) return void 0;
|
|
2427
|
+
return await setup.prompts.confirm("npm needs an OTP to list trusted publishers. Register publish.yml now?") ? trustCommand(manifest.name, ownerRepo) : void 0;
|
|
2318
2428
|
}
|
|
2319
2429
|
/**
|
|
2320
2430
|
* Apply the PR-only branch ruleset to the default branch. Tags stay unrestricted — the
|
|
@@ -2372,6 +2482,7 @@ async function applyBranchRuleset(setup, ownerRepo) {
|
|
|
2372
2482
|
async function runSetup(options) {
|
|
2373
2483
|
const setup = {
|
|
2374
2484
|
dryRun: false,
|
|
2485
|
+
unattended: false,
|
|
2375
2486
|
...options
|
|
2376
2487
|
};
|
|
2377
2488
|
const { ui } = setup;
|
|
@@ -2387,7 +2498,7 @@ async function runSetup(options) {
|
|
|
2387
2498
|
}
|
|
2388
2499
|
await writeWorkflows(setup);
|
|
2389
2500
|
await normalizeContract(setup, manifest);
|
|
2390
|
-
await firstPublish(setup, manifest.name, manifest.version);
|
|
2501
|
+
const justPublished = await firstPublish(setup, manifest.name, manifest.version);
|
|
2391
2502
|
await pushVersionTag(setup, manifest.version);
|
|
2392
2503
|
await registerTrustedPublisher(setup);
|
|
2393
2504
|
const declared = repositoryUrlOf(manifest);
|
|
@@ -2395,10 +2506,12 @@ async function runSetup(options) {
|
|
|
2395
2506
|
if (ownerRepo === void 0) ui.warn("no GitHub owner/repo — skipping the branch ruleset");
|
|
2396
2507
|
else await applyBranchRuleset(setup, ownerRepo);
|
|
2397
2508
|
ui.heading("Doctor");
|
|
2398
|
-
|
|
2509
|
+
const report = await runDoctor({
|
|
2399
2510
|
ctx: setup.ctx,
|
|
2400
2511
|
ui
|
|
2401
|
-
})
|
|
2512
|
+
});
|
|
2513
|
+
if (justPublished) ui.info("published just now: npm can answer 404 for a few minutes, re-run release:doctor then");
|
|
2514
|
+
return report.failed ? 1 : 0;
|
|
2402
2515
|
}
|
|
2403
2516
|
//#endregion
|
|
2404
2517
|
//#region src/lib/argv.ts
|
|
@@ -2737,7 +2850,8 @@ async function dispatch(parsed, ctx, ui) {
|
|
|
2737
2850
|
ctx,
|
|
2738
2851
|
ui,
|
|
2739
2852
|
prompts: parsed.yes || parsed.dryRun ? ASSENTING_PROMPTS : createBrandPrompts(),
|
|
2740
|
-
dryRun: parsed.dryRun
|
|
2853
|
+
dryRun: parsed.dryRun,
|
|
2854
|
+
unattended: parsed.yes
|
|
2741
2855
|
});
|
|
2742
2856
|
if (parsed.command === "release" && parsed.releaseType !== void 0) return runRelease({
|
|
2743
2857
|
ctx,
|