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.
- package/README.md +105 -0
- package/bin/setup-git-repo.js +289 -0
- package/package.json +48 -0
- package/src/git.mjs +74 -0
- package/src/resolve-template.mjs +44 -0
- package/src/template.mjs +142 -0
- package/template/.github/scripts/next-free-version.mjs +73 -0
- package/template/.github/workflows/auto-merge-and-create-prs.yml +96 -0
- package/template/.github/workflows/auto-merge-claude.yml +50 -0
- package/template/.github/workflows/deploy-test-reports.yml +85 -0
- package/template/.github/workflows/npm-publish.yml +394 -0
- package/template/.github/workflows/tests.yml +116 -0
- package/template/README.md +89 -0
- package/template/apps/test-reports/package.json +14 -0
- package/template/apps/test-reports/wrangler.jsonc +10 -0
- package/template/codecov.yml +50 -0
- package/template/docs/BADGES.md +271 -0
- package/template/docs/SECRETS.md +77 -0
- package/template/docs/WORKFLOWS.md +205 -0
- package/template/gitignore +11 -0
- package/template/package.json +35 -0
- package/template/packages/README.md +1 -0
- package/template/turbo.json +42 -0
- package/template/vitest.config.ts +54 -0
|
@@ -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,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.
|