-
Notifications
You must be signed in to change notification settings - Fork 0
343 lines (331 loc) · 15.1 KB
/
Copy pathci.yml
File metadata and controls
343 lines (331 loc) · 15.1 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
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
name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
# The compiler is not installed by any step here. rust-toolchain.toml
# pins the version and names the components, and the rustup every
# hosted image ships installs both on the first cargo command. A step
# that installed a toolchain would be a second place the version is
# written, and the pin exists so there is only one.
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: cargo fmt --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: Swatinem/rust-cache@v2
# The library target and not --all-targets. This crate is a
# cdylib whose whole surface is reached through napi's
# registration, which a test harness build does not link, so
# every exported item reads as dead code there and the lints
# that matter are drowned by six that cannot be true. The tests
# are JavaScript, and the job below is what runs them.
- run: cargo clippy --all-features -- -D warnings
# The package as a resolver sees it, which no amount of running it
# here can answer: the tests import the checkout, where every path
# exists and every condition is beside the last. What breaks a
# published package is the resolution instead, and only tsc and attw
# read a package that way.
#
# The same job holds the API gate, because the gate needs the build
# this job already paid for. It is two links of one chain. The first
# asks whether binding.d.cts still says what the Rust says, since that
# file is generated and a committed generated file is a file that can
# go stale: change a signature, forget to rebuild, and the types ship
# describing a function that no longer exists. The second asks whether
# etc/zudb.api.md still says what binding.d.cts says, so that a change
# to what this package exports arrives as a diff in one small file
# somebody has to read rather than as a line buried in six hundred
# generated ones.
#
# What the report leaves out, said out loud: api-extractor reads
# declarations rather than the checker's merged view, so it does not
# follow the `declare module './binding.cjs'` augmentation in
# zudb.d.cts, and `stream` and `Symbol.asyncDispose` are missing from
# Connection there. Those two are held from the other side, by the
# fixtures under test/types that call both and are compiled by
# check:types under the settings a user's compiler would use.
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- uses: Swatinem/rust-cache@v2
- run: npm ci
# The declarations are generated by the build, so there is nothing
# to type-check until it has run.
- run: npm run build:debug
- name: The generated declarations, still generated from this Rust
run: git diff --exit-code -- binding.d.cts
- run: npm run check:types
- run: npm run check:package
# The gate is validated the only way a gate can be: a change to
# the surface has to fail it. Renaming an exported function is the
# cheapest change that is unambiguously one, and the tree is put
# back before anything else looks at it.
- name: A surface change the gate is meant to catch, caught
run: |
sed -i 's/export declare function version(): string/export declare function versionRenamed(): string/' binding.d.cts
set +e
npm run check:api
rc=$?
set -e
git checkout -- binding.d.cts
test $rc -ne 0 || { echo "the api gate did not fire on a renamed export"; exit 1; }
- run: npm run check:api
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
# 24 is the active LTS and the version the package requires. 26
# is the current release and the one where Temporal is
# unflagged, so the temporal tests run there without anything
# being asked for. Bun and Deno join this matrix with the line
# of DX3 that claims them, since claiming a runtime nothing runs
# on is how a client acquires a broken runtime.
node: [24, 26]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
- uses: Swatinem/rust-cache@v2
- run: npm ci
# Debug, because what this job is asking is whether the binding
# is correct rather than how fast it is, and a release build of
# the engine is several minutes of LTO per row of the matrix.
- run: npm run build:debug
- run: npm test
# The same suite again with Temporal turned on, because on 24 it
# is behind a flag and the temporal tests skip themselves without
# it, and a test that skips everywhere is a test that proves
# nothing. Only on 24: 26 has Temporal unflagged, so the run above
# is already that run, and the flag it would be passed there is
# one V8 no longer has.
- run: npm run test:temporal
if: matrix.node == 24
# The same suite again, over an addon built with AddressSanitizer.
#
# There is no `unsafe` in this crate, which is the reason to run this
# rather than the reason not to: what a binding gets wrong is not
# arithmetic on a raw pointer but a handle used after the scope that
# owned it closed, a buffer read on a thread the runtime had already
# taken back, an engine allocation freed on one side of the boundary
# and touched from the other. None of those are `unsafe` blocks here
# and every one of them is a use-after-free somewhere, which is the
# thing this tool exists to find.
#
# ASan's runtime has to be loaded before anything it instruments, and
# the addon is opened by `require` long after node has started, so it
# is preloaded rather than linked: `-Zexternal-clangrt` tells rustc
# not to bundle its own copy, and LD_PRELOAD supplies clang's. The
# path is asked for rather than written down, because it moves with
# the LLVM version the image ships. Leak detection is off here and
# the job below is where it lives, since ASan and LSan report the
# same run twice and a report that arrives in two jobs is a report
# nobody can attribute.
sanitizer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- uses: Swatinem/rust-cache@v2
# The instrumented build is nightly, because `-Zsanitizer` is,
# and it is the only thing here that is: what ships is built by
# the pinned compiler in every other job.
- run: rustup toolchain install nightly --profile minimal
- run: sudo apt-get update && sudo apt-get install -y libclang-rt-18-dev
- run: npm ci
- name: The addon, instrumented
env:
RUSTUP_TOOLCHAIN: nightly
RUSTFLAGS: -Zsanitizer=address -Zexternal-clangrt
CC: clang
CXX: clang++
run: npm run build:debug
- name: The suite, watched
run: |
runtime=$(clang -print-file-name=libclang_rt.asan-$(uname -m).so)
test -f "$runtime"
LD_PRELOAD="$runtime" ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 \
npm test
# The third tool over the same suite, and the one that watches what
# the sanitizer cannot. ASan instruments the source it compiles, so
# it sees nothing node itself does with the memory it hands the
# addon; Valgrind instruments the instructions that run, so both
# sides of the boundary are watched and so is every prebuilt thing
# either of them links. It also reports a read of memory nobody
# wrote, which ASan does not look for at all.
#
# Definite leaks only, and for the reason a Rust process always gives:
# one-time allocations held at exit are still reachable and not lost,
# and counting those would fire this gate on every run and teach
# everyone to ignore it. --error-exitcode is what makes a report a
# failure rather than something in a log nobody opens.
#
# tools/valgrind.supp is three lines of rule and says what it leaves
# out. It exists because which node this runs on decides what leaks: a
# node linked against the system OpenSSL needs nothing suppressed, and
# the one the hosted images install has OpenSSL inside it and holds
# twenty four bytes of compression table forever. The rule names the
# binary that allocated rather than the leak that was reported, which
# is what stops it growing a line every bad week.
#
# --jitless turns V8's compilers off, and it is here so that the
# uninitialised-value check can stay on. Maglev branches on memory
# nobody wrote while it compiles, on a thread of its own, and reports
# it from inside the node binary a dozen frames from anything this
# package wrote. Suppressing that would mean suppressing a whole class
# of error inside node, and that class is the one worth having: an
# addon that hands a half-filled buffer back is exactly what this tool
# sees and ASan does not. The flag is --jitless and not --no-opt
# because --no-opt left Maglev on and the job stayed red: the tiers
# have their own switches now and naming them one by one is a list
# that goes stale the next time V8 grows one. --jitless is the flag
# that cannot be partially true. The suite runs it in ten seconds
# against a job that is already fifty times slower than the real
# thing, so it costs nothing it was measuring.
leaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- uses: Swatinem/rust-cache@v2
- run: sudo apt-get update && sudo apt-get install -y valgrind
- run: npm ci
# An ordinary build. Valgrind wants the instructions the addon
# actually ships, and an instrumented one would be a different
# program with a different allocator underneath it.
- run: npm run build:debug
# The gate is validated the only way a gate can be: a deliberate
# leak has to fail it, and it has to be a leak of the shape being
# looked for, out of a shared object node loaded rather than out
# of node. Preloaded rather than required, because a real napi
# module is a lot of code to write for four thousand bytes and
# nothing about the check needs node to have called it.
- name: A leak the job is meant to catch, caught
run: |
cat > /tmp/leak.c <<'EOF'
#include <stdlib.h>
__attribute__((constructor)) static void leak(void) {
void *lost = malloc(4096);
(void)lost;
}
EOF
cc -shared -fPIC -o /tmp/leak.so /tmp/leak.c
set +e
LD_PRELOAD=/tmp/leak.so valgrind --error-exitcode=1 --leak-check=full \
--show-leak-kinds=definite --errors-for-leak-kinds=definite \
--suppressions=tools/valgrind.supp -q \
node --jitless -e ''
test $? -eq 1 || { echo "the leak gate did not fire on a leak"; exit 1; }
- name: The suite, counted
run: |
valgrind --error-exitcode=1 --leak-check=full \
--show-leak-kinds=definite --errors-for-leak-kinds=definite \
--suppressions=tools/valgrind.supp -q \
node --jitless --test "test/*.test.mjs"
# Bun and Deno run the same suite over the same binary, because the
# binary is the same one: N-API is the ABI all three implement, and a
# runtime this package claims and nothing runs on is a runtime this
# package has broken. Node builds the addon in both jobs, since the
# build is napi-rs and cargo and neither has anything to do with which
# runtime is going to load what they made.
bun:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
- uses: Swatinem/rust-cache@v2
- run: npm ci
- run: npm run build:debug
# `bun test` and not `bun run`, because the tests are written
# against `node:test` and Bun's shim for it refuses to register a
# test outside its own runner. The shim brings Bun's own five
# second per-test timeout with it, which node does not have and
# two of these tests do not fit inside: they build a table of
# sixty thousand people so that a stream has something to be
# faster than, and the build alone is most of five seconds on a
# shared runner. `--timeout` puts the limit where it catches a
# hang rather than a fixture.
- run: npm run test:bun
deno:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- uses: denoland/setup-deno@v2
with:
deno-version: 2.9.5
- uses: Swatinem/rust-cache@v2
- run: npm ci
- run: npm run build:debug
# Permissions rather than `-A`, so the list a Deno user has to
# write is a list this job has proved is enough. `--no-check` is
# about the checkout and not about the package: the tests reach
# the declarations through relative paths here rather than through
# `npm:zudb`, and Deno reads the `.cjs` beside them as a module
# with an `export =` in it. The package job above is what checks
# the types, through the resolution a user actually gets.
- run: npm run test:deno
# The shared corpus, which is the same cases the engine runs against
# itself and every other client runs against theirs. A job of its own
# because it needs a second checkout, and on one platform because what
# it asks about is this client's value mapping rather than anything the
# operating system decides.
corpus:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- uses: Swatinem/rust-cache@v2
# The cases are versioned with the engine, so the revision comes
# out of the pin this client already builds against rather than
# being written down a second time and drifting away from it.
- id: pin
run: |
rev=$(sed -n 's/^zudb = .*rev = "\([0-9a-f]*\)".*/\1/p' Cargo.toml)
test -n "$rev"
echo "rev=$rev" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v7
with:
repository: tamnd/zu
ref: ${{ steps.pin.outputs.rev }}
path: engine
- run: npm ci
- run: npm run build:debug
# The runner first, because its summary is the line a person reads
# when this job goes red, and the suite second, because it is the
# one that knows which cases this client is allowed to leave
# unanswered.
- run: node conformance/cli.mjs engine/conformance/cases
- run: node --test "test/conformance*.test.mjs"
env:
ZU_CASES: engine/conformance/cases