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
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, so no wasm32 target install is required
# here either.
#
# 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
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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.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