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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
name: CI
on:
push:
branches:
pull_request:
schedule:
# The feature powerset, nightly. See the `powerset` job for why it is not
# on the pull-request path.
- cron: "17 4 * * *"
# Every job here checks out the repository, compiles it, and reports through
# the check run the platform creates on its own. None of them writes an issue,
# a comment, a release, or a package, so the token they are handed should not
# be able to. Declared once at the top rather than nine times: a job added
# later inherits the restriction instead of inheriting the default, which is
# the failure mode this block exists to prevent.
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
# `rust-toolchain.toml` pins `stable`, and a toolchain file beats the
# toolchain `dtolnay/rust-toolchain` installs. Without an override every leg
# of the matrix below would build with stable and the MSRV leg would prove
# nothing. `RUSTUP_TOOLCHAIN` outranks the file, so each job sets it.
RUSTUP_TOOLCHAIN: stable
jobs:
test:
name: Test (${{ matrix.rust }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust:
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: arcature_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/arcature_test
RUSTUP_TOOLCHAIN: ${{ matrix.rust }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: ${{ matrix.rust }}
components: clippy, rustfmt
- name: Check formatting
run: cargo fmt --all -- --check
- name: Clippy (default features)
run: cargo clippy --all-targets
- name: Build (default features)
run: cargo build
- name: Test (default features)
run: cargo test
- name: Build (no default features, kernel only)
run: cargo build --no-default-features
- name: Build (fullstack)
run: cargo build --features fullstack
# Everything that does not touch a database. `jobs` and `cli` are absent
# on purpose and are not oversights: `jobs` pulls `database` directly and
# `cli` pulls it through `uag -> dx -> jobs`, so naming either here makes
# this a database build with no driver, which is the one thing
# `src/database/mod.rs` refuses to compile. `just drivers` covers them.
- name: Build (no database)
run: cargo build --no-default-features --features "macros,inertia,auth,validation,cache,storage-fs,mail,events,api,observe,pages,realtime,templates"
drivers:
name: Full features, per driver
runs-on: ubuntu-latest
# The honest equivalent of `--all-features`, which cannot work here: a
# build speaks exactly one SQL dialect, so "all features" is three builds
# and not one. Anything that hard-codes a driver type, or writes SQL only
# PostgreSQL parses, fails here and nowhere else -- the `features` job pins
# PostgreSQL and would never see it. Mirrors `just drivers`.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: stable
- name: Build the full feature set once per driver
run: |
feats=api,api-docs,auth,cache,cli,database,dev-proxy,dx,events,inertia,jobs,macros,mail
feats=$feats,oauth,observe,otel,pages,realtime,storage-fs,storage-s3,templates,test-kit,uag,validation
for driver in db-postgres db-sqlite db-mysql; do
echo "== $driver =="
cargo check --no-default-features --features "$feats,$driver" --all-targets
done
features:
name: Each feature alone
runs-on: ubuntu-latest
# The flags are not tuning. They are the compile-time invariant in
# `src/database/mod.rs` written out as cargo-hack arguments:
#
# --features db-postgres six features (`database`, `jobs`, `dx`,
# `uag`, `cli`, `api-docs`) pull `database`
# without naming a driver, so on their own
# they hit the "needs a driver" error. The
# driver is a build-wide choice like a target,
# not something each feature opts into.
# --skip db-sqlite,db-mysql otherwise cargo-hack adds a second driver on
# top of the pinned one. The `drivers` job
# gives the other two a build of their own.
# --exclude-all-features `--all-features` is all three drivers at
# once, which never compiles.
#
# `--no-dev-deps` keeps this to the library: a test file that forgets its
# own `#![cfg(feature = ...)]` is a test-file bug, not a library one.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: stable
- uses: taiki-e/install-action@a2a5f6e99e1a31540baa0468acfa302cff0f359f # v2.86.4
with:
tool: cargo-hack
- name: Every feature on its own
run: cargo hack check --each-feature --no-dev-deps --features db-postgres --skip db-sqlite,db-mysql --exclude-all-features
powerset:
name: Feature powerset (nightly)
runs-on: ubuntu-latest
if: github.event_name == 'schedule'
# Not on the pull-request path. The crate has 29 features, so an uncapped
# powerset is 292,672 builds -- not slow, unrunnable. `--depth 2` is 263,
# and pairwise is where feature-interaction bugs actually live: a feature
# that breaks alone is caught by the `features` job, and one that breaks
# only in a specific trio is rare enough not to be worth three orders of
# magnitude. Even 263 is too much to put in front of every pull request,
# so it runs once a night. Raise the depth to 3 (1,599 builds) when
# chasing one.
#
# `--skip database` on top of the pinned driver: `db-postgres` already
# enables it, so including it again only doubles the subsets.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: stable
- uses: taiki-e/install-action@a2a5f6e99e1a31540baa0468acfa302cff0f359f # v2.86.4
with:
tool: cargo-hack
- name: All feature pairs
run: cargo hack build --feature-powerset --depth 2 --features db-postgres --skip database,db-sqlite,db-mysql --exclude-all-features --keep-going
scaffold:
name: Scaffold and compile
runs-on: ubuntu-latest
# What `arc new` writes has to compile. There used to be four application
# packages under `examples/` making this point by existing; they were empty,
# so they made it only in the manifest comment. Generating the tree here
# instead means the layout under test is the one the templates produce, and
# there is no second copy to keep in sync.
# Three stacks by three drivers. The driver axis is not padding: it picks a
# different set of `arcature` features and a different migration dialect,
# and a template that only ever compiled against SQLite is a template that
# breaks on someone's first `--db postgres`.
strategy:
fail-fast: false
matrix:
stack:
db:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: stable
- name: Build the CLI
run: cargo build --bin arc --features cli
- name: Scaffold
run: ./target/debug/arc new app --stack ${{ matrix.stack }} --db ${{ matrix.db }}
# `arc new` writes a normal `arcature = "<version>"` dependency, which is
# correct and will resolve the moment the crate is published. It is not
# published yet, so
# until then CI has to point the generated project at the working tree it
# was generated from -- otherwise this job would only ever test the last
# release, which is the one thing it is not here to test.
#
# Delete this step after the first `cargo publish`.
- name: Point the generated project at this checkout
run: |
{
echo ""
echo "[patch.crates-io]"
echo 'arcature = { path = ".." }'
} >> app/Cargo.toml
- name: Compile it
working-directory: app
run: cargo build
# Only on the SQLite leg. The scaffold's smoke test boots the application,
# and on the other two drivers that means connecting to a server this job
# does not run. Compiling is the gate for those; SQLite is where the test
# suite the template ships actually gets executed.
- name: Its own test suite
if: matrix.db == 'sqlite'
working-directory: app
run: cargo test
deny:
name: Licences and advisories
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1
book:
name: Guide
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: taiki-e/install-action@a2a5f6e99e1a31540baa0468acfa302cff0f359f # v2.86.4
with:
tool: mdbook
- name: Build the guide
run: mdbook build docs
publish-dry-run:
name: cargo publish --dry-run
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: "1.97.1"
# `arcature-macros` first, and unconditionally. It depends on nothing this
# workspace publishes, so its dry run is meaningful today, and it is the
# crate that has to go out first anyway.
- name: cargo publish --dry-run -p arcature-macros
run: cargo publish --dry-run -p arcature-macros
env:
RUSTUP_TOOLCHAIN: "1.97.1"
# `arcature` cannot be dry-run yet, and that failure says nothing about
# this crate. Packaging resolves `arcature-macros = { version = "=0.1.0",
# path = "macros" }` against the registry -- `--no-verify` skips the
# build, not the resolve -- so before the first publish the step can only
# report
#
# no matching package named `arcature-macros` found
#
# This is what turned every run on `main` red. A required job that cannot
# pass would hold `CI success` permanently red and make the gate
# meaningless, so the step asks the sparse index whether macros exists and
# runs only when the answer is yes. It begins working on its own the
# moment the first publish lands -- nothing here needs revisiting then.
- name: cargo publish --dry-run -p arcature
run: |
if curl --proto '=https' --tlsv1.2 --silent --show-error --fail \
--location https://index.crates.io/ar/ca/arcature-macros \
--output /dev/null; then
cargo publish --dry-run -p arcature
else
echo "::notice title=arcature dry run skipped::arcature-macros is not on crates.io, so packaging arcature would fail on the registry resolve rather than on anything in this tree."
fi
env:
RUSTUP_TOOLCHAIN: "1.97.1"
# ---------------------------------------------------------------------------
# The one check branch protection requires.
#
# `main` requires exactly one status check: this job. Listing the real jobs
# individually would work today and rot tomorrow -- `test` and `scaffold` are
# matrices, so their check names carry their matrix values (`Test (stable)`,
# `Scaffold and compile (react, sqlite)`), and adding a stack, a driver or a
# toolchain silently adds a check that protection does not require. A leg that
# nobody requires is a leg that can go red without blocking a merge.
#
# `if: always()` is what makes this a gate rather than a formality. Without
# it the job is skipped when anything it needs fails, and a skipped required
# check reads to the merge button as "not failing".
#
# `powerset` is deliberately absent: it only runs on the schedule, so
# requiring it would leave every pull request waiting on a job that never
# starts.
ci-success:
name: CI success
if: always()
needs:
- test
- drivers
- features
- scaffold
- deny
- book
- publish-dry-run
runs-on: ubuntu-latest
steps:
- name: Fail unless every required job succeeded
# `cancelled` and `skipped` are failures here for the same reason
# `always()` is above: neither one is a green build, and treating
# either as passing hands the merge button a result nobody produced.
if: >-
contains(needs.*.result, 'failure')
|| contains(needs.*.result, 'cancelled')
|| contains(needs.*.result, 'skipped')
run: |
echo "One or more required jobs did not succeed."
exit 1
- name: Report
run: echo "Every required job succeeded."