depbot-policy 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Munawirul Hadi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,588 @@
1
- # Temporary Holding Version
1
+ # depbot-policy
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/heyhadi/depbot-policy/actions/workflows/ci.yml/badge.svg)](https://github.com/heyhadi/depbot-policy/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/depbot-policy.svg)](https://www.npmjs.com/package/depbot-policy)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ Write one small policy file. Get a `dependabot.yml` and a GitHub Actions workflow that
8
+ auto-merges only the dependency updates you consider safe, and hands everything else to this
9
+ week's reviewer.
10
+
11
+ ```yaml
12
+ # depbot.policy.yml
13
+ version: 1
14
+ ecosystems:
15
+ - type: npm
16
+ directory: /
17
+ autoMerge:
18
+ updateTypes: [patch]
19
+ block:
20
+ - name: react
21
+ reason: Pinned until the React 19 migration
22
+ review:
23
+ rotation: [alice, bob, carol]
24
+ ```
25
+
26
+ ```sh
27
+ npx depbot-policy generate
28
+ ```
29
+
30
+ - `.github/dependabot.yml`: what Dependabot updates, how often, and what it ignores, with your
31
+ reasons kept as comments.
32
+ - `.github/workflows/dependabot-auto-merge.yml`: merges patch updates once CI passes, never major
33
+ ones, and requests a review from this week's person for everything else.
34
+
35
+ **[Try it in the browser →](https://heyhadi.github.io/depbot-policy/)**
36
+
37
+ ## Contents
38
+
39
+ - [Why](#why)
40
+ - [Quick start](#quick-start)
41
+ - [CLI](#cli)
42
+ - [Policy reference](#policy-reference)
43
+ - [What gets generated](#what-gets-generated)
44
+ - [Repository setup](#repository-setup)
45
+ - [Keeping files in sync in CI](#keeping-files-in-sync-in-ci)
46
+ - [Validation and errors](#validation-and-errors)
47
+ - [Web playground](#web-playground)
48
+ - [Library API](#library-api)
49
+ - [Development](#development)
50
+ - [Releasing](#releasing)
51
+ - [Design notes](#design-notes)
52
+
53
+ ## Why
54
+
55
+ Dependabot opens a pull request for every dependency update. Teams end up either merging them
56
+ without looking or letting them pile up. Setting up something better usually means hand-writing
57
+ two YAML files that have to agree with each other:
58
+
59
+ - `dependabot.yml`, for what to update and what to ignore, and
60
+ - an auto-merge workflow full of `fetch-metadata` outputs and GitHub expressions.
61
+
62
+ depbot-policy replaces both with one validated file:
63
+
64
+ - **Safe updates merge themselves.** Patch updates (and minor ones, if you opt in) merge after your
65
+ CI passes.
66
+ - **Risky updates go to a person.** Major versions, packages whose maintainers changed, and
67
+ anything else outside the policy get a review request from whoever's turn it is this week.
68
+ - **Blocked packages stay blocked, with a reason.** The reason is kept next to the rule, so the
69
+ block list can be cleaned up later.
70
+
71
+ ## Quick start
72
+
73
+ Requires Node 22 or newer.
74
+
75
+ ```sh
76
+ # 1. Create a starter policy (valid as-is, with every option explained)
77
+ npx depbot-policy init
78
+
79
+ # 2. Edit depbot.policy.yml, then generate the files
80
+ npx depbot-policy generate
81
+
82
+ # 3. Commit all three files
83
+ git add depbot.policy.yml .github/dependabot.yml .github/workflows/dependabot-auto-merge.yml
84
+ git commit -m "Manage Dependabot with depbot-policy"
85
+ ```
86
+
87
+ Then do the one-time [repository setup](#repository-setup), so that auto-merge waits for CI.
88
+
89
+ To change anything later, edit `depbot.policy.yml` and run `npx depbot-policy generate` again.
90
+ Don't edit the generated files by hand. [`check`](#keeping-files-in-sync-in-ci) catches that.
91
+
92
+ ## CLI
93
+
94
+ ```text
95
+ depbot-policy <command> [options]
96
+
97
+ Commands:
98
+ init Create a starter depbot.policy.yml
99
+ generate Write .github/dependabot.yml and the auto-merge workflow
100
+ check Fail if the policy is invalid or the generated files are out of date
101
+ reviewer Print this week's reviewer from review.rotation
102
+
103
+ Options:
104
+ --policy <file> Policy file (default: depbot.policy.yml)
105
+ --out <dir> Repository root to write to or check (default: .)
106
+ --dry-run generate: print the files instead of writing them
107
+ --force init: overwrite an existing policy file
108
+ --date <date> reviewer: use this date instead of today (e.g. 2026-10-12)
109
+ -h, --help Show this help
110
+ -v, --version Show the version
111
+ ```
112
+
113
+ | Command | Exit code |
114
+ |---|---|
115
+ | Success | `0` |
116
+ | Invalid policy, or `check` found missing or outdated files | `1` |
117
+ | Wrong usage (unknown command or option, bad `--date`) | `2` |
118
+
119
+ Examples:
120
+
121
+ ```sh
122
+ npx depbot-policy generate --dry-run # preview without writing anything
123
+ npx depbot-policy generate --policy ops/deps.yml # policy somewhere else
124
+ npx depbot-policy reviewer # who's on duty this week?
125
+ npx depbot-policy reviewer --date 2026-12-28 # ...and in the last week of the year?
126
+ ```
127
+
128
+ You can also install it as a dev dependency (`npm install --save-dev depbot-policy`) and call
129
+ `depbot-policy` from npm scripts.
130
+
131
+ ## Policy reference
132
+
133
+ A complete policy, with every option:
134
+
135
+ ```yaml
136
+ version: 1 # required, always 1
137
+
138
+ ecosystems: # required, at least one
139
+ - type: npm # required
140
+ directory: / # required, starts with "/"
141
+ schedule: weekly # daily | weekly | monthly (default: weekly)
142
+ - type: github-actions
143
+ directory: /
144
+ schedule: monthly
145
+
146
+ autoMerge: # optional; defaults shown
147
+ updateTypes: [patch] # patch, minor
148
+ dependencyTypes: [development, production]
149
+ mergeMethod: squash # squash | merge | rebase
150
+
151
+ block: # optional, default: []
152
+ - name: react # package name or glob, e.g. "@types/*"
153
+ reason: Pinned until the React 19 migration # required
154
+ ecosystems: [npm] # optional; default: every ecosystem
155
+
156
+ review: # optional
157
+ rotation: [alice, bob, carol] # GitHub usernames
158
+ ```
159
+
160
+ ### `version`
161
+
162
+ Must be `1`. It's there so that a future, incompatible format can be detected and migrated.
163
+
164
+ ### `ecosystems`
165
+
166
+ One entry for each package manager and directory that Dependabot should watch.
167
+
168
+ | Field | Required | Values | Default |
169
+ |---|---|---|---|
170
+ | `type` | yes | `bundler`, `cargo`, `composer`, `docker`, `github-actions`, `gomod`, `gradle`, `maven`, `mix`, `npm`, `nuget`, `pip`, `pub`, `swift`, `terraform` | |
171
+ | `directory` | yes | Path from the repository root, starting with `/` (e.g. `/`, `/apps/web`) | |
172
+ | `schedule` | no | `daily`, `weekly`, `monthly` | `weekly` |
173
+
174
+ - Yarn, pnpm and Bun projects use `npm`; Poetry and Pipenv use `pip`. If you write one of those
175
+ names instead, the error message tells you which one to use.
176
+ - The same `type` can appear more than once with different directories, which is how monorepos
177
+ are handled. The same `type` and `directory` twice is an error.
178
+ - Include `github-actions` if you can. It keeps your workflows' actions up to date, including the
179
+ pinned `fetch-metadata` action in the generated workflow.
180
+
181
+ ### `autoMerge`
182
+
183
+ Which Dependabot pull requests merge without a person.
184
+
185
+ | Field | Values | Default |
186
+ |---|---|---|
187
+ | `updateTypes` | `patch`, `minor` | `[patch]` |
188
+ | `dependencyTypes` | `development`, `production` | `[development, production]` |
189
+ | `mergeMethod` | `squash`, `merge`, `rebase` | `squash` |
190
+
191
+ - `major` is deliberately not allowed. Major versions can contain breaking changes, so a person
192
+ should always read them.
193
+ - `development` and `production` mean direct dependencies (Dependabot's `direct:development` and
194
+ `direct:production`). Indirect (transitive) updates are never auto-merged.
195
+ - The merge method must be enabled in your repository settings.
196
+
197
+ If you leave out `autoMerge`, the defaults apply: patch updates to all direct dependencies,
198
+ squash-merged.
199
+
200
+ ### `block`
201
+
202
+ Packages Dependabot should never update. Each entry becomes an `ignore` rule.
203
+
204
+ | Field | Required | Description |
205
+ |---|---|---|
206
+ | `name` | yes | Exact package name, or a glob such as `@types/*` |
207
+ | `reason` | yes | Why it's blocked. Kept as a comment in `dependabot.yml`. |
208
+ | `ecosystems` | no | Only block it for these ecosystem types (each must appear in `ecosystems`). Without this, it's blocked everywhere. |
209
+
210
+ ### `review`
211
+
212
+ | Field | Required | Description |
213
+ |---|---|---|
214
+ | `rotation` | yes, if `review` is present | GitHub usernames, without `@`. Compared case-insensitively, so `alice` and `Alice` count as duplicates. |
215
+
216
+ Every Dependabot pull request that **isn't** auto-merged gets a review request from one person:
217
+
218
+ - **One person per week.** Weeks run from Monday 00:00 to Sunday 23:59 UTC, and people take turns
219
+ in list order.
220
+ - **Based on when the pull request was opened**, so re-runs always pick the same person.
221
+ - **Nothing is stored anywhere.** The turn is worked out from the date alone, so
222
+ `npx depbot-policy reviewer` (and the playground) can tell you who's on duty.
223
+ - **Changing the rota:** to cover a holiday, reorder or edit the list and regenerate.
224
+
225
+ Reviewers must have access to the repository, or GitHub rejects the request.
226
+
227
+ ## What gets generated
228
+
229
+ For the [example policy](examples/depbot.policy.yml):
230
+
231
+ <details>
232
+ <summary><code>.github/dependabot.yml</code></summary>
233
+
234
+ ```yaml
235
+ # Generated by depbot-policy from depbot.policy.yml. Do not edit by hand:
236
+ # change the policy file and regenerate.
237
+
238
+ version: 2
239
+ updates:
240
+ - package-ecosystem: npm
241
+ directory: /
242
+ schedule:
243
+ interval: weekly
244
+ ignore:
245
+ - dependency-name: react # Pinned until the React 19 migration
246
+ - dependency-name: "@types/*" # Type packages are updated together with their runtime package
247
+ - package-ecosystem: github-actions
248
+ directory: /
249
+ schedule:
250
+ interval: monthly
251
+ ```
252
+
253
+ </details>
254
+
255
+ <details>
256
+ <summary><code>.github/workflows/dependabot-auto-merge.yml</code></summary>
257
+
258
+ ```yaml
259
+ # Generated by depbot-policy from depbot.policy.yml. Do not edit by hand:
260
+ # change the policy file and regenerate.
261
+
262
+ # Requires, in the repository settings:
263
+ # - "Allow auto-merge" enabled.
264
+ # - Branch protection on the default branch with required status checks.
265
+ # Without required checks, GitHub merges at once instead of waiting for CI.
266
+
267
+ name: Dependabot auto-merge
268
+ on: pull_request
269
+ permissions:
270
+ contents: write
271
+ pull-requests: write
272
+ jobs:
273
+ auto-merge:
274
+ runs-on: ubuntu-latest
275
+ if: github.event.pull_request.user.login == 'dependabot[bot]' && github.actor == 'dependabot[bot]'
276
+ steps:
277
+ - id: metadata
278
+ uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
279
+ with:
280
+ github-token: ${{ secrets.GITHUB_TOKEN }}
281
+ - id: auto-merge
282
+ name: Enable auto-merge
283
+ if: steps.metadata.outputs.update-type == 'version-update:semver-patch' && (steps.metadata.outputs.dependency-type == 'direct:development' || steps.metadata.outputs.dependency-type == 'direct:production') && steps.metadata.outputs.maintainer-changes != 'true'
284
+ run: gh pr merge --auto --squash "$PR_URL"
285
+ env:
286
+ PR_URL: ${{ github.event.pull_request.html_url }}
287
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
288
+ - name: Request a review from this week's reviewer
289
+ if: steps.auto-merge.outcome == 'skipped'
290
+ run: |-
291
+ read -ra reviewers <<< "$REVIEWERS"
292
+ created=$(date -u -d "$PR_CREATED_AT" +%s)
293
+ week=$(( (created + 259200) / 604800 ))
294
+ reviewer="${reviewers[week % ${#reviewers[@]}]}"
295
+ gh pr edit "$PR_URL" --add-reviewer "$reviewer"
296
+ env:
297
+ REVIEWERS: alice bob carol
298
+ PR_CREATED_AT: ${{ github.event.pull_request.created_at }}
299
+ PR_URL: ${{ github.event.pull_request.html_url }}
300
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
301
+ ```
302
+
303
+ </details>
304
+
305
+ ### When does a pull request get auto-merged?
306
+
307
+ All of these must be true:
308
+
309
+ 1. **Dependabot opened the pull request, and Dependabot triggered this run.** If anyone else
310
+ pushes a commit to a Dependabot branch, that run doesn't qualify.
311
+ 2. **The update type is allowed** by `autoMerge.updateTypes`. For grouped updates,
312
+ `fetch-metadata` reports the largest change in the group, so one minor bump makes the whole
313
+ group "minor".
314
+ 3. **The dependency type is allowed** by `autoMerge.dependencyTypes`.
315
+ 4. **The package's maintainers haven't changed.** A new maintainer is a common sign of a
316
+ supply-chain attack, so those updates wait for a person.
317
+
318
+ The workflow then runs `gh pr merge --auto`. That doesn't merge immediately. It tells GitHub to
319
+ merge once the branch's protection rules pass, which is why the setup below matters. If any
320
+ condition is false and the policy has a rotation, the next step requests a review instead.
321
+
322
+ ## Repository setup
323
+
324
+ Do this once in each repository that uses the generated files.
325
+
326
+ 1. **Allow auto-merge:** *Settings → General → Pull Requests →* check **Allow auto-merge**.
327
+ 2. **Allow your merge method:** in the same section, make sure the method in
328
+ `autoMerge.mergeMethod` (squash by default) is enabled.
329
+ 3. **Require CI before merging:** *Settings → Branches* (or *Settings → Rules → Rulesets*) → add a
330
+ rule for your default branch → **Require status checks to pass**, and select your CI checks.
331
+
332
+ > ⚠️ Without required status checks, `gh pr merge --auto` merges straight away, without waiting
333
+ > for CI.
334
+
335
+ 4. **Commit the policy and the generated files** to the default branch.
336
+
337
+ You don't need any extra tokens or secrets. The workflow uses the built-in `GITHUB_TOKEN` and
338
+ declares the two permissions it needs. If your organization stops workflows from raising token
339
+ permissions, allow it for this repository under *Settings → Actions → General → Workflow
340
+ permissions*.
341
+
342
+ **To check it works:** the next Dependabot patch update should show "Auto-merge enabled" and merge
343
+ by itself once CI passes. A major update should get a review request instead.
344
+
345
+ ## Keeping files in sync in CI
346
+
347
+ Add `check` to your CI, so a policy change can't be merged without regenerating the files:
348
+
349
+ ```yaml
350
+ - run: npx depbot-policy check
351
+ ```
352
+
353
+ It fails, with exit code 1, when the policy is invalid or when either generated file is missing or
354
+ differs from what the policy produces:
355
+
356
+ ```text
357
+ .github/workflows/dependabot-auto-merge.yml is out of date
358
+ Run `depbot-policy generate` and commit the result.
359
+ ```
360
+
361
+ ## Validation and errors
362
+
363
+ The policy is validated before anything is generated, and every problem is reported at once, not
364
+ just the first. Each line is `file:line:column: path: message`, which editors and CI logs can
365
+ link to:
366
+
367
+ ```text
368
+ $ npx depbot-policy check
369
+ depbot.policy.yml:4:5: ecosystems[0].type: Dependabot covers yarn under "npm"; use type: npm
370
+ depbot.policy.yml:5:5: ecosystems[0].directory: Must start with "/" (paths are relative to the repository root)
371
+ depbot.policy.yml:6:5: ecosystems[0].shedule: Unknown key
372
+ depbot.policy.yml:9:24: autoMerge.updateTypes[1]: Major updates are never auto-merged; they always need a human review
373
+ depbot.policy.yml:12:5: block[0].reason: Required
374
+ ```
375
+
376
+ Rules worth knowing:
377
+
378
+ - **Unknown keys are errors**, so a typo like `automerge:` or `shedule:` is caught instead of
379
+ being silently ignored.
380
+ - **Duplicates are errors**, and the error points at the second occurrence. This covers the same
381
+ ecosystem and directory, the same blocked package, the same username in the rotation (ignoring
382
+ case), and the same value twice in any list.
383
+ - **YAML syntax errors** and **duplicate YAML keys** are reported with their position too.
384
+
385
+ ## Web playground
386
+
387
+ **[heyhadi.github.io/depbot-policy](https://heyhadi.github.io/depbot-policy/)** lets you write a
388
+ policy and see the generated files as you type.
389
+
390
+ - **Live validation.** Errors are underlined in the editor and listed below it in line order.
391
+ Click one to jump to the exact text.
392
+ - **Generated files in tabs**, with Copy and Download buttons. While the policy has errors, the
393
+ last valid output stays visible, dimmed.
394
+ - **This week's reviewer** is shown when the policy has a rotation.
395
+ - **Presets:** full example, minimal, monorepo, and one with deliberate mistakes.
396
+ - **Share link:** stores the policy in the URL after the `#`. That part of a URL is never sent to
397
+ a server, so nothing leaves your browser.
398
+ - Light and dark mode follow your system settings, the layout works on phones, and the file tabs
399
+ can be used with the keyboard.
400
+
401
+ It's built with Next.js 16 (static export), React 19, CodeMirror 6 and Tailwind CSS 4, and it
402
+ imports the library straight from [`src/`](src/), so it always matches the code in the same commit.
403
+
404
+ ## Library API
405
+
406
+ ```sh
407
+ npm install depbot-policy
408
+ ```
409
+
410
+ ```ts
411
+ import { generateFiles, parsePolicy } from "depbot-policy";
412
+
413
+ const result = parsePolicy(source); // source: the policy's YAML text
414
+
415
+ if (!result.ok) {
416
+ for (const error of result.errors) {
417
+ // error.path: "ecosystems[0].type"
418
+ // error.message: 'Dependabot covers yarn under "npm"; use type: npm'
419
+ // error.location?: { start, end, line, column } (offsets, plus 1-based line and column)
420
+ }
421
+ } else {
422
+ for (const file of generateFiles(result.policy)) {
423
+ // file.path: ".github/dependabot.yml", ".github/workflows/dependabot-auto-merge.yml"
424
+ // file.contents: the YAML to write
425
+ }
426
+ }
427
+ ```
428
+
429
+ | Export | Description |
430
+ |---|---|
431
+ | `parsePolicy(source: string): ParseResult` | Parses and validates YAML. Returns `{ ok: true, policy }` with defaults filled in, or `{ ok: false, errors }`. Never throws for bad input. |
432
+ | `generateFiles(policy): GeneratedFile[]` | Every generated file, as `{ path, contents }`. |
433
+ | `generateDependabotConfig(policy): string` | Just `.github/dependabot.yml`. |
434
+ | `generateAutoMergeWorkflow(policy): string` | Just `.github/workflows/dependabot-auto-merge.yml`. |
435
+ | `reviewerFor(rotation: string[], date: Date): string` | The reviewer on duty at `date`, using the same formula as the workflow. |
436
+ | `policySchema` | The Zod schema, if you need to validate an already-parsed object. |
437
+ | `ecosystemTypes` | The supported ecosystem names. |
438
+ | Types: `Policy`, `ParseResult`, `PolicyError`, `SourceLocation`, `GeneratedFile` | |
439
+
440
+ Everything is pure: no file system, network or global state. That's what lets the same code run in
441
+ the CLI, in tests and in the browser.
442
+
443
+ ## Development
444
+
445
+ Requires Node 22 or newer (see [`.nvmrc`](.nvmrc)). Node runs the TypeScript source directly
446
+ during development; only the published package is compiled.
447
+
448
+ ```sh
449
+ git clone https://github.com/heyhadi/depbot-policy.git
450
+ cd depbot-policy
451
+ npm install
452
+ npm test
453
+ ```
454
+
455
+ ### Commands
456
+
457
+ At the repository root:
458
+
459
+ | Command | What it does |
460
+ |---|---|
461
+ | `npm test` | Library and CLI tests (Vitest) |
462
+ | `npm run typecheck` | Type-check the library, CLI and tests |
463
+ | `npm run cli -- <command>` | Run the CLI from source, e.g. `npm run cli -- generate --dry-run --policy examples/depbot.policy.yml` |
464
+ | `npm run build` | Compile `src/` to `dist/` (JavaScript and type declarations) |
465
+
466
+ In `playground/` (run `npm install` at the root first; the playground uses the library source):
467
+
468
+ | Command | What it does |
469
+ |---|---|
470
+ | `npm install` | Install the playground's own dependencies |
471
+ | `npm run dev` | Dev server on http://localhost:3000 |
472
+ | `npm run build` | Static site in `playground/out/` |
473
+ | `npm test` | Playground tests (Vitest, React Testing Library, jsdom) |
474
+ | `npm run typecheck` | Type-check the playground |
475
+
476
+ ### Project structure
477
+
478
+ ```text
479
+ src/
480
+ schema.ts Policy schema and validation rules (Zod)
481
+ parse.ts parsePolicy: YAML → validated policy or errors with locations
482
+ locate.ts Maps an error path to its position in the YAML source
483
+ dependabot.ts dependabot.yml generator
484
+ workflow.ts Auto-merge workflow generator
485
+ rotation.ts Weekly reviewer: TypeScript formula and the workflow's shell version
486
+ files.ts generateFiles: every generated file and its path
487
+ cli.ts, bin.ts The depbot-policy command (bin.ts is the executable entry point)
488
+ starter.ts The policy written by `init`
489
+ index.ts Public exports
490
+ test/ Library and CLI tests; __snapshots__/ holds generated files
491
+ examples/ The example policy (the playground's default preset must match it)
492
+ playground/ Next.js web playground (app/, components/, lib/, test/)
493
+ depbot.policy.yml This repository's own policy; .github/dependabot.yml and the
494
+ auto-merge workflow are generated from it
495
+ .github/workflows/
496
+ ci.yml Tests, checks and linting on every pull request
497
+ pages.yml Deploys the playground to GitHub Pages
498
+ ```
499
+
500
+ ### Tests
501
+
502
+ - **Validation:** every rule has a test that checks the exact error path and message;
503
+ `locate.test.ts` checks the text each error points at.
504
+ - **Generators:** two kinds of test for each file.
505
+ - **Snapshots** (`test/__snapshots__/*.yml`) catch any change to the output's formatting. They're
506
+ real YAML files, so changes are easy to read in a diff.
507
+ - **Content tests** parse the output and check what it means, whatever the formatting.
508
+ - **Rotation:** the workflow's shell script runs in bash, with stand-in `date` and `gh` commands,
509
+ and must pick the same person as `reviewerFor` on every test date.
510
+ - **CLI:** every command runs against a real temporary directory.
511
+ - **Playground:** unit tests for its logic, plus tests of the whole page with React Testing Library.
512
+ CodeMirror can't run in jsdom, so the tests replace the two small editor components with plain
513
+ elements.
514
+
515
+ When you change a generator on purpose, update the snapshots and review the diff before
516
+ committing:
517
+
518
+ ```sh
519
+ npx vitest run -u
520
+ git diff test/__snapshots__
521
+ ```
522
+
523
+ ### Continuous integration
524
+
525
+ [`ci.yml`](.github/workflows/ci.yml) runs on every pull request and every push to `main`:
526
+
527
+ - **Library:**
528
+ - Type-check and tests.
529
+ - `depbot-policy check` on this repository's own generated files.
530
+ - A **packed-package smoke test**: `npm pack`, install the tarball in an empty project, then run
531
+ `init`, `generate` and `check`.
532
+ - [actionlint](https://github.com/rhysd/actionlint) on this repository's workflows *and* on the
533
+ generated workflow snapshots, which also runs shellcheck on their scripts. actionlint is
534
+ downloaded from a pinned release and checked against its SHA-256.
535
+ - **Playground:** type-check, tests and a production build.
536
+
537
+ [`pages.yml`](.github/workflows/pages.yml) deploys the playground to GitHub Pages when `src/` or
538
+ `playground/` changes on `main`.
539
+
540
+ ### Updating the pinned `fetch-metadata` action
541
+
542
+ The generated workflow pins `dependabot/fetch-metadata` to a commit SHA. To move to a new
543
+ release:
544
+
545
+ 1. Find the release's commit: `gh api repos/dependabot/fetch-metadata/commits/<tag> -q .sha`
546
+ 2. Update `action` and `version` in `fetchMetadata` in [`src/workflow.ts`](src/workflow.ts).
547
+ 3. Check that the outputs used in the generated `if:` conditions still exist in that release.
548
+ 4. Update the snapshots (`npx vitest run -u`), run the tests, and regenerate this repository's own
549
+ files (`npm run cli -- generate`).
550
+
551
+ ## Releasing
552
+
553
+ 1. Update `version` in `package.json` (for example `npm version minor`).
554
+ 2. `npm publish`. `prepublishOnly` runs the type-check, the tests and the build first, so a broken
555
+ package can't be published by accident. Only `dist/`, `README.md`, `LICENSE` and
556
+ `package.json` are included (about 11 kB).
557
+ 3. Push the commit and tag.
558
+
559
+ ## Design notes
560
+
561
+ Short versions of the main decisions. The pull requests have the full reasoning.
562
+
563
+ - **One source of truth.** The Zod schema defines both the validation rules and the `Policy`
564
+ TypeScript type, so they can't disagree.
565
+ - **Strict validation.** Unknown keys are errors, because silently ignoring `automerge:` would leave
566
+ auto-merge on its defaults without you knowing.
567
+ - **Return errors, don't throw.** Invalid input is an expected case for a config tool, and callers
568
+ need every error, not just the first.
569
+ - **Pure core.** No I/O in the library, so it runs unchanged in the CLI, in tests and in the
570
+ browser.
571
+ - **Generated YAML via the `yaml` document API**, not string templates. The library handles quoting
572
+ (`"@types/*"` must be quoted) and comments.
573
+ - **Stateless rotation.** The reviewer is a function of the week, so there's nothing to store, sync
574
+ or get out of date.
575
+ - **Pinned actions.** Actions are referenced by commit SHA with a `# vX.Y.Z` comment, both in the
576
+ generated workflow and in this repository's own workflows. Tags can be moved; SHAs can't.
577
+ Dependabot understands the comment and updates both.
578
+ - **Least-privilege workflows.**
579
+ - Only the permissions each job needs.
580
+ - `pull_request`, not `pull_request_target`.
581
+ - No `${{ }}` expressions inside `run:` scripts. Values go through environment variables, which
582
+ prevents script injection.
583
+ - **It uses itself.** This repository's Dependabot setup is generated from its own
584
+ [`depbot.policy.yml`](depbot.policy.yml), and CI checks it.
585
+
586
+ ## License
587
+
588
+ [MIT](LICENSE) © Munawirul Hadi
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./cli.js";
3
+ process.exitCode = run(process.argv.slice(2), {
4
+ cwd: process.cwd(),
5
+ stdout: (text) => process.stdout.write(text),
6
+ stderr: (text) => process.stderr.write(text),
7
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type PolicyError } from "./parse.ts";
2
+ export interface CliIo {
3
+ cwd: string;
4
+ stdout: (text: string) => void;
5
+ stderr: (text: string) => void;
6
+ }
7
+ /** Exit codes: 0 success, 1 invalid policy or outdated files, 2 wrong usage. */
8
+ export declare const exitCodes: {
9
+ readonly ok: 0;
10
+ readonly failed: 1;
11
+ readonly usage: 2;
12
+ };
13
+ /** Runs the CLI and returns its exit code. Side effects go through `io` and the file system. */
14
+ export declare function run(argv: readonly string[], io: CliIo): number;
15
+ /** `file:line:column: path: message`, the format editors and CI annotations understand. */
16
+ export declare function formatError(file: string, error: PolicyError): string;