@moku-labs/ci 1.2.6 → 1.4.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
@@ -24,6 +24,7 @@ everything else lives here, once. Not a build tool and not a framework — it ca
24
24
  [Workflows](#workflows) ·
25
25
  [Project checks](#project-checks) ·
26
26
  [PR previews](#pr-previews) ·
27
+ [Demos](#demos) ·
27
28
  [CLI](#cli) ·
28
29
  [The contract](#the-contract) ·
29
30
  [Versioning](#versioning) ·
@@ -40,14 +41,16 @@ bun add -d @moku-labs/ci
40
41
  bun run release:setup
41
42
  ```
42
43
 
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.
44
+ `setup` writes the two workflow files and the Dependabot files, adds the missing scripts,
45
+ does the first publish, registers the trusted publisher, applies the branch ruleset and
46
+ allows auto-merge. Run it again any time — it only does what is still missing.
46
47
 
47
48
  > [!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.
49
+ > **Status: `1.x`, in use.** All eight moku packages — `core`, `common`, `web`, `worker`, `room`,
50
+ > `system`, `ai`, `native` — run `package-ci.yml` and `package-release.yml` from `@v1`, and each
51
+ > has shipped a tokenless release with provenance through them. `moku-release setup` did every
52
+ > one of those migrations live, including two first publishes behind 2FA.
53
+ > `app-deploy.yml` has no live user yet.
51
54
 
52
55
  > [!IMPORTANT]
53
56
  > Two steps are yours alone. The CLI never handles a credential, and there is no `NPM_TOKEN`
@@ -60,7 +63,7 @@ only does what is still missing.
60
63
 
61
64
  ## Why @moku-labs/ci
62
65
 
63
- - **One copy of the pipeline.** Seven repos had seven different `ci.yml` and six different
66
+ - **One copy of the pipeline.** Eight packages had eight different `ci.yml` and eight different
64
67
  `publish.yml`. A fix now lands here and reaches every project through the `@v1` tag.
65
68
  - **Scripts are the interface, not tools.** The workflows call `bun run lint`, never `biome`.
66
69
  A project with special needs changes its own script, not the central YAML.
@@ -106,10 +109,13 @@ flowchart LR
106
109
  |---|---|
107
110
  | [`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
111
  | [`rulesets/main.json`](rulesets/main.json) | Branch ruleset for `main`: PR only, no force-push, the four `ci / …` checks required. |
112
+ | [`examples/dependabot.yml`](examples/dependabot.yml) | Dependabot config: one grouped PR a day for `@moku-labs/*` only. `setup` writes it to `.github/dependabot.yml`. |
113
+ | [`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
114
 
110
115
  > [!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.
116
+ > A Layer-3 app copies `examples/app/ci.yml` and `examples/dependabot.yml` by hand and needs
117
+ > a `deploy` script. An app merges Dependabot PRs by hand, because the merge runs the deploy.
118
+ > The CLI sets up packages only.
113
119
 
114
120
  ## Project checks
115
121
 
@@ -152,11 +158,45 @@ bun add https://pkg.pr.new/@moku-labs/core@42 # 42 = PR number, a commit sha w
152
158
  | A preview URL never reaches `main`: the `lint` job and `doctor` both refuse it. | `package-ci.yml`, `preview-deps` |
153
159
  | `preview` is not a required check. Turn it off with `with: { preview: false }`. | caller `ci.yml` |
154
160
 
161
+ ## Demos
162
+
163
+ `demos.yml` runs the moku demos (`moku-labs/demos`) against what a pull request builds, so an
164
+ engine or editor change that breaks a real game fails on its own PR.
165
+
166
+ ```yaml
167
+ demos:
168
+ needs: ci
169
+ if: github.event_name == 'pull_request'
170
+ uses: moku-labs/ci/.github/workflows/demos.yml@v1
171
+ with:
172
+ game: https://pkg.pr.new/@moku-labs/game@${{ github.event.pull_request.head.sha }}
173
+ tier: ${{ contains(github.event.pull_request.labels.*.name, 'demos:full') && 'full' || 'fast' }}
174
+ ```
175
+
176
+ | Input | Default | Meaning |
177
+ |---|---|---|
178
+ | `game`, `editor` | `""` | A version, a pkg.pr.new URL, or empty for each demo's own pin |
179
+ | `tier` | `fast` | `fast`: `bun run test` on ubuntu. `full`: plus `test:visual` on macos-latest and `test:editor` |
180
+ | `demos_ref` | `main` | demos branch when no paired branch exists |
181
+ | `paired_ref` | PR branch | demos branch with this name wins, for an intended break |
182
+ | `only` | `[]` | JSON array of demo folders; empty = all |
183
+
184
+ | Rule | Where |
185
+ |---|---|
186
+ | A demo is a folder whose `package.json` has `"moku": { "demo": true }`. | demos repo |
187
+ | The workflow only calls the demo's scripts with `--engine` / `--editor`; the demo's runner picks the package, as it does locally. | demo runner |
188
+ | A tracked file with `/Users/`, `/home/…` or `?? "../` fails the run. Paths are parameters. | `discover` |
189
+ | Checks to require: `demos / fast`; `demos / full` reports only on the full tier. | rulesets |
190
+ | Pixels run on macOS: on Linux runners WebGPU loses its device. | `visual-run` |
191
+
192
+ Examples: `examples/package/ci-with-demos.yml` (label `demos:full`) and
193
+ `examples/package/demos-comment.yml` (PR comment `/demos full`).
194
+
155
195
  ## CLI
156
196
 
157
197
  | Command | When | What it does |
158
198
  |---|---|---|
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. |
199
+ | `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
200
  | `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
201
  | `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
202
 
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,28 @@
1
+ name: CI
2
+
3
+ # ci.yml of a package whose PRs must run the moku demos (the engine, the editor).
4
+ # Same thin caller as ci.yml, plus a `demos` job after the preview is published.
5
+ # Required checks: "ci / lint", "ci / types", "ci / test", "ci / build" and "demos / fast".
6
+ # The label `demos:full` on the PR adds pixels (macOS) and editor scenarios: "demos / full".
7
+ on:
8
+ push:
9
+ branches: [main]
10
+ pull_request:
11
+ types: [opened, synchronize, reopened, labeled]
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ ci:
18
+ uses: moku-labs/ci/.github/workflows/package-ci.yml@v1
19
+
20
+ demos:
21
+ needs: ci
22
+ if: github.event_name == 'pull_request'
23
+ uses: moku-labs/ci/.github/workflows/demos.yml@v1
24
+ with:
25
+ # The preview job of `ci` published this commit; a full sha resolves on pkg.pr.new.
26
+ # In the editor's repo, pass `editor:` instead of `game:`.
27
+ game: https://pkg.pr.new/@moku-labs/game@${{ github.event.pull_request.head.sha }}
28
+ tier: ${{ contains(github.event.pull_request.labels.*.name, 'demos:full') && 'full' || 'fast' }}
@@ -0,0 +1,41 @@
1
+ name: Demos on comment
2
+
3
+ # A PR comment `/demos full` runs the full demos tier against the PR's preview, without a label.
4
+ # Only members and collaborators can trigger it.
5
+ on:
6
+ issue_comment:
7
+ types: [created]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ head:
14
+ if: >-
15
+ github.event.issue.pull_request &&
16
+ startsWith(github.event.comment.body, '/demos full') &&
17
+ contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
18
+ runs-on: ubuntu-latest
19
+ permissions:
20
+ pull-requests: read
21
+ outputs:
22
+ sha: ${{ steps.pr.outputs.sha }}
23
+ ref: ${{ steps.pr.outputs.ref }}
24
+ steps:
25
+ - id: pr
26
+ env:
27
+ GH_TOKEN: ${{ github.token }}
28
+ PR: ${{ github.event.issue.number }}
29
+ REPO: ${{ github.repository }}
30
+ run: |
31
+ set -euo pipefail
32
+ gh api "repos/$REPO/pulls/$PR" --jq '"sha=\(.head.sha)\nref=\(.head.ref)"' >> "$GITHUB_OUTPUT"
33
+
34
+ demos:
35
+ needs: head
36
+ uses: moku-labs/ci/.github/workflows/demos.yml@v1
37
+ with:
38
+ game: https://pkg.pr.new/@moku-labs/game@${{ needs.head.outputs.sha }}
39
+ # issue_comment has no head_ref: name the branch to pair with explicitly.
40
+ paired_ref: ${{ needs.head.outputs.ref }}
41
+ tier: full
@@ -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.4.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": [
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@biomejs/biome": "2.4.16",
35
- "@moku-labs/common": "0.3.0",
35
+ "@moku-labs/common": "0.3.3",
36
36
  "@types/bun": "1.3.14",
37
37
  "@vitest/coverage-istanbul": "4.0.18",
38
38
  "eslint": "9.39.3",