@moku-labs/ci 1.2.4 → 1.2.6
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 +68 -25
- 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
|
@@ -685,19 +685,48 @@ function requiredCheckContexts(stdout) {
|
|
|
685
685
|
return parseRules(stdout).filter((rule) => rule.type === REQUIRED_CHECKS_RULE).flatMap((rule) => rule.parameters?.required_status_checks ?? []).flatMap((check) => check.context === void 0 ? [] : [check.context]);
|
|
686
686
|
}
|
|
687
687
|
/**
|
|
688
|
+
* What is wrong with a ruleset's required checks, measured against the central ones. A
|
|
689
|
+
* legacy check is a central one without the caller prefix (`lint` for `ci / lint`): the thin
|
|
690
|
+
* caller never reports it. Any other context is the project's own (`ci-pass` of a matrix
|
|
691
|
+
* workflow) and is none of our business.
|
|
692
|
+
*
|
|
693
|
+
* @param required - The contexts the ruleset requires.
|
|
694
|
+
* @param central - The central contexts (`ci / lint`, …).
|
|
695
|
+
* @returns The legacy contexts to drop and the central ones still missing.
|
|
696
|
+
* @example
|
|
697
|
+
* requiredChecksDrift(["lint", "ci-pass"], ["ci / lint"]); // { legacy: ["lint"], missing: ["ci / lint"] }
|
|
698
|
+
*/
|
|
699
|
+
function requiredChecksDrift(required, central) {
|
|
700
|
+
const bare = new Set(central.map((context) => context.slice(5)));
|
|
701
|
+
return {
|
|
702
|
+
legacy: required.filter((context) => bare.has(context)),
|
|
703
|
+
missing: central.filter((context) => !required.includes(context))
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
688
707
|
* The body that moves an existing ruleset onto the central required checks. Every other
|
|
689
|
-
* rule
|
|
708
|
+
* rule is kept as it is, and so is every required check the project added itself.
|
|
690
709
|
*
|
|
691
710
|
* @param existing - The full existing ruleset as JSON.
|
|
692
711
|
* @param template - The central ruleset template as JSON.
|
|
693
|
-
* @returns The `PUT` body: the existing rules with the
|
|
712
|
+
* @returns The `PUT` body: the existing rules with the central required checks merged in.
|
|
694
713
|
* @example
|
|
695
714
|
* const body = withCentralRequiredChecks(existingJson, renderMainRuleset());
|
|
696
715
|
*/
|
|
697
716
|
function withCentralRequiredChecks(existing, template) {
|
|
698
717
|
const kept = parseRules(existing).filter((rule) => rule.type !== REQUIRED_CHECKS_RULE);
|
|
699
718
|
const central = parseRules(template).filter((rule) => rule.type === REQUIRED_CHECKS_RULE);
|
|
700
|
-
|
|
719
|
+
const centralContexts = requiredCheckContexts(template);
|
|
720
|
+
const { legacy } = requiredChecksDrift(requiredCheckContexts(existing), centralContexts);
|
|
721
|
+
const own = requiredCheckContexts(existing).filter((context) => !legacy.includes(context) && !centralContexts.includes(context));
|
|
722
|
+
const merged = central.map((rule) => ({
|
|
723
|
+
...rule,
|
|
724
|
+
parameters: {
|
|
725
|
+
...rule.parameters,
|
|
726
|
+
required_status_checks: [...rule.parameters?.required_status_checks ?? [], ...own.map((context) => ({ context }))]
|
|
727
|
+
}
|
|
728
|
+
}));
|
|
729
|
+
return JSON.stringify({ rules: [...kept, ...merged] });
|
|
701
730
|
}
|
|
702
731
|
/**
|
|
703
732
|
* The id of the newest workflow run from `gh run list --json databaseId`.
|
|
@@ -1179,11 +1208,11 @@ function renderMainRuleset() {
|
|
|
1179
1208
|
* const stale = await findStaleRuleset(ctx, "moku-labs/system", listing.stdout);
|
|
1180
1209
|
*/
|
|
1181
1210
|
async function findStaleRuleset(ctx, ownerRepo, listing) {
|
|
1182
|
-
const central =
|
|
1211
|
+
const central = requiredCheckContexts(renderMainRuleset());
|
|
1183
1212
|
for (const id of activeBranchRulesetIds(listing)) {
|
|
1184
1213
|
const detail = await ctx.exec.capture("gh", ["api", `repos/${ownerRepo}/rulesets/${id}`]);
|
|
1185
1214
|
if (detail.code !== 0) continue;
|
|
1186
|
-
const legacy = requiredCheckContexts(detail.stdout)
|
|
1215
|
+
const { legacy } = requiredChecksDrift(requiredCheckContexts(detail.stdout), central);
|
|
1187
1216
|
if (legacy.length > 0) return {
|
|
1188
1217
|
id,
|
|
1189
1218
|
body: detail.stdout,
|
|
@@ -2217,16 +2246,15 @@ async function isCommittedUnchanged(setup, path) {
|
|
|
2217
2246
|
*
|
|
2218
2247
|
* @param setup - The wizard state.
|
|
2219
2248
|
* @param template - The central workflow about to be written.
|
|
2220
|
-
* @param existing - The current contents of the file.
|
|
2221
2249
|
* @returns `true` when the caller may write the template now.
|
|
2222
2250
|
* @example
|
|
2223
|
-
* if (!(await clearExistingWorkflow(setup, template
|
|
2251
|
+
* if (!(await clearExistingWorkflow(setup, template))) continue;
|
|
2224
2252
|
*/
|
|
2225
|
-
async function clearExistingWorkflow(setup, template
|
|
2226
|
-
const kind = isThinWorkflow(existing, template) ? "differs" : "is a legacy workflow";
|
|
2253
|
+
async function clearExistingWorkflow(setup, template) {
|
|
2227
2254
|
const committed = await isCommittedUnchanged(setup, template.path);
|
|
2228
2255
|
const safety = committed ? "git keeps the original" : "a .bak copy is kept";
|
|
2229
|
-
|
|
2256
|
+
const question = `${template.path} is a legacy workflow. Replace it (${safety})?`;
|
|
2257
|
+
if (!await setup.prompts.confirm(question)) {
|
|
2230
2258
|
setup.ui.check(false, `${template.path} left unchanged`);
|
|
2231
2259
|
return false;
|
|
2232
2260
|
}
|
|
@@ -2252,7 +2280,11 @@ async function writeWorkflows(setup) {
|
|
|
2252
2280
|
setup.ui.check(true, `${template.path} up to date`);
|
|
2253
2281
|
continue;
|
|
2254
2282
|
}
|
|
2255
|
-
if (
|
|
2283
|
+
if (existing !== void 0 && isThinWorkflow(existing, template)) {
|
|
2284
|
+
setup.ui.check(true, `${template.path} is a thin caller with its own inputs, left as is`);
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
if (!(existing === void 0 ? !deferred(setup, `write ${template.path}`) : await clearExistingWorkflow(setup, template))) continue;
|
|
2256
2288
|
await setup.ctx.files.write(template.path, template.content);
|
|
2257
2289
|
setup.ui.check(true, `${template.path} written`);
|
|
2258
2290
|
}
|
|
@@ -2295,10 +2327,10 @@ async function normalizeContract(setup, manifest) {
|
|
|
2295
2327
|
*
|
|
2296
2328
|
* @param setup - The wizard state.
|
|
2297
2329
|
* @param name - The package name.
|
|
2298
|
-
* @param version - The version about to be published.
|
|
2299
|
-
* @returns
|
|
2330
|
+
* @param version - The version about to be published; a released package may keep none.
|
|
2331
|
+
* @returns Where the package stands on npm after this step.
|
|
2300
2332
|
* @example
|
|
2301
|
-
* await firstPublish(setup, "@moku-labs/common", "0.2.0");
|
|
2333
|
+
* const state = await firstPublish(setup, "@moku-labs/common", "0.2.0");
|
|
2302
2334
|
*/
|
|
2303
2335
|
async function firstPublish(setup, name, version) {
|
|
2304
2336
|
setup.ui.heading("First publish");
|
|
@@ -2309,17 +2341,21 @@ async function firstPublish(setup, name, version) {
|
|
|
2309
2341
|
]);
|
|
2310
2342
|
if (view.code === 0) {
|
|
2311
2343
|
setup.ui.check(true, `${name}@${view.stdout.trim()} already on npm`);
|
|
2312
|
-
return
|
|
2344
|
+
return "present";
|
|
2345
|
+
}
|
|
2346
|
+
if (version === void 0) {
|
|
2347
|
+
setup.ui.check(false, `${MANIFEST_PATH} has no \`version\`, a first publish needs one`);
|
|
2348
|
+
return "absent";
|
|
2313
2349
|
}
|
|
2314
2350
|
if (!await setup.prompts.confirm(`Publish ${name}@${version} to npm now?`)) {
|
|
2315
2351
|
setup.ui.check(false, "first publish skipped");
|
|
2316
|
-
return
|
|
2352
|
+
return "absent";
|
|
2317
2353
|
}
|
|
2318
|
-
if (deferred(setup, `run bun run build && npm publish --access public`)) return
|
|
2319
|
-
if (needsOwner(setup, "first publish", "bun run build && npm publish --access public")) return
|
|
2354
|
+
if (deferred(setup, `run bun run build && npm publish --access public`)) return "published";
|
|
2355
|
+
if (needsOwner(setup, "first publish", "bun run build && npm publish --access public")) return "absent";
|
|
2320
2356
|
if (await setup.ctx.exec.inherit("bun", ["run", "build"]) !== 0) {
|
|
2321
2357
|
setup.ui.error("build failed — not publishing");
|
|
2322
|
-
return
|
|
2358
|
+
return "absent";
|
|
2323
2359
|
}
|
|
2324
2360
|
const published = await setup.ctx.exec.inherit("npm", [
|
|
2325
2361
|
"publish",
|
|
@@ -2327,7 +2363,7 @@ async function firstPublish(setup, name, version) {
|
|
|
2327
2363
|
"public"
|
|
2328
2364
|
]);
|
|
2329
2365
|
setup.ui.check(published === 0, `npm publish ${name}@${version}`);
|
|
2330
|
-
return published === 0;
|
|
2366
|
+
return published === 0 ? "published" : "absent";
|
|
2331
2367
|
}
|
|
2332
2368
|
/**
|
|
2333
2369
|
* Tag the published version and push ONLY that tag — the branch is never written from
|
|
@@ -2346,6 +2382,10 @@ async function pushVersionTag(setup, version) {
|
|
|
2346
2382
|
setup.ui.check(true, `release tags exist, latest is ${latest}`);
|
|
2347
2383
|
return;
|
|
2348
2384
|
}
|
|
2385
|
+
if (version === void 0) {
|
|
2386
|
+
setup.ui.check(false, `${MANIFEST_PATH} has no \`version\` and the repo has no release tag`);
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2349
2389
|
const tag = `v${version}`;
|
|
2350
2390
|
if ((await setup.ctx.exec.capture("git", [
|
|
2351
2391
|
"tag",
|
|
@@ -2492,14 +2532,17 @@ async function runSetup(options) {
|
|
|
2492
2532
|
});
|
|
2493
2533
|
if (!await ensurePrerequisites(setup)) return 1;
|
|
2494
2534
|
const manifest = await readManifest(setup.ctx.files);
|
|
2495
|
-
if (!manifest?.name
|
|
2496
|
-
ui.error(`${MANIFEST_PATH} is missing, malformed, or has no name
|
|
2535
|
+
if (!manifest?.name) {
|
|
2536
|
+
ui.error(`${MANIFEST_PATH} is missing, malformed, or has no name`);
|
|
2497
2537
|
return 1;
|
|
2498
2538
|
}
|
|
2499
2539
|
await writeWorkflows(setup);
|
|
2500
2540
|
await normalizeContract(setup, manifest);
|
|
2501
|
-
const
|
|
2502
|
-
|
|
2541
|
+
const published = await firstPublish(setup, manifest.name, manifest.version);
|
|
2542
|
+
if (published === "absent") {
|
|
2543
|
+
ui.heading("Tag");
|
|
2544
|
+
ui.check(false, "no version tag pushed, the package is not on npm yet");
|
|
2545
|
+
} else await pushVersionTag(setup, manifest.version);
|
|
2503
2546
|
await registerTrustedPublisher(setup);
|
|
2504
2547
|
const declared = repositoryUrlOf(manifest);
|
|
2505
2548
|
const ownerRepo = declared === void 0 ? void 0 : ownerRepoFrom(declared);
|
|
@@ -2510,7 +2553,7 @@ async function runSetup(options) {
|
|
|
2510
2553
|
ctx: setup.ctx,
|
|
2511
2554
|
ui
|
|
2512
2555
|
});
|
|
2513
|
-
if (
|
|
2556
|
+
if (published === "published" && !setup.dryRun) ui.info("published just now: npm can answer 404 for a few minutes, re-run release:doctor then");
|
|
2514
2557
|
return report.failed ? 1 : 0;
|
|
2515
2558
|
}
|
|
2516
2559
|
//#endregion
|