@moku-labs/ci 1.1.1 → 1.1.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.
package/README.md CHANGED
@@ -43,9 +43,9 @@ registers the trusted publisher and applies the branch ruleset. Run it again any
43
43
  only does what is still missing.
44
44
 
45
45
  > [!NOTE]
46
- > **Status: `1.x`, early.** `package-ci.yml` runs on this repo's own PRs. `package-release.yml`
47
- > and the CLI's `setup` and `release` have passed dry-run and unit tests only; they have not
48
- > yet released a live package.
46
+ > **Status: `1.x`, early.** `package-ci.yml`, `package-release.yml` and `moku-release release`
47
+ > ship this package itself: `1.1.1` was the first live release, tokenless and with provenance.
48
+ > `moku-release setup` has passed dry-run and unit tests only.
49
49
 
50
50
  > [!IMPORTANT]
51
51
  > Two steps are yours alone. The CLI never handles a credential, and there is no `NPM_TOKEN`
@@ -114,7 +114,7 @@ flowchart LR
114
114
  | Command | When | What it does |
115
115
  |---|---|---|
116
116
  | `bun run release:setup` | once per project | Idempotent wizard: workflows, script contract, first publish, first tag, trusted publisher, branch ruleset, then `doctor`. `--dry-run` prints every action and changes nothing. |
117
- | `bun run release:doctor` | any time | Read-only. Eleven checks, one line each, and the exact `fix:` command for every red line. `--json` for machines. |
117
+ | `bun run release:doctor` | any time | Changes nothing in the project; it only runs `git fetch --tags` first. Eleven checks, one line each, and the exact `fix:` command for every red line. `--json` for machines. |
118
118
  | `bun run release <patch\|minor\|major\|prerelease>` | each release | Refuses unless the tree is clean and `HEAD == origin/main`. Dispatches `publish.yml`, watches the run, verifies the version and dist-tag on npm. |
119
119
 
120
120
  The scripts are plain aliases of the `moku-release` bin. Internals and the list of checks:
@@ -152,14 +152,26 @@ YAML stays the same for everyone.
152
152
  | `@v1.x.y` | Immutable tag on every change. Pin it to freeze a project. |
153
153
  | `@v2` | Any breaking change: a removed input, a changed default, a renamed job. `v1` stays where it was. |
154
154
 
155
+ > [!IMPORTANT]
156
+ > `v1` must be a **lightweight** tag. `package-release.yml` calls `./.github/workflows/package-ci.yml`
157
+ > from inside itself, and GitHub cannot resolve that relative call through an annotated tag: every
158
+ > release dies with `startup_failure` and "workflow was not found". Immutable `v1.x.y` tags may be
159
+ > annotated.
160
+ >
161
+ > ```sh
162
+ > git tag -a v1.2.0 -m "v1.2.0"
163
+ > git tag -f v1 v1.2.0^{commit} # no -a, no -m
164
+ > git push origin v1.2.0 && git push -f origin v1
165
+ > ```
166
+
155
167
  A change here runs in every moku repo with `contents: write` and `id-token: write`. Review it
156
168
  like release engineering, not like config.
157
169
 
158
170
  ## When a release fails
159
171
 
160
- One risk is still open: npm may reject a publish that runs inside a workflow owned by another
161
- repository. It is unverified until the first live release. The fallback and sixteen other
162
- traps the workflows already handle are in [docs/release-notes.md](docs/release-notes.md).
172
+ The cross-repo OIDC publish was the open risk; npm accepted it on the first live release
173
+ (`@moku-labs/ci@1.1.1`). The fallback, should npm ever change that, and sixteen traps the
174
+ workflows already handle are in [docs/release-notes.md](docs/release-notes.md).
163
175
 
164
176
  ## Scripts
165
177
 
