@moku-labs/ci 1.0.0 → 1.1.2
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 +18 -6
- package/dist/release.mjs +53 -34
- package/package.json +12 -3
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`
|
|
@@ -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
|
@@ -1694,11 +1694,11 @@ async function runDoctor(options) {
|
|
|
1694
1694
|
/** Workflow file dispatched by name — the same name npm's trusted publisher is bound to. */
|
|
1695
1695
|
const PUBLISH_WORKFLOW_FILE = ".github/workflows/publish.yml".split("/").pop() ?? "publish.yml";
|
|
1696
1696
|
/** 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 =
|
|
1697
|
+
const REGISTRY_POLL_ATTEMPTS = 60;
|
|
1698
|
+
/** Gap between registry polls — 60 × 10s = ten minutes. The first live release needed three. */
|
|
1699
|
+
const REGISTRY_POLL_INTERVAL_MS = 1e4;
|
|
1700
1700
|
/** How many times to look for the dispatched run before concluding it never started. */
|
|
1701
|
-
const RUN_LOOKUP_ATTEMPTS =
|
|
1701
|
+
const RUN_LOOKUP_ATTEMPTS = 20;
|
|
1702
1702
|
/** Gap between run lookups — GitHub takes a moment to materialize a dispatched run. */
|
|
1703
1703
|
const RUN_LOOKUP_INTERVAL_MS = 3e3;
|
|
1704
1704
|
/**
|
|
@@ -1735,32 +1735,48 @@ async function preflight(ctx, ui) {
|
|
|
1735
1735
|
if (report.failed) ui.error("preflight failed — nothing was dispatched");
|
|
1736
1736
|
return !report.failed;
|
|
1737
1737
|
}
|
|
1738
|
+
/** The `gh run list` call that names the newest `publish.yml` run on main. */
|
|
1739
|
+
const LATEST_RUN_ARGS = [
|
|
1740
|
+
"run",
|
|
1741
|
+
"list",
|
|
1742
|
+
"--workflow",
|
|
1743
|
+
PUBLISH_WORKFLOW_FILE,
|
|
1744
|
+
"--branch",
|
|
1745
|
+
"main",
|
|
1746
|
+
"--limit",
|
|
1747
|
+
"1",
|
|
1748
|
+
"--json",
|
|
1749
|
+
"databaseId"
|
|
1750
|
+
];
|
|
1751
|
+
/**
|
|
1752
|
+
* The newest `publish.yml` run right now. Read BEFORE the dispatch, it is the run the
|
|
1753
|
+
* dispatch must not be confused with.
|
|
1754
|
+
*
|
|
1755
|
+
* @param ctx - The ports and flags.
|
|
1756
|
+
* @returns The run id, or `undefined` when the workflow never ran.
|
|
1757
|
+
* @example
|
|
1758
|
+
* const previous = await newestRun(ctx);
|
|
1759
|
+
*/
|
|
1760
|
+
async function newestRun(ctx) {
|
|
1761
|
+
const listing = await ctx.exec.capture("gh", LATEST_RUN_ARGS);
|
|
1762
|
+
return listing.code === 0 ? latestRunId(listing.stdout) : void 0;
|
|
1763
|
+
}
|
|
1738
1764
|
/**
|
|
1739
|
-
* Find the run the dispatch just created, retrying while GitHub materializes it.
|
|
1765
|
+
* Find the run the dispatch just created, retrying while GitHub materializes it. The
|
|
1766
|
+
* newest run is only ours once it differs from `previous`: asked too early, GitHub still
|
|
1767
|
+
* answers with the run of the release before.
|
|
1740
1768
|
*
|
|
1741
1769
|
* @param ctx - The ports and flags.
|
|
1770
|
+
* @param previous - The newest run id from before the dispatch.
|
|
1742
1771
|
* @param sleep - The delay helper.
|
|
1743
|
-
* @returns The run id, or `undefined` when no run appeared.
|
|
1772
|
+
* @returns The run id, or `undefined` when no new run appeared.
|
|
1744
1773
|
* @example
|
|
1745
|
-
* const runId = await findDispatchedRun(ctx, defaultSleep);
|
|
1774
|
+
* const runId = await findDispatchedRun(ctx, previous, defaultSleep);
|
|
1746
1775
|
*/
|
|
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
|
-
];
|
|
1776
|
+
async function findDispatchedRun(ctx, previous, sleep) {
|
|
1760
1777
|
for (let attempt = 0; attempt < RUN_LOOKUP_ATTEMPTS; attempt += 1) {
|
|
1761
|
-
const
|
|
1762
|
-
|
|
1763
|
-
if (runId !== void 0) return runId;
|
|
1778
|
+
const runId = await newestRun(ctx);
|
|
1779
|
+
if (runId !== void 0 && runId !== previous) return runId;
|
|
1764
1780
|
await sleep(RUN_LOOKUP_INTERVAL_MS);
|
|
1765
1781
|
}
|
|
1766
1782
|
}
|
|
@@ -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);
|
|
@@ -2261,21 +2278,23 @@ function parseArgv(argv) {
|
|
|
2261
2278
|
/** Exit code POSIX shells use for "command not found" — what a missing binary reports. */
|
|
2262
2279
|
const COMMAND_NOT_FOUND = 127;
|
|
2263
2280
|
/**
|
|
2264
|
-
* Normalize
|
|
2265
|
-
*
|
|
2266
|
-
* and no
|
|
2281
|
+
* Normalize a failed `execFile` call into a {@link CommandOutput}. A non-zero exit carries
|
|
2282
|
+
* `code` and both streams; a missing binary carries an `ENOENT`-style string code, no exit
|
|
2283
|
+
* status and no output, so the error text stands in for stderr.
|
|
2267
2284
|
*
|
|
2268
|
-
* @param error - The
|
|
2285
|
+
* @param error - The error `execFile` passed to its callback.
|
|
2286
|
+
* @param stdout - What the child wrote to stdout before it failed.
|
|
2287
|
+
* @param stderr - What the child wrote to stderr before it failed.
|
|
2269
2288
|
* @returns The equivalent captured output.
|
|
2270
2289
|
* @example
|
|
2271
|
-
* fromExecError({ code: 1,
|
|
2290
|
+
* fromExecError({ code: 1 }, "", "not logged in");
|
|
2272
2291
|
*/
|
|
2273
|
-
function fromExecError(error) {
|
|
2292
|
+
function fromExecError(error, stdout, stderr) {
|
|
2274
2293
|
const shape = error;
|
|
2275
2294
|
return {
|
|
2276
2295
|
code: typeof shape.code === "number" ? shape.code : COMMAND_NOT_FOUND,
|
|
2277
|
-
stdout
|
|
2278
|
-
stderr:
|
|
2296
|
+
stdout,
|
|
2297
|
+
stderr: stderr === "" && stdout === "" ? String(error) : stderr
|
|
2279
2298
|
};
|
|
2280
2299
|
}
|
|
2281
2300
|
/**
|
|
@@ -2300,7 +2319,7 @@ function createExecutor(cwd) {
|
|
|
2300
2319
|
*/
|
|
2301
2320
|
const capture = (command, args, options = {}) => new Promise((resolve) => {
|
|
2302
2321
|
const child = execFile(command, [...args], { cwd: options.cwd ?? cwd }, (error, stdout, stderr) => {
|
|
2303
|
-
if (error) return resolve(fromExecError(error));
|
|
2322
|
+
if (error) return resolve(fromExecError(error, stdout, stderr));
|
|
2304
2323
|
resolve({
|
|
2305
2324
|
code: 0,
|
|
2306
2325
|
stdout,
|
package/package.json
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moku-labs/ci",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
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
|
-
"sideEffects": [
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"./dist/release.mjs",
|
|
8
|
+
"./src/index.ts"
|
|
9
|
+
],
|
|
7
10
|
"bin": {
|
|
8
11
|
"moku-release": "./dist/release.mjs"
|
|
9
12
|
},
|
|
10
|
-
"files": [
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"examples",
|
|
16
|
+
"rulesets",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
11
20
|
"engines": {
|
|
12
21
|
"node": ">=24.0.0",
|
|
13
22
|
"bun": ">=1.3.14"
|