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
name: API stability
# Enforces the compatibility contract in docs/api-compatibility.md (#695):
# - unintended breakage of the stable public API surface (cargo-semver-checks)
# - the documented feature combinations and targets all build
# - the Send/Sync + panic-free contracts hold under the async feature
#
# These are kept in a dedicated workflow (not ci.yml) so the API-policy gates
# evolve independently of the core fmt/clippy/test pipeline.
on:
push:
branches:
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
# ── Public API breakage detection (AC2) ──────────────────────────────────────
# Compares the PR against the latest version published on crates.io. For a
# 0.x crate, cargo-semver-checks treats a MINOR bump as the breaking axis, so
# this fails only on an *unintended* break of the stable surface that was not
# accompanied by the required version bump.
semver:
name: Public API breakage (cargo-semver-checks)
runs-on: ubuntu-latest
timeout-minutes: 20
# Blocking since v0.32.0 (#695). It ran with continue-on-error from #695
# until then, for one reason: the error enums were exhaustive, so every
# intended new variant reported as breakage and the gate could not tell that
# apart from a real break. #796 marked all 34 public error enums
# #[non_exhaustive] (docs/api-compatibility.md §1, "Error stability rule")
# and v0.32.0 shipped them, which reset the baseline this job compares
# against. A finding here is now an unintended break of the stable surface.
#
# An *intended* break is declared the way release-please reads it: a `!`
# after the type/scope in the PR title (the squash-merge subject) or a
# `BREAKING CHANGE:` footer in the PR body. The step below turns that
# declaration into `--release-type major`: an explicit release type is
# taken literally, and `major` is cargo-semver-checks' name for the
# breaking axis, which on 0.x is the minor bump release-please makes. So a
# declared break is checked as the bump it will produce and the job still
# fails on any break the PR does *not* declare. Cargo.toml is not bumped by
# hand: release-please owns the version and the manifest (#814). Do not
# re-add continue-on-error. Adding a variant to an error enum is not a
# break and does not trip this.
steps:
- uses: actions/checkout@v6
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: semver-checks
- name: Read the release type the change declares
id: declared
env:
# The PR title on pull_request; the squash-merge subject on push.
# Read through env, never interpolated into the script.
SUBJECT: ${{ github.event.pull_request.title || github.event.head_commit.message }}
BODY: ${{ github.event.pull_request.body }}
run: |
release_type=""
if printf '%s\n' "$SUBJECT" | head -n 1 | grep -Eq '^[a-z]+(\([^)]*\))?!:' \
|| printf '%s\n%s\n' "$SUBJECT" "$BODY" | grep -Eq '^BREAKING[ -]CHANGE:'; then
release_type=major
fi
echo "release-type=$release_type" >> "$GITHUB_OUTPUT"
echo "declared release type: ${release_type:-none (derived from Cargo.toml)}"
- name: cargo-semver-checks (stable public API)
uses: obi1kenobi/cargo-semver-checks-action@v2
with:
# Default features = the decode-only `std` stable surface. Writer /
# async / experimental surfaces are checked as they stabilize.
feature-group: default-features
package: djvu-rs
# Empty = derive from the version number (the action skips the flag).
release-type: ${{ steps.declared.outputs.release-type }}
# ── Documented feature combinations & targets (AC3) ──────────────────────────
# The human-readable companion is docs/feature-matrix.md; keep the two in sync.
feature-matrix:
name: Feature matrix (${{ matrix.name }})
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- name: "no_std (host)"
flags: "--no-default-features"
- name: "default (std, decode-only)"
flags: ""
- name: "jpeg"
flags: "--features std,jpeg"
- name: "pdf"
flags: "--features pdf"
- name: "epub"
flags: "--features epub"
- name: "cbz"
flags: "--features cbz"
- name: "tiff"
flags: "--features tiff"
- name: "cli"
flags: "--features cli"
- name: "async"
flags: "--features async"
- name: "parallel"
flags: "--features parallel"
- name: "mmap"
flags: "--features mmap"
- name: "serde"
flags: "--features serde"
- name: "image"
flags: "--features image"
- name: "kitchen-sink"
flags: "--features cli,tiff,async,serde,image,epub,mmap,parallel"
steps:
- uses: actions/checkout@v6
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: feature-matrix
- name: cargo check ${{ matrix.flags }}
run: cargo check ${{ matrix.flags }}
# ── wasm32 documented feature set (AC3) ──────────────────────────────────────
feature-matrix-wasm:
name: Feature matrix (wasm32 ${{ matrix.name }})
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- name: "no_std"
flags: "--no-default-features"
- name: "wasm"
flags: "--features wasm"
- name: "wasm-lazy"
flags: "--features wasm-lazy"
steps:
- uses: actions/checkout@v6
- name: Install Rust stable + wasm32 target
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: feature-matrix-wasm
- name: cargo check --target wasm32-unknown-unknown ${{ matrix.flags }}
run: cargo check --target wasm32-unknown-unknown ${{ matrix.flags }}
# ── Contract tests under the async feature (AC7 + AC8) ───────────────────────
# The default Test (stable) job runs without `async`, so the Send/Sync
# assertions for LazyDocument and the async panic-free surface are only
# exercised here.
contract-tests:
name: Send/Sync + panic-free contracts (async)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: contract-tests
- name: Send/Sync contract
run: cargo test --test send_sync_contract --features async
- name: Panic-free corpus + adversarial inputs
run: cargo test --test panic_free_corpus --features cli