@moku-labs/ci 1.2.6 → 1.3.0

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
@@ -40,14 +40,16 @@ bun add -d @moku-labs/ci
40
40
  bun run release:setup
41
41
  ```
42
42
 
43
- `setup` writes the two workflow files, adds the missing scripts, does the first publish,
44
- registers the trusted publisher and applies the branch ruleset. Run it again any time — it
45
- only does what is still missing.
43
+ `setup` writes the two workflow files and the Dependabot files, adds the missing scripts,
44
+ does the first publish, registers the trusted publisher, applies the branch ruleset and
45
+ allows auto-merge. Run it again any time — it only does what is still missing.
46
46
 
47
47
  > [!NOTE]
48
- > **Status: `1.x`, early.** `package-ci.yml`, `package-release.yml` and `moku-release release`
49
- > ship this package itself: `1.1.1` was the first live release, tokenless and with provenance.
50
- > `moku-release setup` has passed dry-run and unit tests only.
48
+ > **Status: `1.x`, in use.** All eight moku packages — `core`, `common`, `web`, `worker`, `room`,
49
+ > `system`, `ai`, `native` — run `package-ci.yml` and `package-release.yml` from `@v1`, and each
50
+ > has shipped a tokenless release with provenance through them. `moku-release setup` did every
51
+ > one of those migrations live, including two first publishes behind 2FA.
52
+ > `app-deploy.yml` has no live user yet.
51
53
 
52
54
  > [!IMPORTANT]
53
55
  > Two steps are yours alone. The CLI never handles a credential, and there is no `NPM_TOKEN`
@@ -60,7 +62,7 @@ only does what is still missing.
60
62
 
61
63
  ## Why @moku-labs/ci
62
64
 
63
- - **One copy of the pipeline.** Seven repos had seven different `ci.yml` and six different
65
+ - **One copy of the pipeline.** Eight packages had eight different `ci.yml` and eight different
64
66
  `publish.yml`. A fix now lands here and reaches every project through the `@v1` tag.
65
67
  - **Scripts are the interface, not tools.** The workflows call `bun run lint`, never `biome`.
66
68
  A project with special needs changes its own script, not the central YAML.
@@ -106,10 +108,13 @@ flowchart LR
106
108
  |---|---|
107
109
  | [`examples/package/publish.local-publish.yml`](examples/package/publish.local-publish.yml) | Fallback: publish from the project's own job. Use it only if the central publish fails npm auth. |
108
110
  | [`rulesets/main.json`](rulesets/main.json) | Branch ruleset for `main`: PR only, no force-push, the four `ci / …` checks required. |
111
+ | [`examples/dependabot.yml`](examples/dependabot.yml) | Dependabot config: one grouped PR a day for `@moku-labs/*` only. `setup` writes it to `.github/dependabot.yml`. |
112
+ | [`examples/package/dependabot-automerge.yml`](examples/package/dependabot-automerge.yml) | Turns on auto-merge for a Dependabot PR; the ruleset checks stay the gate. `setup` writes it and allows auto-merge on the repo. |
109
113
 
110
114
  > [!TIP]
111
- > A Layer-3 app copies `examples/app/ci.yml` by hand and needs a `deploy` script. The CLI
112
- > sets up packages only.
115
+ > A Layer-3 app copies `examples/app/ci.yml` and `examples/dependabot.yml` by hand and needs
116
+ > a `deploy` script. An app merges Dependabot PRs by hand, because the merge runs the deploy.
117
+ > The CLI sets up packages only.
113
118
 
114
119
  ## Project checks
115
120
 
@@ -156,7 +161,7 @@ bun add https://pkg.pr.new/@moku-labs/core@42 # 42 = PR number, a commit sha w
156
161
 
157
162
  | Command | When | What it does |
158
163
  |---|---|---|
159
- | `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. |
164
+ | `bun run release:setup` | once per project | Idempotent wizard: workflows, Dependabot, script contract, first publish, first tag, trusted publisher, branch ruleset, auto-merge, then `doctor`. `--dry-run` prints every action, changes nothing and asks nothing. `--yes` answers every confirmation for an agent or CI; a step that needs your npm OTP is then printed, not run. |
160
165
  | `bun run release:doctor` | any time | Changes nothing in the project; it only runs `git fetch --tags` first. Twelve checks, one line each, and the exact `fix:` command for every red line. `--json` for machines. |
161
166
  | `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. |
162
167
 
package/dist/release.mjs CHANGED
@@ -1109,6 +1109,25 @@ const CI_WORKFLOW_REF = "moku-labs/ci/.github/workflows/package-ci.yml@v1";
1109
1109
  /** The `ci.yml` body, read from `examples/package/ci.yml`. */
