setup-git-repo 1.0.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.
@@ -0,0 +1,50 @@
1
+ # Codecov configuration for the packages tested by .github/workflows/tests.yml.
2
+ #
3
+ # Statuses are informational: they annotate a pull request without ever blocking
4
+ # it. Test Analytics — the failed-test comment and the flake dashboard — is the
5
+ # part meant to be acted on.
6
+
7
+ codecov:
8
+ # Publish and deploy workflows run on the default branch only, so a pull
9
+ # request often has no CI for Codecov to wait on before reporting.
10
+ require_ci_to_pass: false
11
+
12
+ coverage:
13
+ status:
14
+ project:
15
+ default:
16
+ target: auto
17
+ threshold: 1%
18
+ informational: true
19
+ patch:
20
+ default:
21
+ informational: true
22
+
23
+ # One flag per package. `carryforward` means a package that skipped a run (its
24
+ # paths were untouched) keeps its last known coverage instead of reading as a
25
+ # drop to zero.
26
+ #
27
+ # Flags are created on first upload, so this block only needs entries when a
28
+ # flag's paths differ from the default. Add one per package as the repo grows:
29
+ #
30
+ # individual_flags:
31
+ # - name: my-package
32
+ # paths:
33
+ # - packages/my-package/
34
+ flag_management:
35
+ default_rules:
36
+ carryforward: true
37
+
38
+ comment:
39
+ layout: "header, diff, flags, files"
40
+ # Don't post on pull requests that changed nothing Codecov measures.
41
+ require_changes: true
42
+
43
+ ignore:
44
+ - "**/test/**"
45
+ - "**/tests/**"
46
+ - "**/__tests__/**"
47
+ - "**/examples/**"
48
+ - "**/docs/**"
49
+ - "**/dist/**"
50
+ - "**/build/**"
@@ -0,0 +1,271 @@
1
+ # Badges: what each one needs and how to set it up
2
+
3
+ Every badge in the README falls into one of three groups:
4
+
5
+ | Group | Setup | Badges |
6
+ | --- | --- | --- |
7
+ | **Zero setup** — works the moment the repo is public | none | Stars, Commit Activity, Last Commit, License, PRs Welcome, Deploy to Cloudflare, DeepWiki, tech-stack chips |
8
+ | **Needs a workflow or service connected** | one repo secret or one app install | Tests status, Coverage, npm version, npm downloads |
9
+ | **Needs an account and an id you paste in** | an external account | DOI, Discord, UptimeRobot, YouTube, Docs, API |
10
+
11
+ `setup-git-repo` drops the badge lines for group 3 unless you pass the id, so the
12
+ README never ships a badge pointing at `{{DOI}}`. Add them back later by copying
13
+ the snippet from this file.
14
+
15
+ Almost everything is [Shields.io](https://shields.io), whose URL shape is
16
+ `https://img.shields.io/<type>/<args>.svg?<options>`. Common options: `label=`,
17
+ `color=`/`colorB=`, `logo=` (any [Simple Icons](https://simpleicons.org) slug),
18
+ `logoColor=`, `style=` (`flat`, `flat-square`, `for-the-badge`).
19
+
20
+ ---
21
+
22
+ ## Zero setup
23
+
24
+ ### GitHub Stars
25
+
26
+ ```html
27
+ <a href="https://github.com/OWNER/REPO/stargazers"><img src="https://img.shields.io/github/stars/OWNER/REPO" alt="GitHub Stars" /></a>
28
+ ```
29
+
30
+ Nothing to configure. Works on public repos only — Shields has no token for
31
+ your private repo and renders `invalid` or `repo not found`.
32
+
33
+ ### Commit Activity
34
+
35
+ ```html
36
+ <img src="https://img.shields.io/github/commit-activity/m/OWNER/REPO" alt="Commit Activity" />
37
+ ```
38
+
39
+ `m` is commits per month; `w` and `y` also work. Add `/BRANCH` to scope it:
40
+ `commit-activity/m/OWNER/REPO/main`.
41
+
42
+ ### Last Commit
43
+
44
+ ```html
45
+ <img src="https://img.shields.io/github/last-commit/OWNER/REPO.svg" alt="Last Commit" />
46
+ ```
47
+
48
+ Defaults to the default branch. `?display_timestamp=committer` switches from
49
+ author date to commit date, which is what you usually mean after a rebase.
50
+
51
+ ### License
52
+
53
+ ```html
54
+ <img src="https://img.shields.io/github/license/OWNER/REPO" alt="License" />
55
+ ```
56
+
57
+ Reads GitHub's detected license, which comes from a recognized `LICENSE` file.
58
+ A `LICENSE.md` with a custom or modified text is often detected as `Other` —
59
+ if that bothers you, use a static badge instead:
60
+ `https://img.shields.io/badge/license-MIT-green.svg`.
61
+
62
+ ### PRs Welcome
63
+
64
+ A static badge; it links to GitHub's "creating a pull request" docs. Point it at
65
+ your own `CONTRIBUTING.md` once you have one.
66
+
67
+ ### Deploy to Cloudflare Workers
68
+
69
+ ```html
70
+ <a href="https://deploy.workers.cloudflare.com/?url=https://github.com/OWNER/REPO"><img src="https://deploy.workers.cloudflare.com/button" alt="Deploy to Cloudflare Workers" /></a>
71
+ ```
72
+
73
+ The button image is hosted by Cloudflare; no account needed to *show* it. For the
74
+ click-through to actually work, the repo must be deployable: a `wrangler.jsonc`
75
+ (or `wrangler.toml`) at the root, or a `workers/` directory Cloudflare can find.
76
+ This template's Worker config lives in `apps/test-reports/`, so either move it,
77
+ add a root config, or point the button at a subdirectory URL.
78
+
79
+ ### Ask DeepWiki
80
+
81
+ ```html
82
+ <a href="https://deepwiki.com/OWNER/REPO"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki" /></a>
83
+ ```
84
+
85
+ DeepWiki indexes public repos automatically. The first visit to the link
86
+ triggers indexing if the repo has not been seen; the badge image itself is
87
+ static, so it renders even before indexing finishes.
88
+
89
+ ### Tech-stack chips
90
+
91
+ Pure decoration — static Shields badges with a brand color and a Simple Icons
92
+ slug:
93
+
94
+ ```html
95
+ <img src="https://img.shields.io/badge/Turborepo-EF4444?logo=turborepo&logoColor=white" alt="Turborepo" />
96
+ ```
97
+
98
+ Format is `badge/<label>-<hex color>?logo=<slug>`. A slug that isn't in Simple
99
+ Icons renders the label with no icon and no error, so check
100
+ [simpleicons.org](https://simpleicons.org) before assuming a typo elsewhere. A
101
+ literal `-` in the label must be written `--`, and a space `_`.
102
+
103
+ ---
104
+
105
+ ## Needs a workflow or service connected
106
+
107
+ ### Workflow status (Tests)
108
+
109
+ ```html
110
+ <a href="https://github.com/OWNER/REPO/actions/workflows/tests.yml"><img src="https://github.com/OWNER/REPO/actions/workflows/tests.yml/badge.svg?branch=main" alt="Tests" /></a>
111
+ ```
112
+
113
+ Served by GitHub, not Shields. Three things to get right:
114
+
115
+ - The path segment is the workflow **file name**, not the `name:` inside it.
116
+ `tests.yml` here; renaming the file breaks the badge silently (it renders
117
+ "no status").
118
+ - `?branch=` is worth setting. Without it the badge shows the most recent run on
119
+ *any* branch, so a red feature branch turns your README red.
120
+ - The badge shows `no status` until the workflow has run at least once on that
121
+ branch. Push to the branch or use "Run workflow" from the Actions tab.
122
+
123
+ One badge per workflow file: add `npm-publish.yml` or `deploy-test-reports.yml`
124
+ the same way if you want them visible.
125
+
126
+ ### Coverage (Codecov)
127
+
128
+ ```html
129
+ <a href="https://codecov.io/gh/OWNER/REPO"><img src="https://codecov.io/gh/OWNER/REPO/graph/badge.svg" alt="Coverage" /></a>
130
+ ```
131
+
132
+ Setup:
133
+
134
+ 1. Sign in at [codecov.io](https://codecov.io) with GitHub and add the repo.
135
+ 2. Copy the **repository upload token** and save it as the `CODECOV_TOKEN`
136
+ repository secret (Settings → Secrets and variables → Actions → New secret).
137
+ Public repos on GitHub Actions can often upload tokenless, but rate limits
138
+ make that unreliable in CI — set the secret.
139
+ 3. Push. `tests.yml` uploads `coverage/lcov.info` per package.
140
+
141
+ Getting the token from Codecov's UI: repo page → Settings → General → Repository
142
+ Upload Token. If the badge stays `unknown`, the upload never arrived — check the
143
+ "Upload coverage to Codecov" step's log, not the badge.
144
+
145
+ Private repos need the token in the badge URL too; Codecov's repo Settings →
146
+ Badges page shows the exact markdown with the graph token included.
147
+
148
+ `codecov.yml` in this template sets `carryforward: true` per flag, so a package
149
+ that didn't run in a given PR keeps its last coverage instead of reading as a
150
+ drop to 0%.
151
+
152
+ ### npm version and downloads
153
+
154
+ ```html
155
+ <a href="https://www.npmjs.com/package/PACKAGE"><img src="https://img.shields.io/npm/v/PACKAGE.svg" alt="npm version" /></a>
156
+ <a href="https://www.npmjs.com/package/PACKAGE"><img src="https://img.shields.io/npm/dm/PACKAGE.svg" alt="NPM Monthly Downloads" /></a>
157
+ ```
158
+
159
+ `PACKAGE` is the name in that package's `package.json`, not the repo name. Both
160
+ badges render `invalid` until the package's **first** publish — `npm-publish.yml`
161
+ does that on the first push to the default branch. Scoped packages work with the
162
+ scope included and URL-encoded slash: `npm/v/%40scope%2Fname`.
163
+
164
+ `dm` is downloads/month; `dw` weekly, `dt` total. In a monorepo, pick the package
165
+ users actually install — or show several, one badge each.
166
+
167
+ ---
168
+
169
+ ## Needs an account and an id you paste in
170
+
171
+ ### DOI (Zenodo)
172
+
173
+ ```html
174
+ <a href="https://doi.org/10.5281/zenodo.NNNNNNN"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.NNNNNNN.svg" alt="DOI" /></a>
175
+ ```
176
+
177
+ A DOI makes the repo citable in academic work. Setup:
178
+
179
+ 1. Sign in to [zenodo.org](https://zenodo.org) with GitHub.
180
+ 2. Zenodo → GitHub → flip the repo's switch **on**. This installs a webhook; it
181
+ only affects releases created *after* the switch.
182
+ 3. Create a GitHub Release (a tag alone is not enough — it must be a Release).
183
+ Zenodo archives the tarball and mints a DOI within a few minutes.
184
+ 4. Zenodo shows two DOIs. Use the **concept DOI** ("all versions"), not the
185
+ version-specific one, so the badge keeps pointing at the newest release.
186
+
187
+ Pass it to the CLI without the `https://doi.org/` prefix: `--doi 10.5281/zenodo.NNNNNNN`.
188
+
189
+ ### Discord
190
+
191
+ ```html
192
+ <a href="https://discord.gg/INVITE"><img src="https://img.shields.io/discord/SERVER_ID.svg?label=Chat&logo=Discord&colorB=7289da&style=flat" alt="Join Discord" /></a>
193
+ ```
194
+
195
+ Two different values, and mixing them up is the usual failure:
196
+
197
+ - **Server id** goes in the Shields path (it renders the online count). Get it
198
+ with Discord → User Settings → Advanced → Developer Mode on, then right-click
199
+ the server → Copy Server ID.
200
+ - **Invite code** goes in the `href`. Create a *never-expiring* invite —
201
+ a default invite expires in 7 days and the badge quietly links to a dead page.
202
+
203
+ The server also needs the **Widget** enabled (Server Settings → Widget → Enable
204
+ Server Widget); without it Shields cannot read the member count and the badge
205
+ reads `invalid`.
206
+
207
+ ### UptimeRobot
208
+
209
+ ```html
210
+ <a href="https://stats.uptimerobot.com/PAGE_ID"><img src="https://img.shields.io/badge/Uptime-Status-brightgreen?logo=uptimerobot&logoColor=white" alt="Uptime Status" /></a>
211
+ ```
212
+
213
+ As written this is a **static** badge — it always says "Status" and links to your
214
+ public status page. Setup: [uptimerobot.com](https://uptimerobot.com) → add a
215
+ monitor for your URL → Status Pages → create one → copy the id out of its URL.
216
+
217
+ For a badge that reports the *real* number, use a monitor-specific API key
218
+ (Monitor → Settings → API key, the `m` key) with Shields' UptimeRobot endpoints:
219
+
220
+ ```html
221
+ <img src="https://img.shields.io/uptimerobot/ratio/7/MONITOR_API_KEY" alt="Uptime 7d" />
222
+ <img src="https://img.shields.io/uptimerobot/status/MONITOR_API_KEY" alt="Up or down" />
223
+ ```
224
+
225
+ That key is read-only for one monitor, which is why it is safe in a README —
226
+ never paste your account-wide API key there.
227
+
228
+ ### Docs / API / YouTube links
229
+
230
+ ```html
231
+ <a href="https://your.docs"><img src="https://img.shields.io/badge/Docs-blue?logo=ReadTheDocs&logoColor=white" alt="Documentation" /></a>
232
+ <a href="https://your.api/docs"><img src="https://img.shields.io/badge/API-blue?logo=fastapi&logoColor=white" alt="API" /></a>
233
+ <a href="https://youtu.be/VIDEO_ID"><img height="20px" src="https://img.shields.io/badge/YouTube-red?style=for-the-badge&logo=youtube&logoColor=white" alt="YouTube" /></a>
234
+ ```
235
+
236
+ Static badges whose only "setup" is having somewhere to point them. They exist
237
+ to make the top of the README a navigation bar rather than a status board — the
238
+ logo does the work, so keep the label to one word.
239
+
240
+ Note the YouTube badge is `style=for-the-badge` while its neighbors are flat.
241
+ That is deliberate in the original layout but it renders taller; the `height`
242
+ attribute pulls it back in line. Drop `style=for-the-badge` if you would rather
243
+ they match.
244
+
245
+ ---
246
+
247
+ ## Layout
248
+
249
+ The block is plain HTML inside `<p align="center">`, not markdown, because
250
+ markdown image syntax cannot center or set a height. Consequences:
251
+
252
+ - Keep each `<a>` on one line. A newline inside the tag is fine for HTML but
253
+ makes the block much harder to reorder later.
254
+ - `<br />` between rows is what creates the grouping — GitHub collapses
255
+ whitespace, so blank lines do nothing.
256
+ - GitHub's markdown renderer strips `style` attributes. Sizing has to come from
257
+ the `height` attribute or from Shields' own `style=` parameter.
258
+
259
+ Group by meaning: identity and links on row one, health and freshness on row two,
260
+ community on row three, stack chips last.
261
+
262
+ ## Verifying
263
+
264
+ Badges are cached by GitHub's image proxy (camo), so a fixed badge can keep
265
+ looking broken for a while. To check the real state, open the badge's `src` URL
266
+ directly in a browser — that bypasses camo. Shields renders its own errors into
267
+ the image (`invalid`, `not found`, `inaccessible`), so the image itself tells you
268
+ which half of the URL is wrong.
269
+
270
+ To force GitHub to refetch: `curl -X PURGE <the camo URL>` on the rendered image
271
+ address, or simply wait — camo's TTL is minutes, not hours.
@@ -0,0 +1,77 @@
1
+ # Secrets and settings CI depends on
2
+
3
+ Repository secrets live at **Settings → Secrets and variables → Actions → New
4
+ repository secret**. Names are case-sensitive and must match exactly.
5
+
6
+ | Secret | Used by | Required? | Where it comes from |
7
+ | --- | --- | --- | --- |
8
+ | `CODECOV_TOKEN` | `tests.yml` | For coverage and Test Analytics | codecov.io → your repo → Settings → General → Repository Upload Token |
9
+ | `NPM_TOKEN` | `npm-publish.yml` | Only if you are not using trusted publishing | npmjs.com → Access Tokens → Generate → **Granular**, write access to the packages |
10
+ | `CLOUDFLARE_API_TOKEN` | `deploy-test-reports.yml` | For the report deploy | dash.cloudflare.com → My Profile → API Tokens → "Edit Cloudflare Workers" template |
11
+ | `CLOUDFLARE_ACCOUNT_ID` | `deploy-test-reports.yml` | For the report deploy | Cloudflare dashboard → Workers & Pages → the id in the right sidebar (or the URL) |
12
+ | `GIT_TOKEN` | both auto-merge workflows | For auto-merge | github.com → Settings → Developer settings → Personal access tokens |
13
+
14
+ `GITHUB_TOKEN` is provided automatically — never create a secret with that name;
15
+ GitHub rejects it.
16
+
17
+ ## Which workflows you can run without any secrets
18
+
19
+ `tests.yml` still runs the suites without `CODECOV_TOKEN`; only the two upload
20
+ steps fail, and both are `fail_ci_if_error: false`, so the job stays green while
21
+ you set the token up. Everything else needs its secret to do anything at all.
22
+
23
+ ## GIT_TOKEN scopes
24
+
25
+ A fine-grained personal access token, scoped to this repository, with:
26
+
27
+ - **Contents**: Read and write
28
+ - **Pull requests**: Read and write
29
+
30
+ A classic token needs `repo` (and `workflow` if the merged PRs ever touch
31
+ `.github/workflows`).
32
+
33
+ **Why not `GITHUB_TOKEN`?** Merges made with `GITHUB_TOKEN` do not trigger
34
+ further workflows — an [intentional loop guard][loop]. With it, a PR merged by
35
+ the auto-merge workflow would never fire `npm-publish.yml` or
36
+ `deploy-test-reports.yml` on the default branch. A PAT is a real user, so its
37
+ pushes trigger workflows normally.
38
+
39
+ The tradeoff: that token carries whatever access its owner has. Prefer a
40
+ fine-grained token limited to this one repository, and set an expiry you will
41
+ actually notice.
42
+
43
+ [loop]: https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow
44
+
45
+ ## npm: trusted publishing instead of a token
46
+
47
+ Preferred, because there is nothing to rotate. Per package, on npmjs.com:
48
+
49
+ 1. Go to `npmjs.com/package/<name>/access`.
50
+ 2. Under **Trusted publisher**, add: GitHub Actions, this `owner/repo`, workflow
51
+ file `npm-publish.yml`.
52
+ 3. Leave `NPM_TOKEN` **unset** in the repo. The workflow detects the absence and
53
+ authenticates via OIDC — that is what `permissions: id-token: write` is for.
54
+
55
+ A package's *first* publish cannot use trusted publishing (there is no package
56
+ page yet to configure). Publish once by hand — `npm publish --access public` —
57
+ then set the trusted publisher and let CI take over.
58
+
59
+ ## Repository settings, not secrets
60
+
61
+ | Setting | Where | Needed for |
62
+ | --- | --- | --- |
63
+ | Allow auto-merge | Settings → General → Pull Requests | `gh pr merge --auto` in both auto-merge workflows |
64
+ | Branch protection with ≥1 required check | Settings → Branches | Makes `--auto` actually wait for CI instead of merging immediately |
65
+ | Read and write permissions for Actions | Settings → Actions → General → Workflow permissions | The version-bump commit in `npm-publish.yml` |
66
+ | Allow GitHub Actions to create and approve pull requests | Settings → Actions → General | `gh pr create` in `auto-merge-and-create-prs.yml` |
67
+
68
+ ## Rotation
69
+
70
+ - `NPM_TOKEN`: granular tokens expire after **90 days maximum**. The workflow's
71
+ preflight step turns an expired token into a clear error instead of a
72
+ late `E404`, but it still stops the release. Trusted publishing avoids this
73
+ entirely.
74
+ - `GIT_TOKEN`: set an expiry and a calendar reminder. An expired PAT makes the
75
+ auto-merge workflows fail with `gh: Bad credentials`.
76
+ - `CLOUDFLARE_API_TOKEN`: does not expire unless you set a TTL, but scope it to
77
+ Workers Scripts:Edit on one account rather than using a global API key.
@@ -0,0 +1,205 @@
1
+ # The workflows
2
+
3
+ Five files in `.github/workflows`, plus one helper script. Each is described
4
+ here by what it does, what it needs, and how it fails — the failure modes are
5
+ the part that costs hours if you meet them cold.
6
+
7
+ | Workflow | Trigger | Needs |
8
+ | --- | --- | --- |
9
+ | `tests.yml` | PR, push to default branch | `CODECOV_TOKEN` |
10
+ | `npm-publish.yml` | push to default branch | `NPM_TOKEN` *or* trusted publishing |
11
+ | `deploy-test-reports.yml` | push to default branch | `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` |
12
+ | `auto-merge-claude.yml` | PR opened/updated | `GIT_TOKEN`, "Allow auto-merge" |
13
+ | `auto-merge-and-create-prs.yml` | every 12h, manual | `GIT_TOKEN` |
14
+
15
+ ---
16
+
17
+ ## tests.yml
18
+
19
+ Discovers every workspace package with a `test:ci` script, runs each as its own
20
+ matrix job, and uploads results to Codecov Test Analytics and coverage to
21
+ Codecov.
22
+
23
+ **Why the matrix is discovered rather than listed.** A hand-maintained matrix
24
+ goes stale the first time someone adds a package and forgets to edit the
25
+ workflow — the package silently has no CI. A `discover` job emits the list as
26
+ JSON; the `test` job consumes it through `fromJSON`. GitHub cannot compute a
27
+ matrix inside the job that uses it, which is why it is two jobs.
28
+
29
+ **What your package must produce.** Relative to the package directory:
30
+
31
+ ```
32
+ junit.xml test results — what Test Analytics ingests
33
+ coverage/lcov.info coverage — uploaded when the runner can produce it
34
+ ```
35
+
36
+ For Vitest:
37
+
38
+ ```json
39
+ "test:ci": "vitest run --reporter=junit --outputFile=junit.xml --coverage"
40
+ ```
41
+
42
+ **Details that matter:**
43
+
44
+ - `fail-fast: false` — one package's red suite must not cancel the other jobs
45
+ before they upload. Their results are the ones you need to compare against.
46
+ - `if: ${{ !cancelled() }}` on both upload steps, not `if: success()`. Without
47
+ it, the upload is skipped on exactly the runs whose results matter most.
48
+ - `bun install --ignore-scripts` — postinstall hooks in a workspace tend to want
49
+ env vars CI does not have (database URLs, Tauri toolchains, doc generators),
50
+ and none of them affect tests.
51
+ - Tests run through `bunx turbo run test:ci --filter=...` rather than `cd` +
52
+ `bun test`, so a package that imports a sibling through its built `dist` gets
53
+ that sibling built first.
54
+
55
+ **Failure modes:**
56
+
57
+ | Symptom | Cause → fix |
58
+ | --- | --- |
59
+ | A package you added never appears as a job | It has no `test:ci` script, or it lives outside `packages/`, `apps/` |
60
+ | `Error: Unable to process file command 'output'` in `discover` | A package name contains a newline or the JSON exceeded the 1MB output cap |
61
+ | Codecov shows the run but no coverage | The runner wrote no `lcov.info` — `--coverage` missing, or a provider that writes nothing on your Node version |
62
+ | Coverage drops to 0% for an untouched package | `carryforward` is off for that flag in `codecov.yml` |
63
+
64
+ ## npm-publish.yml
65
+
66
+ Publishes every non-private workspace package **whose content changed**, then
67
+ commits the version bumps back.
68
+
69
+ **The rule it exists to enforce:** nobody has to remember a version number. Each
70
+ package is packed with `npm pack --dry-run --json` and its integrity hash
71
+ compared against `npm view <pkg>@<version> dist.integrity`. npm tarballs are
72
+ reproducible (mtimes are normalized), so identical hashes mean nothing to
73
+ release; a different hash means bump the patch and publish.
74
+
75
+ **Credentials — two supported paths:**
76
+
77
+ 1. **Trusted publishing (OIDC), preferred.** No secret at all. npm ≥ 11.5.1
78
+ trades the job's `id-token` for a short-lived publish token. Each package
79
+ must name this repo + workflow as its trusted publisher at
80
+ `npmjs.com/package/<name>/access`. Nothing to rotate.
81
+ 2. **`NPM_TOKEN` secret.** A granular access token with write access. These
82
+ expire after at most 90 days; classic automation tokens no longer work for
83
+ direct publishing.
84
+
85
+ Setting the secret picks (2); leaving it unset picks (1).
86
+
87
+ **Why the credential is checked in its own step, before anything is built.** npm
88
+ answers an *unauthorized* PUT with `E404 ... could not be found or you do not
89
+ have permission to access it` — which reads like a missing package, not a
90
+ permissions problem. Without the preflight you pay a full build for every
91
+ package before the first one hits it, and each failed attempt still signs a
92
+ provenance statement into the public sigstore transparency log.
93
+
94
+ **Details that matter:**
95
+
96
+ - `set +e` in the publish loop. GitHub runs `run:` steps with `bash -e -o
97
+ pipefail`, so *not* writing `set -e` does not disable errexit — the first
98
+ failing package would kill the step and leave every later package unevaluated.
99
+ Outcomes travel through `$rc`; the step still exits non-zero at the end.
100
+ - Exit 43 means "npm rejected the credential". The loop stops attempting further
101
+ packages: they would all fail the same way, after another build and another
102
+ provenance signature each.
103
+ - `workspace:*` dependencies are rewritten to real semver ranges before packing.
104
+ npm keeps the literal protocol in the tarball, and consumers cannot resolve it.
105
+ Only the `version` field of that edit is committed back.
106
+ - A local version *behind* the registry is synced forward first, otherwise the
107
+ publish fails with "Cannot implicitly apply the latest tag".
108
+ - `E409 cannot publish over previously staged version` is retried at the next
109
+ free version, up to five times. `latest` lags versions an interrupted publish
110
+ reserved; `.github/scripts/next-free-version.mjs` reads the full version list
111
+ *and* the release timeline, which is where those numbers appear.
112
+ - The final step rewrites only the `version` field back into each
113
+ `package.json`, by regex rather than by re-serializing the parsed object, so
114
+ the file keeps its own formatting. It matches `"version"\s*:\s*"..."` rather
115
+ than a literal `"version": "x"`, and fails the step if the replace found
116
+ nothing — a package.json written without the space after the colon would
117
+ otherwise silently lose the bump while the log claimed to keep it.
118
+ - The bump commit ends in `[skip ci]` so it does not retrigger the workflow.
119
+ - A no-op `husky` is put on PATH. Some published dependencies ship
120
+ `prepare: "husky install"`; when npm reconciles bun's linked `node_modules` it
121
+ runs that hook and dies with exit 127 — and `--ignore-scripts` does **not**
122
+ suppress it for bun-linked packages.
123
+
124
+ **Failure modes:**
125
+
126
+ | Symptom | Cause → fix |
127
+ | --- | --- |
128
+ | `E404` on publish for a package that exists | The credential cannot write to it. Read the error annotation the workflow emits |
129
+ | `EUNSUPPORTEDPROTOCOL workspace:*` | An `npm` command ran without `--no-workspaces` against bun's symlinks |
130
+ | `vite: not found` during build | Someone replaced `bun install` with `npm install`; npm cannot install a bun workspace |
131
+ | Versions bump on every run | The build is not reproducible — a timestamp or absolute path is landing in `dist` |
132
+ | Nothing publishes, no errors | Every package is `private: true`, or content genuinely did not change |
133
+
134
+ ## deploy-test-reports.yml
135
+
136
+ Runs the whole suite with Vitest's HTML reporter and deploys the result to
137
+ Cloudflare Workers, so the report has a permanent URL.
138
+
139
+ **The two non-obvious parts:**
140
+
141
+ - `continue-on-error: true` on the test step. A red suite must still publish its
142
+ report — that report is how you see what went red.
143
+ - The "Ensure a report exists to deploy" step. What a red suite must *not* do is
144
+ leave `dist` missing: `wrangler deploy` treats an absent `assets.directory` as
145
+ a hard error, so the run would end on a config error instead of on the test
146
+ signal. A placeholder page is written instead.
147
+
148
+ **The reporter gotcha.** Vitest's HTML reporter ignores `outputFile` and writes
149
+ `<outputDir>/index.html` plus a UI bundle, where `outputDir` is a *reporter
150
+ option* defaulting to `.vitest`. A reporter named on the command line
151
+ (`--reporter=html`) is constructed without options and silently keeps that
152
+ default — so the deploy finds nothing. `vitest.config.ts` declares the reporter
153
+ and its destination together, keyed off `VITEST_HTML_REPORT_DIR`; `test:report`
154
+ sets it.
155
+
156
+ ## auto-merge-claude.yml
157
+
158
+ Enables auto-merge on PRs opened by trusted agents and maintainers.
159
+
160
+ `gh pr merge --auto` is the safe path: GitHub holds the merge until branch
161
+ protection is satisfied. It works **only** when both are true:
162
+
163
+ - "Allow auto-merge" is on in Settings → General → Pull Requests, and
164
+ - the base branch has protection with at least one **required** status check.
165
+
166
+ Without both, GitHub rejects `--auto` and the fallback merges immediately —
167
+ without waiting for CI. That is why the actor allowlist in the `if:` matters,
168
+ and why you should turn on branch protection before turning on this workflow.
169
+
170
+ It uses `GIT_TOKEN` (a PAT) rather than `GITHUB_TOKEN` deliberately: merges made
171
+ with `GITHUB_TOKEN` do not trigger further workflows, so the publish and deploy
172
+ runs on the default branch would never fire.
173
+
174
+ Branches named `production`, `prod`, `staging` or `develop` are merged without
175
+ `--delete-branch`.
176
+
177
+ ## auto-merge-and-create-prs.yml
178
+
179
+ A twice-daily sweep that merges PRs already clean/approved/green, and opens a PR
180
+ for any pushed branch that never got one. Both halves are idempotent, so a
181
+ delayed or duplicated run is harmless — and scheduled workflows **are** delayed,
182
+ sometimes by hours, during periods of high Actions load. Never rely on the exact
183
+ minute.
184
+
185
+ A *merged* PR counts when checking whether a branch already has one: reopening a
186
+ PR for a branch whose work already landed creates an empty, permanently open PR.
187
+
188
+ The second step deliberately skips `actions/checkout` — it only needs refs, so
189
+ it fetches them into an empty workspace rather than paying for a full checkout.
190
+
191
+ ---
192
+
193
+ ## Adapting these to another repo
194
+
195
+ - **Different package manager.** Replace `oven-sh/setup-bun@v2` with
196
+ `actions/setup-node@v4` and the `bun install --ignore-scripts` /
197
+ `bunx turbo` lines with `npm ci --ignore-scripts` / `npx turbo`. The publish
198
+ loop's `--no-workspaces` flags exist for bun's symlinks and are harmless
199
+ otherwise.
200
+ - **Not a monorepo.** `tests.yml`'s discovery finds nothing; either add a
201
+ `packages/` layout, or replace the matrix with a single job that runs
202
+ `npm run test:ci` at the root.
203
+ - **`main` instead of `master`.** Every `branches:` filter in this template is
204
+ written by `setup-git-repo` from your repo's actual default branch. If you
205
+ rename the branch later, grep the workflows for the old name.
@@ -0,0 +1,11 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ .next/
5
+ .turbo/
6
+ coverage/
7
+ junit.xml
8
+ *.tsbuildinfo
9
+ .env
10
+ .env.local
11
+ .DS_Store
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "{{REPO}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "{{DESCRIPTION}}",
6
+ "type": "module",
7
+ "packageManager": "bun@1.3.11",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/{{OWNER}}/{{REPO}}"
12
+ },
13
+ "workspaces": [
14
+ "packages/*",
15
+ "apps/*"
16
+ ],
17
+ "scripts": {
18
+ "build": "turbo run build",
19
+ "dev": "turbo run dev",
20
+ "lint": "turbo run lint",
21
+ "typecheck": "turbo run typecheck",
22
+ "test": "turbo run test",
23
+ "test:ci": "turbo run test:ci",
24
+ "test:report": "VITEST_HTML_REPORT_DIR=apps/test-reports/dist vitest run",
25
+ "test:watch": "turbo run test:watch",
26
+ "coverage": "turbo run coverage",
27
+ "clean": "turbo run clean && rm -rf .turbo node_modules"
28
+ },
29
+ "devDependencies": {
30
+ "@vitest/coverage-v8": "^4.1.0",
31
+ "turbo": "^2.10.12",
32
+ "typescript": "^5.9.3",
33
+ "vitest": "^4.1.0"
34
+ }
35
+ }
@@ -0,0 +1 @@
1
+ Placeholder — publishable libraries live here, one directory per package.