package/dist/release.mjs CHANGED
@@ -1671,6 +1671,11 @@ async function evaluate(check, ctx) {
1671
1671
  async function runDoctor(options) {
1672
1672
  const { ctx, ui, json = false, checks = allChecks } = options;
1673
1673
  const entries = [];
1674
+ await ctx.exec.capture("git", [
1675
+ "fetch",
1676
+ "--tags",
1677
+ "--prune"
1678
+ ]);
1674
1679
  for (const check of checks) entries.push(await evaluate(check, ctx));
1675
1680
  const failed = entries.some((entry) => entry.status === "fail");
1676
1681
  if (json) {
@@ -1694,11 +1699,11 @@ async function runDoctor(options) {
1694
1699
  /** Workflow file dispatched by name — the same name npm's trusted publisher is bound to. */
1695
1700
  const PUBLISH_WORKFLOW_FILE = ".github/workflows/publish.yml".split("/").pop() ?? "publish.yml";
1696
1701
  /** How long to keep asking the registry for the new version before giving up. */
1697
- const REGISTRY_POLL_ATTEMPTS = 24;
1698
- /** Gap between registry polls — 24 × 5s two minutes of registry lag tolerated. */
1699
- const REGISTRY_POLL_INTERVAL_MS = 5e3;
1702
+ const REGISTRY_POLL_ATTEMPTS = 60;
1703
+ /** Gap between registry polls — 60 × 10s = ten minutes. The first live release needed three. */
1704
+ const REGISTRY_POLL_INTERVAL_MS = 1e4;
1700
1705
  /** How many times to look for the dispatched run before concluding it never started. */
1701
- const RUN_LOOKUP_ATTEMPTS = 10;
1706
+ const RUN_LOOKUP_ATTEMPTS = 20;
1702
1707
  /** Gap between run lookups — GitHub takes a moment to materialize a dispatched run. */
1703
1708
  const RUN_LOOKUP_INTERVAL_MS = 3e3;
1704
1709
  /**
@@ -1735,32 +1740,48 @@ async function preflight(ctx, ui) {
1735
1740
  if (report.failed) ui.error("preflight failed — nothing was dispatched");
1736
1741
  return !report.failed;
1737
1742
  }
1743
+ /** The `gh run list` call that names the newest `publish.yml` run on main. */
1744
+ const LATEST_RUN_ARGS = [
1745
+ "run",
1746
+ "list",
1747
+ "--workflow",
1748
+ PUBLISH_WORKFLOW_FILE,
1749
+ "--branch",
1750
+ "main",
1751
+ "--limit",
1752
+ "1",
1753
+ "--json",
1754
+ "databaseId"
1755
+ ];
1756
+ /**
1757
+ * The newest `publish.yml` run right now. Read BEFORE the dispatch, it is the run the
1758
+ * dispatch must not be confused with.
1759
+ *
1760
+ * @param ctx - The ports and flags.
1761
+ * @returns The run id, or `undefined` when the workflow never ran.
1762
+ * @example
1763
+ * const previous = await newestRun(ctx);
1764
+ */
1765
+ async function newestRun(ctx) {
1766
+ const listing = await ctx.exec.capture("gh", LATEST_RUN_ARGS);
1767
+ return listing.code === 0 ? latestRunId(listing.stdout) : void 0;
1768
+ }
1738
1769
  /**
1739
- * Find the run the dispatch just created, retrying while GitHub materializes it.
1770
+ * Find the run the dispatch just created, retrying while GitHub materializes it. The
1771
+ * newest run is only ours once it differs from `previous`: asked too early, GitHub still
1772
+ * answers with the run of the release before.
1740
1773
  *
1741
1774
  * @param ctx - The ports and flags.
1775
+ * @param previous - The newest run id from before the dispatch.
1742
1776
  * @param sleep - The delay helper.
1743
- * @returns The run id, or `undefined` when no run appeared.
1777
+ * @returns The run id, or `undefined` when no new run appeared.
1744
1778
  * @example
1745
- * const runId = await findDispatchedRun(ctx, defaultSleep);
1779
+ * const runId = await findDispatchedRun(ctx, previous, defaultSleep);
1746
1780
  */
1747
- async function findDispatchedRun(ctx, sleep) {
1748
- const args = [
1749
- "run",
1750
- "list",
1751
- "--workflow",
1752
- PUBLISH_WORKFLOW_FILE,
1753
- "--branch",
1754
- "main",
1755
- "--limit",
1756
- "1",
1757
- "--json",
1758
- "databaseId"
1759
- ];
1781
+ async function findDispatchedRun(ctx, previous, sleep) {
1760
1782
  for (let attempt = 0; attempt < RUN_LOOKUP_ATTEMPTS; attempt += 1) {
1761
- const listing = await ctx.exec.capture("gh", args);
1762
- const runId = listing.code === 0 ? latestRunId(listing.stdout) : void 0;
1763
- if (runId !== void 0) return runId;
1783
+ const runId = await newestRun(ctx);
1784
+ if (runId !== void 0 && runId !== previous) return runId;
1764
1785
  await sleep(RUN_LOOKUP_INTERVAL_MS);
1765
1786
  }
1766
1787
  }
@@ -1832,11 +1853,6 @@ async function runRelease(options) {
1832
1853
  ui.error("package.json is missing, malformed, or has no `name`");
1833
1854
  return 1;
1834
1855
  }
1835
- await ctx.exec.capture("git", [
1836
- "fetch",
1837
- "--tags",
1838
- "--prune"
1839
- ]);
1840
1856
  if (!await preflight(ctx, ui)) return 1;
1841
1857
  const declared = repositoryUrlOf(manifest);
1842
1858
  const ownerRepo = declared === void 0 ? void 0 : ownerRepoFrom(declared);
@@ -1854,6 +1870,7 @@ async function runRelease(options) {
1854
1870
  return 0;
1855
1871
  }
1856
1872
  ui.heading("Dispatch");
1873
+ const previousRun = await newestRun(ctx);
1857
1874
  const dispatched = await ctx.exec.capture("gh", [
1858
1875
  "workflow",
1859
1876
  "run",
@@ -1868,7 +1885,7 @@ async function runRelease(options) {
1868
1885
  return 1;
1869
1886
  }
1870
1887
  ui.check(true, `${PUBLISH_WORKFLOW_FILE} dispatched (${releaseType})`);
1871
- const runId = await findDispatchedRun(ctx, sleep);
1888
+ const runId = await findDispatchedRun(ctx, previousRun, sleep);
1872
1889
  if (runId === void 0) {
1873
1890
  ui.error("the dispatched run never appeared — check GitHub Actions");
1874
1891
  return 1;
@@ -1886,7 +1903,7 @@ async function runRelease(options) {
1886
1903
  ui.heading("Registry");
1887
1904
  const version = await awaitPublishedVersion(ctx, manifest.name, tag, before, sleep);
1888
1905
  if (version === void 0) {
1889
- ui.error(`npm dist-tag \`${tag}\` did not move — the run passed but nothing was published`);
1906
+ ui.error(`npm dist-tag \`${tag}\` did not move in ten minutes — the run passed, so check \`npm view ${manifest.name} dist-tags\` before releasing again`);
1890
1907
  return 1;
1891
1908
  }
1892
1909
  renderSummary(ui, manifest.name, version, ownerRepo);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moku-labs/ci",
3
- "version": "1.1.1",
3
+ "version": "1.1.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": [