@moku-labs/ci 1.2.2 → 1.2.3

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.
Files changed (2) hide show
  1. package/dist/release.mjs +107 -13
  2. 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 text = `${JSON.stringify(manifest, void 0, 2)}\n`;
803
- if (source === void 0 || !isAsciiOnly(source)) return text;
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("npm asks for an OTP, cannot verify from here", `npm trust list ${manifest.name}`);
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
  }
@@ -2222,7 +2280,7 @@ async function normalizeContract(setup, manifest) {
2222
2280
  * @param setup - The wizard state.
2223
2281
  * @param name - The package name.
2224
2282
  * @param version - The version about to be published.
2225
- * @returns `true` when the package exists on npm after this step.
2283
+ * @returns `true` when this run performed the first publish.
2226
2284
  * @example
2227
2285
  * await firstPublish(setup, "@moku-labs/common", "0.2.0");
2228
2286
  */
@@ -2235,7 +2293,7 @@ async function firstPublish(setup, name, version) {
2235
2293
  ]);
2236
2294
  if (view.code === 0) {
2237
2295
  setup.ui.check(true, `${name}@${view.stdout.trim()} already on npm`);
2238
- return true;
2296
+ return false;
2239
2297
  }
2240
2298
  if (!await setup.prompts.confirm(`Publish ${name}@${version} to npm now?`)) {
2241
2299
  setup.ui.check(false, "first publish skipped");
@@ -2307,14 +2365,48 @@ async function pushVersionTag(setup, version) {
2307
2365
  async function registerTrustedPublisher(setup) {
2308
2366
  setup.ui.heading("Trusted publisher");
2309
2367
  const result = await trustedPublisherCheck.run(setup.ctx);
2310
- if (result.status !== "fail" || result.fix === void 0) {
2311
- setup.ui.check(result.status === "pass", `${result.detail}`, result.fix);
2368
+ if (result.status === "pass") {
2369
+ setup.ui.check(true, result.detail);
2370
+ return;
2371
+ }
2372
+ const registration = result.detail === "npm asks for an OTP, cannot verify from here" ? await registrationBehindOtp(setup) : failFix(result);
2373
+ if (registration === void 0) {
2374
+ setup.ui.check(false, result.detail, result.fix);
2312
2375
  return;
2313
2376
  }
2314
- if (deferred(setup, result.fix)) return;
2315
- const [command = "npm", ...args] = result.fix.split(" ");
2377
+ if (deferred(setup, registration)) return;
2378
+ const [command = "npm", ...args] = registration.split(" ");
2316
2379
  const code = await setup.ctx.exec.inherit(command, args);
2317
- setup.ui.check(code === 0, "trusted publisher registered");
2380
+ const hint = code === 0 ? void 0 : `verify: ${result.fix}`;
2381
+ setup.ui.check(code === 0, "trusted publisher registered", hint);
2382
+ }
2383
+ /**
2384
+ * The fix of a failing result. A warn or a skip is not something the wizard may act on.
2385
+ *
2386
+ * @param result - The check result.
2387
+ * @returns The fix command when the result is a failure.
2388
+ * @example
2389
+ * failFix({ status: "fail", detail: "…", fix: "npm trust github …" });
2390
+ */
2391
+ function failFix(result) {
2392
+ return result.status === "fail" ? result.fix : void 0;
2393
+ }
2394
+ /**
2395
+ * The registration command for an account behind 2FA. There the listing needs an OTP, so
2396
+ * the check cannot tell "missing" from "registered"; the wizard asks, and npm — with stdio
2397
+ * inherited — prompts for the OTP itself and refuses a duplicate.
2398
+ *
2399
+ * @param setup - The wizard state.
2400
+ * @returns The command to run, or `undefined` when it cannot be built or was declined.
2401
+ * @example
2402
+ * const registration = await registrationBehindOtp(setup);
2403
+ */
2404
+ async function registrationBehindOtp(setup) {
2405
+ const manifest = await readManifest(setup.ctx.files);
2406
+ const declared = manifest === void 0 ? void 0 : repositoryUrlOf(manifest);
2407
+ const ownerRepo = declared === void 0 ? void 0 : ownerRepoFrom(declared);
2408
+ if (!manifest?.name || ownerRepo === void 0) return void 0;
2409
+ return await setup.prompts.confirm("npm needs an OTP to list trusted publishers. Register publish.yml now?") ? trustCommand(manifest.name, ownerRepo) : void 0;
2318
2410
  }
2319
2411
  /**
2320
2412
  * Apply the PR-only branch ruleset to the default branch. Tags stay unrestricted — the
@@ -2387,7 +2479,7 @@ async function runSetup(options) {
2387
2479
  }
2388
2480
  await writeWorkflows(setup);
2389
2481
  await normalizeContract(setup, manifest);
2390
- await firstPublish(setup, manifest.name, manifest.version);
2482
+ const justPublished = await firstPublish(setup, manifest.name, manifest.version);
2391
2483
  await pushVersionTag(setup, manifest.version);
2392
2484
  await registerTrustedPublisher(setup);
2393
2485
  const declared = repositoryUrlOf(manifest);
@@ -2395,10 +2487,12 @@ async function runSetup(options) {
2395
2487
  if (ownerRepo === void 0) ui.warn("no GitHub owner/repo — skipping the branch ruleset");
2396
2488
  else await applyBranchRuleset(setup, ownerRepo);
2397
2489
  ui.heading("Doctor");
2398
- return (await runDoctor({
2490
+ const report = await runDoctor({
2399
2491
  ctx: setup.ctx,
2400
2492
  ui
2401
- })).failed ? 1 : 0;
2493
+ });
2494
+ if (justPublished) ui.info("published just now: npm can answer 404 for a few minutes, re-run release:doctor then");
2495
+ return report.failed ? 1 : 0;
2402
2496
  }
2403
2497
  //#endregion
2404
2498
  //#region src/lib/argv.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moku-labs/ci",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Central CI and release for the moku family: reusable workflows, caller examples and the moku-release CLI.",
5
5
  "type": "module",
6
6
  "sideEffects": [