Repository navigation
246 lines (233 loc) · 15.7 KB
/
Copy pathci.yml
File metadata and controls
246 lines (233 loc) · 15.7 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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
name: CI
# One workflow for the whole workspace. Every check runs its task across the workspace through turbo from the repository root, rather than against a single package directory, so adding a package needs no edit here.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
jobs:
commitlint:
name: Commitlint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
standalone: true
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .tool-versions
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Validate the last commit with commitlint
if: github.event_name == 'push'
run: pnpm exec commitlint --last --verbose
- name: Validate every PR commit with commitlint
if: github.event_name == 'pull_request'
run: pnpm exec commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-workspace
with:
task: lint
command: pnpm lint
typecheck:
name: Typecheck
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-workspace
with:
task: typecheck
# _typecheck:attw (attw --pack per published package, checking each package's declared types resolve under every module resolution mode) is one of the tasks `pnpm typecheck` runs through turbo, depending on that package's own _build. No separate build step is needed to give it a dist/ to inspect.
command: pnpm typecheck
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-workspace
with:
task: test
# The SQLite wasm store's integration tests run in Chromium through Vitest's browser mode, and the web app's end-to-end tests in the same Chromium through Playwright, which pins the same version; Playwright installs the browser build that version expects.
command: pnpm --filter @exadev/process-graph-sqlite-wasm exec playwright install --with-deps chromium && pnpm test:coverage && pnpm test:integration && pnpm test:smoke && pnpm test:e2e
knip:
name: Knip
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-workspace
with:
task: knip
# Building first matters: without a real dist/ for every workspace package to resolve types against, knip cannot see a dependency that is only reachable through another package's declarations and reports it as unused.
command: pnpm build && pnpm exec knip
dependency-versions:
name: Dependency versions
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-workspace
with:
task: deps-lint
command: pnpm run deps:lint
actionlint:
name: Actionlint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# actionlint and the shellcheck it runs on every `run:` script, each at an exact version and each archive checked against the SHA-256 committed here before it is unpacked. The digests were checked against actionlint's release checksums file and against the asset digests GitHub records for both releases. shellcheck is installed explicitly so the script checks never depend on what the runner image happens to carry.
- name: Install actionlint and shellcheck
env:
ACTIONLINT_VERSION: 1.7.12
SHELLCHECK_VERSION: 0.11.0
run: |
case "$RUNNER_ARCH" in
X64)
actionlint_arch=amd64 actionlint_sha256=8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
shellcheck_arch=x86_64 shellcheck_sha256=b7af85e41cc99489dcc21d66c6d5f3685138f06d34651e6d34b42ec6d54fe6f6
;;
*)
echo "::error::No pinned actionlint or shellcheck build for runner architecture $RUNNER_ARCH"
exit 1
;;
esac
tools="$RUNNER_TEMP/workflow-linters"
mkdir -p "$tools"
cd "$tools"
actionlint_archive="actionlint_${ACTIONLINT_VERSION}_linux_${actionlint_arch}.tar.gz"
shellcheck_archive="shellcheck-v${SHELLCHECK_VERSION}.linux.${shellcheck_arch}.tar.gz"
curl -fsSLo "$actionlint_archive" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/${actionlint_archive}"
curl -fsSLo "$shellcheck_archive" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/${shellcheck_archive}"
printf '%s %s\n' "$actionlint_sha256" "$actionlint_archive" "$shellcheck_sha256" "$shellcheck_archive" | sha256sum --check --strict
tar -xzf "$actionlint_archive" actionlint
tar -xzf "$shellcheck_archive" --strip-components=1 "shellcheck-v${SHELLCHECK_VERSION}/shellcheck"
echo "$tools" >> "$GITHUB_PATH"
# Every workflow the repository tracks, wherever it sits.
- name: Lint every workflow
run: git ls-files -z -- ':(glob)**/.github/workflows/*.yml' ':(glob)**/.github/workflows/*.yaml' | xargs -0 actionlint
zizmor:
name: Zizmor
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# uvx rather than zizmorcore/zizmor-action: the action runs zizmor's container image, which needs a Docker daemon. setup-uv checks the uv download against its published checksum, and uv fetches a Python for zizmor's environment if the runner has none.
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: 0.12.19
enable-cache: false
# Offline, so the result never depends on whether a token happens to be in the environment: the online audits (impostor commits, known-vulnerable actions, ref confusion) only run with one. Every workflow and composite action in the repository is audited.
- name: Audit every workflow and action
run: uvx zizmor@1.30.1 --offline --config .github/zizmor.yml --no-progress .
release:
name: Release
needs:
[
commitlint,
lint,
typecheck,
test,
knip,
dependency-versions,
actionlint,
zizmor,
]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
# Two pushes to main close together must never release at the same time: the orchestrator pushes its release commit and tags to main, and semantic-release refuses to publish the moment the remote holds a commit this checkout does not, so a concurrent run's pushes would make every package after that point report "no release". `cancel-in-progress: false` queues the second run behind the first, because cancelling mid-release would leave published packages without their dependents' bumps.
concurrency:
group: release-workspace
cancel-in-progress: false
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: write # to create a GitHub Release per package; the release commit and tags are pushed with the deploy key, not this token
issues: write # to comment on released issues
pull-requests: write # to comment on released pull requests
id-token: write # OIDC identity for npm trusted publishing (no NPM_TOKEN)
steps:
# The default GITHUB_TOKEN cannot bypass a repository ruleset (GitHub does not support naming the github-actions[bot] identity as a bypass actor), so the orchestrator's release commit and tag push needs its own bypass identity. A deploy key with write access, listed as a DeployKey bypass actor on the main ruleset, is the lightest one: repository-scoped, with no GitHub App installation and no personal or organisation-admin credential. Because the key can push straight to main, it exists on the runner only for the last two steps: checkout does not receive it, so it is not in git config or on disk while the setup actions, the install and the npm upgrade run. That is the whole gain. Those earlier steps can still change PATH, the environment or installed code (node_modules, global npm) that later runs with the key present, so this narrows exposure to code that merely reads the secret when it runs and does not isolate the key from a compromised earlier step. Full isolation needs the install separated from the credential.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# ref: main, not the bare event SHA: the orchestrator pushes to the current branch by name, so a detached HEAD stops the run with a WorkspaceStateError rather than pushing HEAD:HEAD. fetch-depth: 0 because each package's release is analysed from its own last matching tag.
ref: main
fetch-depth: 0
# The deploy key, not this job's token, is the push credential, so nothing is left in git config for later steps. @semantic-release/github calls the GitHub API with GITHUB_TOKEN from the Release step's env and needs no git credential.
persist-credentials: false
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
standalone: true
# Deliberately no `registry-url`: setting it writes an .npmrc `_authToken` line that wins over the OIDC exchange, so the input that looks like it configures the registry is the one that breaks trusted publishing.
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .tool-versions
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Upgrade npm for OIDC trusted publishing (needs npm CLI >=11.5.1)
# Pinned, not npm@latest: a moved dist-tag must not run new code in a job that goes on to hold a push credential. 11.5.1 is the first release with trusted publishing (OIDC); the pin is the newest release at least a week old when it was chosen, matching the install age gate, and its Node engine range includes the version in .tool-versions. Raise it deliberately.
# zizmor's adhoc-packages audit flags any install outside a lockfile; this one is a single exact version, chosen and raised deliberately as the comment above says, so it is ignored here rather than in the whole repository.
run: npm install -g npm@12.1.0 # zizmor: ignore[adhoc-packages]
# The last step before Release and the only place the key is provisioned: written to the runner's temp directory with mode 600 from the step's env (never a command line), together with GitHub's ed25519 host key, pinned so the connection is verified rather than trusted on first use (fingerprint SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU, as published at https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints). The step then proves the key authenticates with write access to the repository, so a missing or wrong key fails here, before the orchestrator starts. The dry run targets a ref name that does not exist, so it can never be rejected as non-fast-forward when main has moved on since checkout. GitHub does not evaluate rulesets on a dry run, so this does not prove the key is a bypass actor. BASH_ENV is blanked and ssh and git are called by absolute path so that a variable or PATH entry set by an earlier step does not redirect them.
- name: Provision the deploy key for the Release step
env:
RELEASE_DEPLOY_KEY: ${{ secrets.RELEASE_DEPLOY_KEY }}
# Blanked so a BASH_ENV exported by an earlier step cannot run in the shell that holds the key.
BASH_ENV: ""
run: |
install -d -m 700 "$RUNNER_TEMP/release-ssh"
(umask 077 && printf '%s\n' "${RELEASE_DEPLOY_KEY%$'\n'}" > "$RUNNER_TEMP/release-ssh/id")
printf '%s\n' 'github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl' > "$RUNNER_TEMP/release-ssh/known_hosts"
# Must match the GIT_SSH_COMMAND of the Release step.
GIT_SSH_COMMAND="/usr/bin/ssh -i $RUNNER_TEMP/release-ssh/id -o IdentitiesOnly=yes -o UserKnownHostsFile=$RUNNER_TEMP/release-ssh/known_hosts -o StrictHostKeyChecking=yes" \
/usr/bin/git push --dry-run --no-verify "git@github.com:$GITHUB_REPOSITORY.git" "HEAD:refs/heads/release-key-probe-$GITHUB_RUN_ID"
- name: Release every package that changed, in dependency order
# One orchestrator run discovers the workspace from pnpm-workspace.yaml, orders the packages topologically and runs semantic-release per package, with commits path-filtered to that package's own directory and tags in `name@version` form. A package whose only change is a sibling's release still gets a patch release, because its published dependency range changed. HUSKY=0 so local git hooks never fire against the automated commits.
run: HUSKY=0 pnpm release
env:
# Used by @semantic-release/github for the GitHub API (releases, issue and pull request comments), never for git.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# The orchestrator pushes with plain `git push --atomic origin HEAD:<branch> <tags>`, and the checkout's origin is the https URL. These three variables are git's own per-process config (GIT_CONFIG_COUNT and its numbered KEY and VALUE pairs): they rewrite every https GitHub URL to its SSH form for this step alone, so that push, and the fetches and ls-remote calls around it, authenticate with the deploy key through GIT_SSH_COMMAND, and nothing is written to the repository's config.
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: url.git@github.com:.insteadOf
GIT_CONFIG_VALUE_0: https://github.com/
# Must match the GIT_SSH_COMMAND of the provisioning step.
GIT_SSH_COMMAND: /usr/bin/ssh -i ${{ runner.temp }}/release-ssh/id -o IdentitiesOnly=yes -o UserKnownHostsFile=${{ runner.temp }}/release-ssh/known_hosts -o StrictHostKeyChecking=yes
# Provenance for every package the run publishes, matching each package.json's own publishConfig.provenance.
NPM_CONFIG_PROVENANCE: "true"
# Blanked, not omitted: an inherited NPM_TOKEN or NODE_AUTH_TOKEN from a workflow-level env block, reusable workflow or composite action would otherwise be used in preference to the OIDC exchange.
NPM_TOKEN: ""
NODE_AUTH_TOKEN: ""
- name: Remove the deploy key
if: always()
run: rm -rf "$RUNNER_TEMP/release-ssh"