@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 +19 -7
- package/dist/release.mjs +47 -30
- package/package.json +1 -1
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
|
|
47
|
-
>
|
|
48
|
-
>
|
|
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 |
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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 =
|
|
1698
|
-
/** Gap between registry polls —
|
|
1699
|
-
const REGISTRY_POLL_INTERVAL_MS =
|
|
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 =
|
|
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
|
|
1762
|
-
|
|
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
|
|
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);
|