1110
1110
  const CI_WORKFLOW = readCentral("examples/package/ci.yml");
1111
1111
  //#endregion
1112
+ //#region src/templates/dependabot.ts
1113
+ /**
1114
+ * @file `moku-release` — the Dependabot templates.
1115
+ *
1116
+ * Read from this package's own `examples/dependabot.yml` and
1117
+ * `examples/package/dependabot-automerge.yml`. Dependabot opens one grouped PR a day when a
1118
+ * new `@moku-labs/*` version is out; the automerge workflow merges it once the required
1119
+ * `ci / …` checks pass. The config sits outside `examples/package/` on purpose: it is not a
1120
+ * workflow, and the self-test runs actionlint on every `.yml` one folder below `examples/`.
1121
+ */
1122
+ /** Repo-relative path the Dependabot config is written to. */
1123
+ const DEPENDABOT_CONFIG_PATH = ".github/dependabot.yml";
1124
+ /** The Dependabot config body, read from `examples/dependabot.yml`. */
1125
+ const DEPENDABOT_CONFIG = readCentral("examples/dependabot.yml");
1126
+ /** Repo-relative path the automerge workflow is written to. */
1127
+ const DEPENDABOT_AUTOMERGE_PATH = ".github/workflows/dependabot-automerge.yml";
1128
+ /** The automerge workflow body, read from `examples/package/dependabot-automerge.yml`. */
1129
+ const DEPENDABOT_AUTOMERGE = readCentral("examples/package/dependabot-automerge.yml");
1130
+ //#endregion
1112
1131
  //#region src/templates/publish.ts
