-
Notifications
You must be signed in to change notification settings - Fork 1
203 lines (175 loc) · 7.85 KB
/
Copy pathci.yml
File metadata and controls
203 lines (175 loc) · 7.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
name: CI
# Classic OSS model: one `main` trunk, contributors fork → PR → `main`, and a release is a push to `main`
# whose pipeline goes green — the `release` job publishes what npm doesn't have yet and only THEN creates
# the `v<version>` tag. The tag is the RESULT of a release, never its trigger: nothing runs on a tag push.
# pull_request → main the gate: build + type-check + lint + tests + smoke (forks run without secrets)
# push → main the same gate, then publish + tag (dist-tag from the version)
# `main` IS gated on push, because a push to `main` is the release path — it must never publish bytes that
# weren't just built and tested. Run the gate on a branch ad-hoc with workflow_dispatch.
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
id-token: write # OIDC: npm Trusted Publisher (provenance) + tokenless Codecov
concurrency:
group: ci-${{ github.ref }}
# Only PR runs are cancelled by a newer push. A run on `main` may end in a publish — never cancel it, and
# let the next one queue behind it so two releases can't interleave.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup Node.js 22
uses: actions/setup-node@v6
with:
node-version: 22
- name: Setup Bun latest
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Type check (TypeScript 7)
run: bun run types
- name: Type check (TypeScript 6)
run: bun run types:6
- name: Lint (eslint)
run: bun run lint
- name: Source tests
run: bun test ./src --coverage --coverage-reporter=text --coverage-reporter=lcov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v7
with:
files: ./coverage/lcov.info
slug: 1gr14/error0
use_oidc: true # tokenless: GitHub OIDC + the Codecov GitHub App (no stored token)
fail_ci_if_error: false # coverage is informational for now — never fail CI on it
- name: Build
run: bun run build
- name: Check published package (publint + are-the-types-wrong)
run: bun run check:package
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: build-files
path: |
dist/
scripts/
package.json
retention-days: 1
smoke:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
node:
- 20
- 22
- 24
steps:
- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- name: Download build artifacts
uses: actions/download-artifact@v8
with:
name: build-files
path: .
- name: Smoke test built package
run: node scripts/smoke.mjs
# Release — the last job of a green push to `main`, and the ONLY publish path (never from a PR or a
# branch push; a fork can't push to `main`, so untrusted code can never publish). It asks npm whether
# package.json's version is already there — if not, it publishes (bumps happen locally via
# `bun run release`) and only THEN creates and pushes the annotated `v<version>` tag, at the exact commit
# that was just built and tested. dist-tag is derived from the version (prerelease x.y.z-next.N → next,
# stable x.y.z → latest). Auth is npm OIDC Trusted Publisher (→ provenance), no NPM_TOKEN — the workflow
# filename (ci.yml) must match the trusted-publisher config on npm, which is why publishing stays here.
release:
needs: [build, smoke]
runs-on: ubuntu-latest
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
permissions:
contents: write # push the release tag
id-token: write # OIDC: npm Trusted Publisher (provenance)
# Serialized across runs: two release commits pushed back to back must publish in order, or an older run
# could re-point `latest` at the older version. Never cancelled mid-publish.
concurrency:
group: release
cancel-in-progress: false
steps:
# Default `persist-credentials: true` — the tag push at the end uses that GITHUB_TOKEN.
- name: Checkout code
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
registry-url: https://registry.npmjs.org
scope: '@1gr14'
# Pinned to 11.x: `npm@latest` is 12.x now, and installing it over the npm that Node 24 bundles leaves a
# global tree without `sigstore`, so the provenance publish dies with MODULE_NOT_FOUND.
- name: Upgrade npm (OIDC Trusted Publisher needs npm >= 11.5.1)
run: npm install -g npm@11
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Download build artifacts
uses: actions/download-artifact@v8
with:
name: build-files
path: .
- name: Install dependencies
run: bun install --frozen-lockfile
# Is this version already released? `npm view` exits non-zero on E404 — for an unknown version AND for
# a package that was never published at all (the very first release), and both mean "go". Version
# 0.0.0 is the never-released sentinel: blank0 (the template this file is copied from) sits there
# forever, and a freshly scaffolded library does too until its first `bun run release` — neither may
# publish or tag anything.
- id: state
name: Is this version already published?
run: |
name=$(node -p "require('./package.json').name")
version=$(node -p "require('./package.json').version")
echo "version=$version" >> "$GITHUB_OUTPUT"
if [ "$version" = "0.0.0" ]; then
echo "release=false" >> "$GITHUB_OUTPUT"
echo "$name is at 0.0.0 — the never-released sentinel. Nothing to publish, nothing to tag."
exit 0
fi
echo "release=true" >> "$GITHUB_OUTPUT"
if npm view "$name@$version" version >/dev/null 2>&1; then
echo "published=true" >> "$GITHUB_OUTPUT"
echo "$name@$version is already on npm — nothing to publish."
else
echo "published=false" >> "$GITHUB_OUTPUT"
echo "$name@$version is not on npm — publishing."
fi
# Idempotent anyway (publish.ts re-asks npm itself); the gate above just keeps an ordinary push quiet.
- name: Publish package
if: ${{ steps.state.outputs.release == 'true' && steps.state.outputs.published == 'false' }}
run: bun run publish:packages
# The tag lands last: after a green pipeline and a successful publish, at the exact commit that was
# built, tested and shipped. Not gated on `published`, so a run that published but died before tagging
# heals on re-run; an existing remote tag is left alone — tags are immutable here, never moved.
- name: Tag the release
if: ${{ steps.state.outputs.release == 'true' }}
run: |
tag="v${{ steps.state.outputs.version }}"
if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; then
echo "tag $tag already exists on origin — leaving it alone."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$tag" -m "$tag" "$GITHUB_SHA"
git push origin "refs/tags/$tag"
echo "tagged $tag at $GITHUB_SHA"