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
name: CI
# This repository had no test workflow. `book-pages.yml`, `msrv.yml` and
# `rust-release.yml` were the whole of its CI, so 127 native tests and 31
# doctests were committed, maintained, and never run by anything but a person
# choosing to run them. All of them pass; nothing was watching.
#
# `msrv.yml` already builds at the 1.81 floor, so that is not repeated here.
on:
push:
branches:
pull_request:
permissions:
contents: read
concurrency:
# Pull requests share one group per ref and cancel superseded runs; default-
# branch runs get one group per commit, since GitHub supersedes a pending run
# in a shared group whatever `cancel-in-progress` says.
group: melinoe-ci-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: "0"
# `rust-toolchain.toml` pins 1.97.0, so every cargo call here uses it whatever
# the action installs. Without this input the action defaults to `stable` and
# rustup then fetches 1.97.0 as a second toolchain on first use, with the
# action's `components:` installed for the one that does not run.
RUST_TOOLCHAIN: 1.97.0
jobs:
# Public-surface compatibility, informational on pull requests: the
# detected change class is visible while the change is still in
# review. The blocking comparison runs on release.
# Atlas ATLAS-SEMVER-GATE-FLEETWIDE-2026-08-28.
semver:
name: SemVer gate
uses: ryancinsight/atlas/.github/workflows/semver-gate.yml@da9d549bc50dbb9c4e1d18dca8f37d17059098cb
with:
package: melinoe
rust-toolchain: 1.97.0
permissions:
contents: read
verify:
name: Verify
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: clippy, rustfmt
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: taiki-e/install-action@ea4c0b50b0d394ee91c5bb68a6a295b0556feb8e # nextest
with:
tool: nextest
# Cheapest first, so a formatting slip costs seconds rather than a build.
- name: Format
run: cargo fmt --all -- --check
# `std` is a superset of `alloc`, and the crate's whole point is that the
# branded types work without either. A default-features build would never
# exercise that, so the no-default-features check is not optional here.
- name: Build without default features
run: cargo check --locked --no-default-features
- name: Clippy
run: cargo clippy --locked --all-targets --all-features -- -D warnings
- name: Native tests
run: cargo nextest run --locked --all-features
- name: Documentation tests
run: cargo test --locked --doc --all-features
- name: Documentation
env:
RUSTDOCFLAGS: -D warnings
run: cargo doc --locked --no-deps --all-features
# The README pins soundness two ways: `compile_fail` doctests, which `verify`
# already runs, and Miri (Stacked Borrows + data-race detection). Nothing ran
# the second one, so the claim was unfalsifiable in CI. This job makes it
# falsifiable.
#
# Miri needs nightly; the crate's own `rust-toolchain.toml` pins stable, so
# the toolchain is selected per-invocation rather than through the repo file.
#
# `PROPTEST_CASES` is bounded deliberately. `tests/partition.rs` carries three
# proptests at proptest's default 256 cases, each spawning real threads over up
# to 255 elements. Under Miri's ~1000x slowdown that single suite exceeds a
# 20-minute budget and is killed mid-run, so an unbounded invocation cannot
# pass. At 8 cases the full 26-test suite completes cleanly in ~67s. The bound
# is a scheduling constraint, not a reduction in what is checked: every test
# still runs, and the shrinker still minimizes any counterexample it finds.
miri:
name: Miri (aliasing and data-race detection)
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install the Miri nightly toolchain
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # nightly
with:
toolchain: nightly
components: miri
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# Both aliasing models the README names are run, over the same targets.
# Stacked Borrows is Miri's default; Tree Borrows is a *more permissive*
# model, so a program clean under Tree Borrows can still be UB under
# Stacked Borrows. Neither subsumes the other, so both are run in full —
# a single model would not pin the "Stacked and Tree Borrows" claim.
- name: Miri — melinoe (Stacked Borrows)
env:
PROPTEST_CASES: "8"
MIRIFLAGS: -Zmiri-disable-isolation
run: cargo +nightly miri test --locked --all-features
- name: Miri — melinoe (Tree Borrows)
env:
PROPTEST_CASES: "8"
MIRIFLAGS: -Zmiri-tree-borrows -Zmiri-disable-isolation
run: cargo +nightly miri test --locked --all-features