@moku-labs/ci 1.2.3 → 1.2.5
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 +27 -2
- package/dist/release.mjs +54 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ everything else lives here, once. Not a build tool and not a framework — it ca
|
|
|
22
22
|
[Install](#install) ·
|
|
23
23
|
[How it works](#how-it-works) ·
|
|
24
24
|
[Workflows](#workflows) ·
|
|
25
|
+
[Project checks](#project-checks) ·
|
|
25
26
|
[PR previews](#pr-previews) ·
|
|
26
27
|
[CLI](#cli) ·
|
|
27
28
|
[The contract](#the-contract) ·
|
|
@@ -94,8 +95,8 @@ flowchart LR
|
|
|
94
95
|
|
|
95
96
|
| Workflow | Called from | Jobs | Inputs (all optional) |
|
|
96
97
|
|---|---|---|---|
|
|
97
|
-
| [`package-ci.yml`](.github/workflows/package-ci.yml) | [`examples/package/ci.yml`](examples/package/ci.yml) | `lint` · `types` · `test` · `build` · `preview` | `runs_on`, `bun_version`, `validate`, `preview` |
|
|
98
|
-
| [`package-release.yml`](.github/workflows/package-release.yml) | [`examples/package/publish.yml`](examples/package/publish.yml) | `check` → `release` → `package` → `publish` | `release_type`, `publish`, `runs_on`, `bun_version`, `node_version`, `artifact_name`, `validate` |
|
|
98
|
+
| [`package-ci.yml`](.github/workflows/package-ci.yml) | [`examples/package/ci.yml`](examples/package/ci.yml) | `lint` · `types` · `test` · `build` · `extra` · `preview` | `runs_on`, `bun_version`, `validate`, `preview`, `extra` |
|
|
99
|
+
| [`package-release.yml`](.github/workflows/package-release.yml) | [`examples/package/publish.yml`](examples/package/publish.yml) | `check` → `release` → `package` → `publish` | `release_type`, `publish`, `runs_on`, `bun_version`, `node_version`, `artifact_name`, `validate`, `extra` |
|
|
99
100
|
| [`app-deploy.yml`](.github/workflows/app-deploy.yml) | [`examples/app/ci.yml`](examples/app/ci.yml) | `validate` → `deploy` to Cloudflare | script names (`lint_script`, `build_script`, `deploy_script`, …) and two required secrets |
|
|
100
101
|
| [`self-test.yml`](.github/workflows/self-test.yml) | this repo only | `actionlint` over workflows and examples | — |
|
|
101
102
|
|
|
@@ -110,6 +111,30 @@ flowchart LR
|
|
|
110
111
|
> A Layer-3 app copies `examples/app/ci.yml` by hand and needs a `deploy` script. The CLI
|
|
111
112
|
> sets up packages only.
|
|
112
113
|
|
|
114
|
+
## Project checks
|
|
115
|
+
|
|
116
|
+
A check the five contract scripts do not cover goes in `extra`: a JSON array of script names.
|
|
117
|
+
Each one is its own job, run after a build, and reported as `ci / extra (check:bundle)`.
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
# .github/workflows/ci.yml
|
|
121
|
+
jobs:
|
|
122
|
+
ci:
|
|
123
|
+
uses: moku-labs/ci/.github/workflows/package-ci.yml@v1
|
|
124
|
+
with:
|
|
125
|
+
extra: '["check:bundle", "test:cli-e2e"]'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
| Rule | Where |
|
|
129
|
+
|---|---|
|
|
130
|
+
| Give `publish.yml` the same `extra`, so a release waits for the same checks. | caller `publish.yml` |
|
|
131
|
+
| `setup` never replaces a thin caller that differs from the template. | `moku-release setup` |
|
|
132
|
+
| An `extra` job is not a required check until the ruleset of that repo names it. | repo ruleset |
|
|
133
|
+
| A check that needs its own matrix or a cron stays a separate workflow file. `doctor` and `setup` only look at `ci.yml` and `publish.yml`. | the project |
|
|
134
|
+
|
|
135
|
+
A package may keep its version in git tags only. `build` and `preview` then seed
|
|
136
|
+
`package.json` from the latest `v*` tag before they pack. Nothing is committed.
|
|
137
|
+
|
|
113
138
|
## PR previews
|
|
114
139
|
|
|
115
140
|
Every pull request commit is published to [pkg.pr.new](https://pkg.pr.new) by the `preview`
|
package/dist/release.mjs
CHANGED
|
@@ -2152,6 +2152,22 @@ function deferred(setup, description) {
|
|
|
2152
2152
|
return true;
|
|
2153
2153
|
}
|
|
2154
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
|
+
/**
|
|
2155
2171
|
* Verify the two human-only prerequisites. Nothing else in the wizard can run without
|
|
2156
2172
|
* them, and neither can be automated — the wizard prints the command and stops.
|
|
2157
2173
|
*
|
|
@@ -2201,16 +2217,15 @@ async function isCommittedUnchanged(setup, path) {
|
|
|
2201
2217
|
*
|
|
2202
2218
|
* @param setup - The wizard state.
|
|
2203
2219
|
* @param template - The central workflow about to be written.
|
|
2204
|
-
* @param existing - The current contents of the file.
|
|
2205
2220
|
* @returns `true` when the caller may write the template now.
|
|
2206
2221
|
* @example
|
|
2207
|
-
* if (!(await clearExistingWorkflow(setup, template
|
|
2222
|
+
* if (!(await clearExistingWorkflow(setup, template))) continue;
|
|
2208
2223
|
*/
|
|
2209
|
-
async function clearExistingWorkflow(setup, template
|
|
2210
|
-
const kind = isThinWorkflow(existing, template) ? "differs" : "is a legacy workflow";
|
|
2224
|
+
async function clearExistingWorkflow(setup, template) {
|
|
2211
2225
|
const committed = await isCommittedUnchanged(setup, template.path);
|
|
2212
2226
|
const safety = committed ? "git keeps the original" : "a .bak copy is kept";
|
|
2213
|
-
|
|
2227
|
+
const question = `${template.path} is a legacy workflow. Replace it (${safety})?`;
|
|
2228
|
+
if (!await setup.prompts.confirm(question)) {
|
|
2214
2229
|
setup.ui.check(false, `${template.path} left unchanged`);
|
|
2215
2230
|
return false;
|
|
2216
2231
|
}
|
|
@@ -2236,7 +2251,11 @@ async function writeWorkflows(setup) {
|
|
|
2236
2251
|
setup.ui.check(true, `${template.path} up to date`);
|
|
2237
2252
|
continue;
|
|
2238
2253
|
}
|
|
2239
|
-
if (
|
|
2254
|
+
if (existing !== void 0 && isThinWorkflow(existing, template)) {
|
|
2255
|
+
setup.ui.check(true, `${template.path} is a thin caller with its own inputs, left as is`);
|
|
2256
|
+
continue;
|
|
2257
|
+
}
|
|
2258
|
+
if (!(existing === void 0 ? !deferred(setup, `write ${template.path}`) : await clearExistingWorkflow(setup, template))) continue;
|
|
2240
2259
|
await setup.ctx.files.write(template.path, template.content);
|
|
2241
2260
|
setup.ui.check(true, `${template.path} written`);
|
|
2242
2261
|
}
|
|
@@ -2279,10 +2298,10 @@ async function normalizeContract(setup, manifest) {
|
|
|
2279
2298
|
*
|
|
2280
2299
|
* @param setup - The wizard state.
|
|
2281
2300
|
* @param name - The package name.
|
|
2282
|
-
* @param version - The version about to be published.
|
|
2283
|
-
* @returns
|
|
2301
|
+
* @param version - The version about to be published; a released package may keep none.
|
|
2302
|
+
* @returns Where the package stands on npm after this step.
|
|
2284
2303
|
* @example
|
|
2285
|
-
* await firstPublish(setup, "@moku-labs/common", "0.2.0");
|
|
2304
|
+
* const state = await firstPublish(setup, "@moku-labs/common", "0.2.0");
|
|
2286
2305
|
*/
|
|
2287
2306
|
async function firstPublish(setup, name, version) {
|
|
2288
2307
|
setup.ui.heading("First publish");
|
|
@@ -2293,16 +2312,21 @@ async function firstPublish(setup, name, version) {
|
|
|
2293
2312
|
]);
|
|
2294
2313
|
if (view.code === 0) {
|
|
2295
2314
|
setup.ui.check(true, `${name}@${view.stdout.trim()} already on npm`);
|
|
2296
|
-
return
|
|
2315
|
+
return "present";
|
|
2316
|
+
}
|
|
2317
|
+
if (version === void 0) {
|
|
2318
|
+
setup.ui.check(false, `${MANIFEST_PATH} has no \`version\`, a first publish needs one`);
|
|
2319
|
+
return "absent";
|
|
2297
2320
|
}
|
|
2298
2321
|
if (!await setup.prompts.confirm(`Publish ${name}@${version} to npm now?`)) {
|
|
2299
2322
|
setup.ui.check(false, "first publish skipped");
|
|
2300
|
-
return
|
|
2323
|
+
return "absent";
|
|
2301
2324
|
}
|
|
2302
|
-
if (deferred(setup, `run bun run build && npm publish --access public`)) return
|
|
2325
|
+
if (deferred(setup, `run bun run build && npm publish --access public`)) return "published";
|
|
2326
|
+
if (needsOwner(setup, "first publish", "bun run build && npm publish --access public")) return "absent";
|
|
2303
2327
|
if (await setup.ctx.exec.inherit("bun", ["run", "build"]) !== 0) {
|
|
2304
2328
|
setup.ui.error("build failed — not publishing");
|
|
2305
|
-
return
|
|
2329
|
+
return "absent";
|
|
2306
2330
|
}
|
|
2307
2331
|
const published = await setup.ctx.exec.inherit("npm", [
|
|
2308
2332
|
"publish",
|
|
@@ -2310,7 +2334,7 @@ async function firstPublish(setup, name, version) {
|
|
|
2310
2334
|
"public"
|
|
2311
2335
|
]);
|
|
2312
2336
|
setup.ui.check(published === 0, `npm publish ${name}@${version}`);
|
|
2313
|
-
return published === 0;
|
|
2337
|
+
return published === 0 ? "published" : "absent";
|
|
2314
2338
|
}
|
|
2315
2339
|
/**
|
|
2316
2340
|
* Tag the published version and push ONLY that tag — the branch is never written from
|
|
@@ -2329,6 +2353,10 @@ async function pushVersionTag(setup, version) {
|
|
|
2329
2353
|
setup.ui.check(true, `release tags exist, latest is ${latest}`);
|
|
2330
2354
|
return;
|
|
2331
2355
|
}
|
|
2356
|
+
if (version === void 0) {
|
|
2357
|
+
setup.ui.check(false, `${MANIFEST_PATH} has no \`version\` and the repo has no release tag`);
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2332
2360
|
const tag = `v${version}`;
|
|
2333
2361
|
if ((await setup.ctx.exec.capture("git", [
|
|
2334
2362
|
"tag",
|
|
@@ -2375,6 +2403,7 @@ async function registerTrustedPublisher(setup) {
|
|
|
2375
2403
|
return;
|
|
2376
2404
|
}
|
|
2377
2405
|
if (deferred(setup, registration)) return;
|
|
2406
|
+
if (needsOwner(setup, "trusted publisher registration", registration)) return;
|
|
2378
2407
|
const [command = "npm", ...args] = registration.split(" ");
|
|
2379
2408
|
const code = await setup.ctx.exec.inherit(command, args);
|
|
2380
2409
|
const hint = code === 0 ? void 0 : `verify: ${result.fix}`;
|
|
@@ -2464,6 +2493,7 @@ async function applyBranchRuleset(setup, ownerRepo) {
|
|
|
2464
2493
|
async function runSetup(options) {
|
|
2465
2494
|
const setup = {
|
|
2466
2495
|
dryRun: false,
|
|
2496
|
+
unattended: false,
|
|
2467
2497
|
...options
|
|
2468
2498
|
};
|
|
2469
2499
|
const { ui } = setup;
|
|
@@ -2473,14 +2503,17 @@ async function runSetup(options) {
|
|
|
2473
2503
|
});
|
|
2474
2504
|
if (!await ensurePrerequisites(setup)) return 1;
|
|
2475
2505
|
const manifest = await readManifest(setup.ctx.files);
|
|
2476
|
-
if (!manifest?.name
|
|
2477
|
-
ui.error(`${MANIFEST_PATH} is missing, malformed, or has no name
|
|
2506
|
+
if (!manifest?.name) {
|
|
2507
|
+
ui.error(`${MANIFEST_PATH} is missing, malformed, or has no name`);
|
|
2478
2508
|
return 1;
|
|
2479
2509
|
}
|
|
2480
2510
|
await writeWorkflows(setup);
|
|
2481
2511
|
await normalizeContract(setup, manifest);
|
|
2482
|
-
const
|
|
2483
|
-
|
|
2512
|
+
const published = await firstPublish(setup, manifest.name, manifest.version);
|
|
2513
|
+
if (published === "absent") {
|
|
2514
|
+
ui.heading("Tag");
|
|
2515
|
+
ui.check(false, "no version tag pushed, the package is not on npm yet");
|
|
2516
|
+
} else await pushVersionTag(setup, manifest.version);
|
|
2484
2517
|
await registerTrustedPublisher(setup);
|
|
2485
2518
|
const declared = repositoryUrlOf(manifest);
|
|
2486
2519
|
const ownerRepo = declared === void 0 ? void 0 : ownerRepoFrom(declared);
|
|
@@ -2491,7 +2524,7 @@ async function runSetup(options) {
|
|
|
2491
2524
|
ctx: setup.ctx,
|
|
2492
2525
|
ui
|
|
2493
2526
|
});
|
|
2494
|
-
if (
|
|
2527
|
+
if (published === "published" && !setup.dryRun) ui.info("published just now: npm can answer 404 for a few minutes, re-run release:doctor then");
|
|
2495
2528
|
return report.failed ? 1 : 0;
|
|
2496
2529
|
}
|
|
2497
2530
|
//#endregion
|
|
@@ -2831,7 +2864,8 @@ async function dispatch(parsed, ctx, ui) {
|
|
|
2831
2864
|
ctx,
|
|
2832
2865
|
ui,
|
|
2833
2866
|
prompts: parsed.yes || parsed.dryRun ? ASSENTING_PROMPTS : createBrandPrompts(),
|
|
2834
|
-
dryRun: parsed.dryRun
|
|
2867
|
+
dryRun: parsed.dryRun,
|
|
2868
|
+
unattended: parsed.yes
|
|
2835
2869
|
});
|
|
2836
2870
|
if (parsed.command === "release" && parsed.releaseType !== void 0) return runRelease({
|
|
2837
2871
|
ctx,
|