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
name: Lint
# Mirror the local pre-push hook's format and lint gates (`cargo fmt --check`
# and `cargo clippy`) in CI, so style and lint regressions are caught on every
# PR even when a contributor hasn't installed the git hooks. HEAD has
# historically drifted out of clippy/fmt compliance between hook runs; this gate
# keeps `main` green against the same standard the hook enforces locally.
#
# fmt and clippy run as separate jobs so a formatting failure and a lint failure
# are reported independently. The build relies on the committed `prebuilt.html`
# (build.rs falls back to it when trunk is absent), so no trunk step is needed
# just to type-check and lint. The clippy job runs `--workspace` so the `ui`
# member crate is linted too; `ui`'s deps (yew/gloo/web-sys) build fine on the
# host target for clippy's purposes.
#
# That host-target run alone is not enough, though (see #412): `ui` is a
# Yew/WASM crate that only ever actually ships compiled for
# `wasm32-unknown-unknown` (via `trunk build` in publish.yml), and clippy's
# lint set — and even plain compilability — can differ between the host
# target and wasm32 (cfg-gated code paths, wasm-bindgen/web-sys API surface,
# etc.). Before this, nothing on a PR ever built or linted `ui` against the
# target it's actually shipped to; a wasm32-only compile error or clippy
# violation would sail through every PR green and only surface when cutting a
# release. The `clippy-ui-wasm` job below closes that gap by installing the
# wasm32 target and running clippy and a compile check against it directly,
# scoped to the `ui` package.
#
# The pre-push hook's file-size gate (`linecheck --max-lines 500` over
# `src/` and `ui/src/`) has no CI equivalent, so it's a no-op for any
# contributor who hasn't opted into `git config core.hooksPath .githooks`
# (see CONTRIBUTING.md) — a PR could silently blow past the 500-line
# convention and still go fully green. The `linecheck` job below closes
# that gap the same way this file already does for fmt/clippy.
#
# `make readme-check` (scripts/check-readme-blocks.sh) validates every
# ```json fence in README.md with `jq` but isn't run by any git hook either,
# so it was previously exercised only when a contributor remembered to run
# it by hand. The `readme-check` job below runs it on every PR so a
# README example can't silently drift into invalid JSON.
#
# `client/` isn't a Cargo workspace member, so none of the cargo-based jobs
# above ever touch it. The `client-lint` job mirrors the pre-push hook's
# `pnpm --filter client typecheck`/`lint` steps in CI, same reasoning as
# fmt/clippy: a contributor without the git hooks installed shouldn't be able
# to merge a `client/` typecheck or lint regression.
on:
pull_request:
push:
branches:
workflow_call:
inputs:
ref:
description: >
Git ref to check out. Empty (the default) means "use the ref that
triggered this run" — set only by changeset-release.yml, which
calls this workflow directly to lint a branch (changeset-release/main)
other than the one that triggered it.
required: false
type: string
# Restrict the default GITHUB_TOKEN to read-only; this workflow only needs to
# check out the repo to run cargo fmt/clippy.
permissions:
contents: read
# Cancel a stale run when a PR gets a new push, so superseded lint runs don't
# pile up and burn CI minutes. Scoped per-workflow-per-ref so unrelated PRs
# (and the push-to-main trigger) never cancel each other. Uses a literal
# "lint" prefix rather than ${{ github.workflow }}: when called via
# workflow_call (see cut-release.yml), github.workflow resolves to the
# CALLER's name, so lint and test would otherwise share one group with each
# other (and with the caller) and race-cancel one another. inputs.ref falls
# back to github.ref for the normal pull_request/push triggers, where
# inputs.ref is unset.
concurrency:
group: lint-${{ inputs.ref || github.ref }}
cancel-in-progress: true
jobs:
fmt:
name: rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref }}
- name: Install Rust (with rustfmt)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
components: rustfmt
- name: Check formatting
run: cargo fmt --check
clippy:
name: clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref }}
- name: Install Rust (with clippy)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
components: clippy
- name: Cache cargo registry and target/
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
# Scoped so the `clippy` job's --all-targets target/ artifacts
# don't clobber the `test` job's cache entry.
shared-key: clippy
- name: Run clippy
# Match the local pre-push hook exactly: this is a non-virtual
# workspace (the root Cargo.toml declares both a [package] and a
# [workspace]), so a bare `cargo clippy` only checks the root
# `moadim` package and silently skips the `ui` member crate. Without
# `--workspace`, `ui/Cargo.toml`'s `[lints.clippy] all = "deny"` was
# never enforced here, so a PR could lint-regress the dashboard and
# still go fully green.
run: cargo clippy --workspace --all-targets -- -D warnings
clippy-ui-wasm:
name: clippy (ui, wasm32)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref }}
- name: Install Rust (with clippy, wasm32 target)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
components: clippy
targets: wasm32-unknown-unknown
- name: Cache cargo registry and target/
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
# Own cache entry: this job's target/ holds wasm32 artifacts only,
# distinct from the host-target artifacts the `clippy` and `test`
# jobs cache under their own shared-keys.
shared-key: clippy-ui-wasm
- name: Run clippy (ui, wasm32-unknown-unknown)
# See #412: `ui`'s own `[lints.clippy] all = "deny"` (ui/Cargo.toml)
# is otherwise never enforced against the target it actually ships
# for. Scoped to `-p ui` (not `--workspace`) since the root `moadim`
# package doesn't build for wasm32 at all.
run: cargo clippy -p ui --target wasm32-unknown-unknown --all-targets -- -D warnings
- name: Build ui for wasm32-unknown-unknown
# Belt-and-suspenders compile check alongside the clippy run above:
# a plain `cargo build` catches the case where `--all-targets` clippy
# (which also covers tests/benches) diverges from what actually needs
# to compile for the shipped artifact.
run: cargo build -p ui --target wasm32-unknown-unknown
linecheck:
name: linecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref }}
- name: Install Rust
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Cache cargo registry and target/
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: linecheck
- name: Install linecheck
run: cargo install linecheck --locked
- name: Check .rs files do not exceed 500 lines
# Match the local pre-push hook exactly (same paths, same limit).
run: |
# shellcheck disable=SC2046
linecheck --max-lines 500 $(find src ui/src -name '*.rs')
readme-check:
name: readme-check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref }}
- name: Validate README's fenced json blocks
# jq ships preinstalled on the ubuntu-latest runner image, so no
# setup step is needed beyond checkout.
run: make readme-check
client-lint:
name: client (typecheck + lint)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref }}
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm --filter client typecheck
- name: Lint
run: pnpm --filter client lint