1113
1132
  /**
1114
1133
  * @file `moku-release` — the `.github/workflows/publish.yml` template.
@@ -1163,6 +1182,14 @@ const workflowTemplates = [{
1163
1182
  content: PUBLISH_WORKFLOW,
1164
1183
  ref: PUBLISH_WORKFLOW_REF
1165
1184
  }];
1185
+ /** The Dependabot config and its automerge workflow, in the order `setup` writes them. */
1186
+ const dependabotTemplates = [{
1187
+ path: DEPENDABOT_CONFIG_PATH,
1188
+ content: DEPENDABOT_CONFIG
1189
+ }, {
1190
+ path: DEPENDABOT_AUTOMERGE_PATH,
1191
+ content: DEPENDABOT_AUTOMERGE
1192
+ }];
1166
1193
  /**
1167
1194
  * Whether a workflow file on disk is the thin caller — i.e. it delegates to the pinned
1168
1195
  * central reusable workflow. A file that does not is a legacy hand-written pipeline.
@@ -2290,6 +2317,32 @@ async function writeWorkflows(setup) {
2290
2317
  }
2291
2318
  }
2292
2319
  /**
2320
+ * Write the Dependabot config and its automerge workflow. A missing file is written; a
2321
+ * file that differs is the project's own config and is left alone, without asking.
2322
+ *
2323
+ * @param setup - The wizard state.
2324
+ * @returns Nothing.
2325
+ * @example
2326
+ * await writeDependabot(setup);
2327
+ */
2328
+ async function writeDependabot(setup) {
2329
+ setup.ui.heading("Dependabot");
2330
+ for (const template of dependabotTemplates) {
2331
+ const existing = await setup.ctx.files.read(template.path);
2332
+ if (existing === template.content) {
2333
+ setup.ui.check(true, `${template.path} up to date`);
2334
+ continue;
2335
+ }
2336
+ if (existing !== void 0) {
2337
+ setup.ui.check(true, `${template.path} is the project's own, left as is`);
2338
+ continue;
2339
+ }
2340
+ if (deferred(setup, `write ${template.path}`)) continue;
2341
+ await setup.ctx.files.write(template.path, template.content);
2342
+ setup.ui.check(true, `${template.path} written`);
2343
+ }
2344
+ }
2345
+ /**
2293
2346
  * Bring `package.json` onto the release contract. The change summary is printed first and
2294
2347
  * applied only after a confirm; an already-compliant manifest is a checkmark.
2295
2348
  *
@@ -2510,6 +2563,39 @@ async function applyBranchRuleset(setup, ownerRepo) {
2510
2563
  setup.ui.check(created.code === 0, "branch ruleset applied", created.stderr.trim() || void 0);
2511
2564
  }
2512
2565
  /**
2566
+ * Turn on the repo setting the automerge workflow needs. Without it `gh pr merge --auto`
2567
+ * fails and every Dependabot PR waits for a manual merge.
2568
+ *
2569
+ * @param setup - The wizard state.
2570
+ * @param ownerRepo - The `owner/repo` slug.
2571
+ * @returns Nothing.
2572
+ * @example
2573
+ * await enableAutoMerge(setup, "moku-labs/common");
2574
+ */
2575
+ async function enableAutoMerge(setup, ownerRepo) {
2576
+ setup.ui.heading("Auto-merge");
2577
+ const current = await setup.ctx.exec.capture("gh", [
2578
+ "api",
2579
+ `repos/${ownerRepo}`,
2580
+ "--jq",
2581
+ ".allow_auto_merge"
2582
+ ]);
2583
+ if (current.code === 0 && current.stdout.trim() === "true") {
2584
+ setup.ui.check(true, "auto-merge already allowed");
2585
+ return;
2586
+ }
2587
+ if (deferred(setup, `allow auto-merge on ${ownerRepo}`)) return;
2588
+ const updated = await setup.ctx.exec.capture("gh", [
2589
+ "api",
2590
+ `repos/${ownerRepo}`,
2591
+ "--method",
2592
+ "PATCH",
2593
+ "-F",
2594
+ "allow_auto_merge=true"
2595
+ ]);
2596
+ setup.ui.check(updated.code === 0, "auto-merge allowed", updated.stderr.trim() || void 0);
2597
+ }
2598
+ /**
2513
2599
  * Run the whole wizard. Every step is skippable, idempotent, and re-runnable; the last one
2514
2600
  * is always `doctor`, so the wizard's own verdict is the same report the operator gets
2515
2601
  * from `release:doctor`.
@@ -2537,6 +2623,7 @@ async function runSetup(options) {
2537
2623
  return 1;
2538
2624
  }
2539
2625
  await writeWorkflows(setup);
2626
+ await writeDependabot(setup);
2540
2627
  await normalizeContract(setup, manifest);
2541
2628
  const published = await firstPublish(setup, manifest.name, manifest.version);
2542
2629
  if (published === "absent") {
@@ -2546,8 +2633,11 @@ async function runSetup(options) {
2546
2633
  await registerTrustedPublisher(setup);
2547
2634
  const declared = repositoryUrlOf(manifest);
2548
2635
  const ownerRepo = declared === void 0 ? void 0 : ownerRepoFrom(declared);
2549
- if (ownerRepo === void 0) ui.warn("no GitHub owner/repo — skipping the branch ruleset");
2550
- else await applyBranchRuleset(setup, ownerRepo);
2636
+ if (ownerRepo === void 0) ui.warn("no GitHub owner/repo — skipping the branch ruleset and auto-merge");
2637
+ else {
2638
+ await applyBranchRuleset(setup, ownerRepo);
2639
+ await enableAutoMerge(setup, ownerRepo);
2640
+ }
2551
2641
  ui.heading("Doctor");
2552
2642
  const report = await runDoctor({
2553
2643
  ctx: setup.ctx,
@@ -0,0 +1,15 @@
1
+ # Dependabot for a moku-family repo. Only @moku-labs/* is watched, grouped into one PR a day.
2
+ # A package also gets examples/package/dependabot-automerge.yml, so the PR merges itself
3
+ # once the required ci / … checks pass. An app merges it by hand: that merge runs the deploy.
4
+ version: 2
5
+ updates:
6
+ - package-ecosystem: "bun"
7
+ directory: "/"
8
+ schedule:
9
+ interval: "daily"
10
+ allow:
11
+ - dependency-name: "@moku-labs/*"
12
+ groups:
13
+ moku:
14
+ patterns:
15
+ - "@moku-labs/*"
@@ -0,0 +1,21 @@
1
+ name: Dependabot automerge
2
+
3
+ # Turns on auto-merge for a Dependabot PR. The branch ruleset stays the gate: the merge
4
+ # waits for ci / lint, ci / types, ci / test and ci / build. Needs the repo setting
5
+ # allow_auto_merge, which `moku-release setup` turns on.
6
+ on: pull_request
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ jobs:
13
+ automerge:
14
+ if: github.event.pull_request.user.login == 'dependabot[bot]'
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - name: Merge once required checks pass
18
+ run: gh pr merge --auto --squash "$PR_URL"
19
+ env:
20
+ PR_URL: ${{ github.event.pull_request.html_url }}
21
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moku-labs/ci",
3
- "version": "1.2.6",
3
+ "version": "1.3.0",
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": [