template-git-repo 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/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # template-git-repo
2
+
3
+ One command to give a repository the CI, publishing, badges and Turborepo wiring
4
+ worked out in [qwksearch-research-agent](https://github.com/OpenSourceAGI/qwksearch-research-agent):
5
+
6
+ ```bash
7
+ bunx template-git-repo
8
+ ```
9
+
10
+ It detects the repo slug, default branch, package manager, workspace layout and
11
+ the package to advertise on npm, writes the workflows and their helper scripts,
12
+ wires up Turborepo, and injects a badge block into your README. Nothing existing
13
+ is overwritten without `--force`, and `--dry-run` prints the exact plan.
14
+
15
+ ## What it writes
16
+
17
+ | Path | What it is |
18
+ | --- | --- |
19
+ | `.github/workflows/tests.yml` | Per-package tests with a **discovered** matrix, coverage + test analytics to Codecov |
20
+ | `.github/workflows/npm-publish.yml` | Publishes every workspace package whose *content* changed — no version bookkeeping |
21
+ | `.github/workflows/auto-merge-claude.yml` | Auto-merges agent PRs once their checks pass |
22
+ | `.github/workflows/auto-merge-and-create-prs.yml` | Twice-daily sweep: merges green PRs, opens PRs for orphan branches |
23
+ | `.github/workflows/deploy-test-reports.yml` | Publishes the HTML test report to Cloudflare Workers |
24
+ | `scripts/*.mjs` | The five helpers those workflows call |
25
+ | `turbo.json` | Pipeline whose task names the workflows use |
26
+ | `codecov.yml` | Per-package flags with `carryforward` |
27
+ | `README.md` | The badge block, between markers |
28
+
29
+ Full setup notes: **[docs/ACTIONS.md](./docs/ACTIONS.md)** (workflows and secrets)
30
+ and **[docs/BADGES.md](./docs/BADGES.md)** (every badge, and what you have to do
31
+ outside the repo before it shows anything real).
32
+
33
+ ## Options
34
+
35
+ ```bash
36
+ bunx template-git-repo --dry-run # print the plan, change nothing
37
+ bunx template-git-repo --force # replace files that already exist
38
+ bunx template-git-repo --actions-only # just the workflows and their scripts
39
+ bunx template-git-repo --badges-only # just the README badge block
40
+ bunx template-git-repo --no-turbo # leave turbo.json and package.json alone
41
+ ```
42
+
43
+ Badge inputs — each one enables the badge that needs it, and badges without their
44
+ input are left out with a note rather than rendered broken:
45
+
46
+ ```bash
47
+ bunx template-git-repo \
48
+ --doi 10.5281/zenodo.20951725 \
49
+ --docs https://example.com/docs \
50
+ --discord-id 1110227955554209923 --discord-invite https://discord.gg/xxxx \
51
+ --stack Claude,Cloudflare,Next.js
52
+ ```
53
+
54
+ Run `bunx template-git-repo --help` for the rest.
55
+
56
+ ## The parts worth knowing
57
+
58
+ **The test matrix is discovered, not written.** `scripts/list-test-packages.mjs`
59
+ reads the `workspaces` globs and emits one matrix entry per package with a test
60
+ script. A hand-maintained matrix fails silently — a package added to the repo but
61
+ not to the matrix is never tested, and nothing goes red to say so.
62
+
63
+ **Publishing compares content, not version numbers.** npm tarballs are
64
+ reproducible, so `npm pack` integrity against the registry answers "did anything
65
+ actually change". Version bumps happen as a consequence, not as a prerequisite,
66
+ and they are taken from what the registry says is free — including the versions a
67
+ half-finished publish reserved, which `latest` cannot show you.
68
+
69
+ **Badges carry their own setup instructions.** `docs/BADGES.md` is generated from
70
+ the catalog in `src/badges.js`, and a test fails if it drifts. A badge whose
71
+ prerequisites nobody wrote down is a badge that reads `unknown` forever.
72
+
73
+ **The badge block is re-runnable.** It lives between
74
+ `<!-- template-git-repo:badges:start -->` and `...:end -->`, so running the CLI
75
+ again after you publish a package or finish setting up Codecov replaces the block
76
+ and nothing else.
77
+
78
+ ## Programmatic use
79
+
80
+ ```js
81
+ import { renderBadgeBlock, buildContext } from 'template-git-repo';
82
+
83
+ const context = buildContext({ overrides: { stack: 'Bun,Cloudflare' } });
84
+ const { markdown, skipped } = renderBadgeBlock(context);
85
+ ```
86
+
87
+ ## Adding a badge
88
+
89
+ Add an entry to `BADGES` in `src/badges.js` — `needs`, `setup` and `group` are
90
+ what make it self-documenting — then `bun run docs:badges`. Nothing else needs to
91
+ know about it.
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * One command that sets a repository up with the CI, publishing, badges and
4
+ * Turborepo wiring worked out in OpenSourceAGI/qwksearch-research-agent.
5
+ *
6
+ * bunx template-git-repo
7
+ *
8
+ * Everything it can detect, it detects — repo slug, default branch, package
9
+ * manager, workspace layout, the package to advertise on npm — so the common
10
+ * case takes no flags. What it cannot know (a DOI, a Discord server, a docs
11
+ * URL) is passed in, and the badges that need those are skipped with a note
12
+ * rather than rendered broken.
13
+ *
14
+ * Nothing existing is overwritten without `--force`, and `--dry-run` prints the
15
+ * exact plan a real run would carry out.
16
+ */
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import process from 'node:process';
20
+ import { buildContext } from '../src/context.js';
21
+ import { renderBadgeBlock, BADGES } from '../src/badges.js';
22
+ import { injectBadges, hasUnmarkedBadges } from '../src/readme.js';
23
+ import { planFiles, applyPlan, wireTurbo } from '../src/apply.js';
24
+
25
+ const HELP = `
26
+ template-git-repo — set up a repo with the GitHub Actions, badges and Turborepo
27
+ config from the qwksearch reference setup.
28
+
29
+ Usage
30
+ bunx template-git-repo [options]
31
+
32
+ What it writes
33
+ .github/workflows/ tests, npm publish, auto-merge, hosted test reports
34
+ scripts/*.mjs the helpers those workflows call
35
+ turbo.json pipeline whose task names the workflows use
36
+ codecov.yml per-package flags with carryforward
37
+ README.md the badge block, between markers
38
+
39
+ Options
40
+ --dry-run Print the plan and change nothing
41
+ --force Overwrite files that already exist
42
+ --actions-only Only the workflows and their scripts
43
+ --badges-only Only the README badge block
44
+ --no-turbo Do not write turbo.json or touch the root package.json
45
+ --yes, -y Skip the confirmation prompt
46
+
47
+ Context (all optional — detected where possible)
48
+ --repo <owner/repo> Default: the origin remote
49
+ --branch <name> Default: the remote's HEAD branch
50
+ --pm <bun|pnpm|yarn|npm>
51
+ --npm-package <name> Package for the npm version/downloads badges
52
+ --workflow <file> Workflow file for the CI badge (default tests.yml)
53
+
54
+ Badge inputs (each enables the badge that needs it)
55
+ --doi <10.5281/...> --docs <url> --api <url>
56
+ --youtube <url> --uptime <url> --test-report <url>
57
+ --discord-id <id> --discord-invite <url>
58
+ --stack <A,B,C> --cloudflare-deploy
59
+ --exclude <id,id> --only <id,id>
60
+
61
+ Badge ids: ${BADGES.map((b) => b.id).join(', ')}
62
+
63
+ Docs
64
+ docs/ACTIONS.md what each workflow does and which secrets it needs
65
+ docs/BADGES.md how to set up each badge
66
+ `;
67
+
68
+ /**
69
+ * A deliberately small parser: this CLI takes `--flag` and `--flag value` and
70
+ * nothing else, so a dependency for it would cost more than it saves.
71
+ *
72
+ * @param {string[]} argv
73
+ * @returns {Record<string, string | boolean>}
74
+ */
75
+ export function parseArgs(argv) {
76
+ const flags = {};
77
+
78
+ for (let i = 0; i < argv.length; i++) {
79
+ const arg = argv[i];
80
+ if (!arg.startsWith('-')) continue;
81
+
82
+ const key = arg.replace(/^--?/, '');
83
+ const next = argv[i + 1];
84
+
85
+ if (next && !next.startsWith('-')) {
86
+ flags[key] = next;
87
+ i++;
88
+ } else {
89
+ flags[key] = true;
90
+ }
91
+ }
92
+
93
+ return flags;
94
+ }
95
+
96
+ /**
97
+ * @param {Record<string, string | boolean>} flags
98
+ * @returns {Record<string, string | undefined>}
99
+ */
100
+ function overridesFrom(flags) {
101
+ const text = (key) => (typeof flags[key] === 'string' ? flags[key] : undefined);
102
+
103
+ return {
104
+ repoSlug: text('repo'),
105
+ defaultBranch: text('branch'),
106
+ packageManager: text('pm'),
107
+ npmPackage: text('npm-package'),
108
+ workflowFile: text('workflow'),
109
+ doi: text('doi'),
110
+ docsUrl: text('docs'),
111
+ apiUrl: text('api'),
112
+ youtubeUrl: text('youtube'),
113
+ uptimeUrl: text('uptime'),
114
+ testReportUrl: text('test-report'),
115
+ discordId: text('discord-id'),
116
+ discordInvite: text('discord-invite'),
117
+ stack: text('stack'),
118
+ cloudflareDeploy: flags['cloudflare-deploy'] ? 'true' : undefined,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * @param {string[]} [argv]
124
+ * @returns {Promise<number>} process exit code
125
+ */
126
+ export async function main(argv = process.argv.slice(2)) {
127
+ const flags = parseArgs(argv);
128
+
129
+ if (flags.help || flags.h) {
130
+ console.log(HELP);
131
+ return 0;
132
+ }
133
+
134
+ const dryRun = Boolean(flags['dry-run']);
135
+ const force = Boolean(flags.force);
136
+ const badgesOnly = Boolean(flags['badges-only']);
137
+ const actionsOnly = Boolean(flags['actions-only']);
138
+ const skipTurbo = Boolean(flags['no-turbo']) || badgesOnly;
139
+
140
+ const context = buildContext({ overrides: overridesFrom(flags) });
141
+
142
+ if (!context.repoSlug) {
143
+ console.error(
144
+ 'Could not work out which GitHub repo this is: no `origin` remote, or one that is not on github.com.\n' +
145
+ 'Pass it explicitly: bunx template-git-repo --repo owner/repo',
146
+ );
147
+ return 1;
148
+ }
149
+
150
+ console.log(`\n📦 ${context.repoSlug} (${context.defaultBranch}, ${context.packageManager})`);
151
+ console.log(` ${context.root}\n`);
152
+
153
+ let changed = 0;
154
+
155
+ // ── Workflows, scripts, turbo.json, codecov.yml ───────────────────────────
156
+ if (!badgesOnly) {
157
+ const include = actionsOnly ? ['.github', 'scripts'] : undefined;
158
+ const filePlan = planFiles({ context, force, include }).filter(
159
+ (entry) => !(skipTurbo && entry.path === 'turbo.json'),
160
+ );
161
+
162
+ for (const entry of filePlan) {
163
+ const mark = entry.action === 'skip' ? '·' : entry.action === 'overwrite' ? '~' : '+';
164
+ console.log(` ${mark} ${entry.path}${entry.action === 'skip' ? ' (exists — use --force to replace)' : ''}`);
165
+ }
166
+
167
+ const { written } = applyPlan(filePlan, { dryRun });
168
+ changed += written.length;
169
+ console.log('');
170
+ }
171
+
172
+ // ── Root package.json: turbo scripts and devDependency ────────────────────
173
+ if (!skipTurbo && !actionsOnly) {
174
+ const manifestPath = path.join(context.root, 'package.json');
175
+
176
+ if (!fs.existsSync(manifestPath)) {
177
+ console.log(' · package.json not found at the repo root — skipping the Turborepo wiring\n');
178
+ } else {
179
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
180
+ const { manifest: next, changes } = wireTurbo(manifest, { packagesDir: context.packagesDir });
181
+
182
+ if (changes.length === 0) {
183
+ console.log(' · package.json already has the turbo wiring\n');
184
+ } else {
185
+ console.log(` ~ package.json (${changes.join(', ')})\n`);
186
+ if (!dryRun) fs.writeFileSync(manifestPath, `${JSON.stringify(next, null, 2)}\n`);
187
+ changed++;
188
+ }
189
+ }
190
+ }
191
+
192
+ // ── README badges ─────────────────────────────────────────────────────────
193
+ if (!actionsOnly) {
194
+ const readmePath = ['README.md', 'readme.md', 'Readme.md']
195
+ .map((name) => path.join(context.root, name))
196
+ .find((candidate) => fs.existsSync(candidate)) ?? path.join(context.root, 'README.md');
197
+
198
+ const existing = fs.existsSync(readmePath) ? fs.readFileSync(readmePath, 'utf8') : '';
199
+
200
+ const { markdown, skipped } = renderBadgeBlock(context, {
201
+ only: typeof flags.only === 'string' ? flags.only.split(',') : undefined,
202
+ exclude: typeof flags.exclude === 'string' ? flags.exclude.split(',') : [],
203
+ });
204
+
205
+ const { content, action } = injectBadges(existing, markdown);
206
+
207
+ if (content === existing) {
208
+ console.log(' · README.md badges already up to date\n');
209
+ } else {
210
+ console.log(` ~ ${path.basename(readmePath)} (badge block ${action})`);
211
+ if (hasUnmarkedBadges(existing)) {
212
+ console.log(' ⚠ the README already had badges of its own — they were left in place, remove the duplicates by hand');
213
+ }
214
+ if (!dryRun) fs.writeFileSync(readmePath, content);
215
+ changed++;
216
+ console.log('');
217
+ }
218
+
219
+ if (skipped.length > 0) {
220
+ console.log(' Badges not included (nothing to point them at):');
221
+ for (const { id, missing } of skipped) {
222
+ console.log(` ${id.padEnd(20)} needs ${missing.join(', ')}`);
223
+ }
224
+ console.log(' → see docs/BADGES.md, then re-run with the matching flag\n');
225
+ }
226
+ }
227
+
228
+ // ── What the human still has to do ────────────────────────────────────────
229
+ if (!badgesOnly) {
230
+ console.log(' Repository secrets the workflows expect:');
231
+ console.log(' CODECOV_TOKEN coverage + test analytics uploads');
232
+ console.log(' NPM_TOKEN only if you are not using npm trusted publishing');
233
+ console.log(' GIT_TOKEN a PAT for auto-merge (GITHUB_TOKEN merges do not trigger workflows)');
234
+ console.log(' CLOUDFLARE_API_TOKEN hosted test reports');
235
+ console.log(' CLOUDFLARE_ACCOUNT_ID hosted test reports');
236
+ console.log(' → docs/ACTIONS.md explains each one\n');
237
+ }
238
+
239
+ if (dryRun) {
240
+ console.log(`Dry run — nothing was written (${changed} file(s) would change).\n`);
241
+ } else {
242
+ console.log(changed === 0 ? 'Already set up — nothing to do.\n' : `Done — ${changed} file(s) changed.\n`);
243
+ }
244
+
245
+ return 0;
246
+ }
247
+
248
+ // Only run when invoked as a program, so the module can be imported by tests.
249
+ if (import.meta.url === `file://${process.argv[1]}`) {
250
+ main().then(
251
+ (code) => process.exit(code),
252
+ (error) => {
253
+ console.error(error);
254
+ process.exit(1);
255
+ },
256
+ );
257
+ }
@@ -0,0 +1,195 @@
1
+ # GitHub Actions setup
2
+
3
+ What each workflow in `template/.github/workflows/` does, what it needs before it
4
+ can work, and the failure it exists to prevent. These are the workflows from
5
+ [OpenSourceAGI/qwksearch-research-agent](https://github.com/OpenSourceAGI/qwksearch-research-agent),
6
+ generalized — most of the comments in them describe an incident.
7
+
8
+ ## The secrets, in one table
9
+
10
+ Add these under **Settings → Secrets and variables → Actions → New repository secret**.
11
+
12
+ | Secret | Needed by | How to get it | If it is missing |
13
+ | --- | --- | --- | --- |
14
+ | `CODECOV_TOKEN` | `tests.yml` | codecov.io → add the repo → Settings → Repository Upload Token | Uploads are rejected; the coverage badge stays `unknown`. Tests still run and still pass. |
15
+ | `NPM_TOKEN` | `npm-publish.yml` | npmjs.com → Access Tokens → Granular, read-write on the packages | Only needed if you are *not* using trusted publishing. See below. |
16
+ | `GIT_TOKEN` | `auto-merge-*.yml` | A fine-grained PAT with Contents: read-write and Pull requests: read-write | Falls back to `GITHUB_TOKEN`, which merges but does **not** trigger further workflows — so a merge would never fire the publish run. |
17
+ | `CLOUDFLARE_API_TOKEN` | `deploy-test-reports.yml` | dash.cloudflare.com → My Profile → API Tokens → "Edit Cloudflare Workers" | The deploy step fails; the rest of the run is unaffected. |
18
+ | `CLOUDFLARE_ACCOUNT_ID` | `deploy-test-reports.yml` | The hex id in any Cloudflare dashboard URL | Same. |
19
+
20
+ Repository settings that matter as much as the secrets:
21
+
22
+ - **Settings → General → Allow auto-merge** — without it, `--auto` is rejected and
23
+ the auto-merge workflows fall back to merging immediately, which skips waiting
24
+ for checks.
25
+ - **Settings → Actions → General → Workflow permissions → Read and write** —
26
+ without it, the version-bump commit in `npm-publish.yml` cannot be pushed.
27
+ - **Branch protection with at least one required check** — this is what `--auto`
28
+ waits on. With no required check there is nothing to wait for.
29
+
30
+ ---
31
+
32
+ ## `tests.yml` — test, coverage, and test analytics
33
+
34
+ Runs every workspace package that has a test suite, uploads `coverage/lcov.info`
35
+ to Codecov under a per-package flag, and uploads `junit.xml` to Codecov Test
36
+ Analytics, which tracks per-test run time and flakiness and comments failures on
37
+ the PR.
38
+
39
+ **The matrix is discovered, not written.** A `discover` job runs
40
+ `scripts/list-test-packages.mjs`, which reads the `workspaces` globs from the root
41
+ `package.json` and emits one entry per package with a `test:coverage`, `test:ci`
42
+ or `test` script. A hand-maintained matrix fails silently — a package added to
43
+ the repo but not to the matrix is simply never tested, and nothing goes red to
44
+ say so.
45
+
46
+ What each package has to provide:
47
+
48
+ ```jsonc
49
+ {
50
+ "scripts": {
51
+ // Whatever writes coverage/lcov.info and junit.xml. For Vitest:
52
+ "test:coverage": "vitest run --coverage"
53
+ },
54
+ // Optional: keep a currently-red suite from failing the workflow while it
55
+ // still uploads its results. Remove once the suite is green.
56
+ "ci": { "allowFailure": true }
57
+ }
58
+ ```
59
+
60
+ For Vitest, `junit.xml` comes from the reporter config, not a flag:
61
+
62
+ ```js
63
+ reporters: process.env.CI ? ['default', 'junit'] : ['default'],
64
+ outputFile: { junit: './junit.xml' },
65
+ ```
66
+
67
+ **Why `if: ${{ !cancelled() }}` and not `always()`** on the upload steps: a
68
+ cancelled run has nothing worth uploading, but a *failed* one is exactly when the
69
+ report matters most. `always()` also runs on cancellation and clutters the
70
+ dashboard with partial data.
71
+
72
+ **Why `fail_ci_if_error: false`**: Codecov having an outage must not turn a green
73
+ suite red.
74
+
75
+ ---
76
+
77
+ ## `npm-publish.yml` — publish what changed, without version bookkeeping
78
+
79
+ Publishes every non-private workspace package whose *content* changed, on every
80
+ push to the default branch. Nobody has to remember to bump a version.
81
+
82
+ Five rules are encoded in it, each learned from a specific failure:
83
+
84
+ 1. **Compare pack integrity, not version numbers.** npm tarballs are reproducible
85
+ (normalized mtimes), so `npm pack --dry-run --json` integrity against
86
+ `npm view <pkg>@<version> dist.integrity` is a reliable "did anything change".
87
+ A version bump is not evidence of a change, and an unchanged version is not
88
+ evidence of no change.
89
+ 2. **Check the credential before building anything.** npm answers an unauthorized
90
+ PUT with `E404 ... could not be found or you do not have permission to access
91
+ it`, which reads like a missing package — and it arrives *after* every package
92
+ has been built, with a provenance statement already signed into the public
93
+ sigstore transparency log for each failed attempt.
94
+ 3. **Walk the workspace in dependency order** (`scripts/workspace-build-order.mjs`).
95
+ Package managers link workspace siblings as symlinks; a sibling that has not
96
+ been built has no `dist/`, and its `exports` → `types` entries point at files
97
+ that do not exist. Alphabetical order produces
98
+ `Cannot find module '<sibling>' or its corresponding type declarations`.
99
+ 4. **Never publish the literal `workspace:*` protocol** — no consumer outside the
100
+ monorepo can resolve it. `scripts/pin-workspace-deps.mjs` substitutes real
101
+ ranges at pack time; `scripts/restore-pinned-deps.mjs` takes them back out
102
+ afterwards, so only the version bump gets committed.
103
+ 5. **The registry decides which versions are spent** (`scripts/next-free-version.mjs`).
104
+ npm refuses a PUT for any version it has *ever* seen, including ones staged by
105
+ an interrupted publish that the `latest` dist-tag cannot show you:
106
+ `E409 ... Cannot publish over previously staged version`. The union of
107
+ `versions` and the `time` timeline is the real taken set.
108
+
109
+ ### Trusted publishing (recommended) vs `NPM_TOKEN`
110
+
111
+ **Trusted publishing (OIDC)** — no secret at all. Leave `NPM_TOKEN` unset. The
112
+ job's `id-token: write` permission is traded by npm ≥ 11.5.1 for a short-lived
113
+ publish token. For each package, go to
114
+ `npmjs.com/package/<name>/access` → **Trusted publisher** and name this
115
+ repository plus `npm-publish.yml`. Nothing to rotate, nothing to expire.
116
+
117
+ **`NPM_TOKEN`** — a granular access token with read-write on the packages. It
118
+ expires after 90 days at most, and classic automation tokens no longer work for
119
+ direct publishing. Setting the secret selects this path; leaving it unset selects
120
+ OIDC.
121
+
122
+ ### Failure decoder
123
+
124
+ | Symptom | Cause → fix |
125
+ | --- | --- |
126
+ | `E404` on publish for a package that exists | The credential cannot write. Rotate `NPM_TOKEN`, or finish the trusted-publisher setup for that package. |
127
+ | `E409 Cannot publish over previously staged version` | A number the registry reserved. The workflow already retries at the next free version five times; more than that means the registry is refusing everything. |
128
+ | `Cannot implicitly apply the latest tag` | The local version fell behind the registry, usually a bump commit that never landed back. The workflow syncs to `latest` and re-evaluates. |
129
+ | `EUNSUPPORTEDPROTOCOL workspace:*` | `npm` was run where the workspace's own package manager should have been, or without `--no-workspaces` over its symlinks. |
130
+ | `husky: not found` (exit 127) | A dependency ships a broken `prepare: "husky install"`. The no-op husky shim step handles it; do not remove that step. |
131
+ | `vite: not found` during a package build | Dependencies were installed with plain `npm` inside a bun/pnpm workspace, which leaves devDependencies uninstalled. |
132
+
133
+ ---
134
+
135
+ ## `auto-merge-claude.yml` — merge agent PRs when they go green
136
+
137
+ Fires on every PR event and, **only for the actors in its `if:` allowlist**,
138
+ enables auto-merge. That allowlist is the entire safety model — edit it to the
139
+ accounts you actually want merged unattended, and never widen it to all
140
+ contributors.
141
+
142
+ It tries `gh pr merge --auto` first (GitHub merges once required checks pass) and
143
+ falls back to an immediate merge, because `--auto` is rejected outright on a repo
144
+ with no branch protection — there is nothing for it to wait on.
145
+
146
+ Long-lived branches (`production`, `prod`, `staging`, `develop`) are merged
147
+ without `--delete-branch`.
148
+
149
+ ---
150
+
151
+ ## `auto-merge-and-create-prs.yml` — the twice-daily sweep
152
+
153
+ The backstop for two things that fall through the cracks: an agent session that
154
+ pushes a branch and never opens a PR, and a PR whose last check finished after
155
+ the per-PR workflow had already run.
156
+
157
+ It merges every open PR that is non-draft, `CLEAN`, not blocked by a requested
158
+ change, and whose checks all concluded `SUCCESS`/`SKIPPED`/`NEUTRAL` — a *pending*
159
+ check means "not yet", not "merge it". Then it opens a PR for every remote branch
160
+ that has neither an open nor a previously merged PR and is not already an ancestor
161
+ of the default branch.
162
+
163
+ It deliberately does not use `actions/checkout`: it needs every remote ref, and
164
+ `git init` + a full `git fetch` is cheaper than `fetch-depth: 0`.
165
+
166
+ ---
167
+
168
+ ## `deploy-test-reports.yml` — a test report with a URL
169
+
170
+ Publishes the Vitest HTML reporter output to Cloudflare Workers on every push to
171
+ the default branch, so the Test Report badge links to something current instead of
172
+ to a workflow log.
173
+
174
+ Needs an `apps/test-reports/` directory with a `wrangler.toml` whose
175
+ `assets.directory` is `dist`, and a root `test:report` script that writes there.
176
+
177
+ The test step is `continue-on-error: true` — a red suite is exactly when the
178
+ report is worth reading. What it must not do is leave `dist/` missing: wrangler
179
+ treats an absent `assets.directory` as a hard error, so the run would end on a
180
+ config error instead of on the test signal. The placeholder-page step exists for
181
+ that.
182
+
183
+ ---
184
+
185
+ ## Adding your own
186
+
187
+ Keep the shape the rest of these use:
188
+
189
+ - `concurrency.group` on anything that pushes or deploys, so two runs cannot race.
190
+ - `fail-fast: false` on any matrix where one entry's failure should not hide the
191
+ others' results.
192
+ - `if: ${{ !cancelled() }}` rather than `always()` on upload steps.
193
+ - A comment at the top saying what the workflow is *for*, and a comment at any
194
+ step whose reason is not obvious from the code. The comments in these files are
195
+ the part that survives; the YAML is the easy half.