Skip to content

Commit cd12793

Browse files
authored
ci: overhaul CI (#253)
This is a general CI overhaul. ### Summary - Add `cargo-deny` security audit for vulnerability and license checking - Add MSRV (1.89) verification job - Add documentation build check - Add Sanitizer workflow with ASAN for FFI crates and TSAN async crates. - Add Windows to tests and pre-commit - Rewrite test workflow with grouped multi-platform matrix (Linux x86/ARM, macOS, Windows) - Add `verify-coverage` job that fails if any crate isn't assigned to a test group - Drop the complex segfault detection logic in the SPV part which was actually hiding test failures - Add enable concurrency control in `pre-commit.yml` to cancel redundant runs - Add `no-std` build checks to verify crates that claim to support it actually build. - Fix and improve fuzz workflow generation files ### Test Groups Tests are now organized in `.github/ci-groups.yml`. CI fails if a new crate is added without being assigned to a group. ### Jobs - 24 test jobs (6 groups (core, spv, ffi, wallet, rpc, tools) × 4 platforms (ubuntu arm, ubuntu x86_64, macos, windows)) - verify-coverage, security, msrv, docs build, sanitizer, no-std checks, pre-commit, 15 x fuzz
1 parent efaac21 commit cd12793

File tree

17 files changed

+601
-642
lines changed

17 files changed

+601
-642
lines changed

.github/ci-groups.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Test group configuration for CI
2+
# CI will fail if any workspace crate is not assigned to a group or excluded
3+
4+
groups:
5+
core:
6+
- dashcore
7+
- dashcore_hashes
8+
- dashcore-private
9+
- dash-network
10+
11+
spv:
12+
- dash-spv
13+
14+
wallet:
15+
- key-wallet
16+
- key-wallet-manager
17+
18+
ffi:
19+
- dash-network-ffi
20+
- dash-spv-ffi
21+
- key-wallet-ffi
22+
23+
rpc:
24+
- dashcore-rpc
25+
- dashcore-rpc-json
26+
27+
tools:
28+
- dashcore-test-utils
29+
- dash-fuzz
30+
31+
# Crates intentionally not tested (with reason)
32+
excluded:
33+
- integration_test # Requires live Dash node

.github/ci-no-std.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# No-std build checks
2+
#
3+
# Lists crates that support no-std and their test configurations.
4+
# Each entry runs: cargo check --no-default-features --features <entry>
5+
6+
dashcore_hashes:
7+
- bare # --no-default-features only
8+
- alloc
9+
- alloc serde # multiple features
10+
11+
dashcore-private:
12+
- bare
13+
- alloc
14+
15+
dash-network:
16+
- no-std

.github/scripts/ci_config.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
#!/usr/bin/env python3
2+
"""CI configuration management script.
3+
4+
Used by GitHub Actions workflows for test management.
5+
6+
Subcommands:
7+
verify-groups Check all workspace crates are assigned to test groups
8+
run-group Run tests for all crates in a group
9+
run-no-std Run no-std build checks
10+
"""
11+
12+
import argparse
13+
import json
14+
import os
15+
import subprocess
16+
import sys
17+
from pathlib import Path
18+
19+
import yaml
20+
21+
22+
def get_workspace_metadata():
23+
"""Get workspace metadata from cargo."""
24+
result = subprocess.run(
25+
["cargo", "metadata", "--no-deps", "--format-version", "1"],
26+
capture_output=True,
27+
text=True,
28+
check=True,
29+
)
30+
return json.loads(result.stdout)
31+
32+
33+
def load_yaml(path: Path):
34+
"""Load YAML file with error handling."""
35+
try:
36+
with open(path) as f:
37+
content = yaml.safe_load(f)
38+
return content if content is not None else {}
39+
except FileNotFoundError:
40+
github_error(f"Configuration file not found: {path}")
41+
sys.exit(1)
42+
except yaml.YAMLError as e:
43+
github_error(f"Invalid YAML in {path}: {e}")
44+
sys.exit(1)
45+
46+
47+
def github_error(msg: str):
48+
"""Print GitHub Actions error annotation."""
49+
print(f"::error::{msg}")
50+
51+
52+
def github_notice(msg: str):
53+
"""Print GitHub Actions notice annotation."""
54+
print(f"::notice::{msg}")
55+
56+
57+
def github_group_start(name: str):
58+
"""Start a GitHub Actions log group."""
59+
print(f"::group::{name}", flush=True)
60+
61+
62+
def github_group_end():
63+
"""End a GitHub Actions log group."""
64+
print("::endgroup::", flush=True)
65+
66+
67+
def github_output(name: str, value: str):
68+
"""Write a GitHub Actions output variable."""
69+
output_file = os.environ.get("GITHUB_OUTPUT")
70+
if output_file:
71+
with open(output_file, "a") as f:
72+
f.write(f"{name}={value}\n")
73+
74+
75+
def verify_groups(args):
76+
"""Verify all workspace crates are assigned to test groups."""
77+
metadata = get_workspace_metadata()
78+
workspace_crates = {pkg["name"] for pkg in metadata["packages"]}
79+
80+
config = load_yaml(args.groups_file)
81+
groups = config.get("groups", {})
82+
83+
assigned = set()
84+
for group_crates in groups.values():
85+
if group_crates:
86+
assigned.update(group_crates)
87+
assigned.update(config.get("excluded", []) or [])
88+
89+
unassigned = workspace_crates - assigned
90+
if unassigned:
91+
github_error(
92+
f"Crates not assigned to any test group: {', '.join(sorted(unassigned))}"
93+
)
94+
print("\nPlease add them to a group or 'excluded' section in ci-groups.yml")
95+
return 1
96+
97+
print(f"All {len(workspace_crates)} workspace crates are assigned to test groups")
98+
99+
# Output groups for GitHub Actions matrix
100+
github_output("groups", json.dumps(list(groups.keys())))
101+
102+
return 0
103+
104+
105+
def run_no_std(args):
106+
"""Run no-std build checks from ci-no-std.yml.
107+
108+
Format: crate_name: [list of configs]
109+
Each config runs: cargo check -p crate --no-default-features --features <config>
110+
Special: 'bare' means just --no-default-features (no features)
111+
"""
112+
config = load_yaml(args.no_std_file)
113+
114+
failed = []
115+
116+
for crate_name, entries in config.items():
117+
if not entries:
118+
continue
119+
120+
for entry in entries:
121+
if not isinstance(entry, str) or not entry.strip():
122+
continue
123+
124+
entry_clean = entry.strip()
125+
126+
# Build cargo flags
127+
if entry_clean == "bare":
128+
flags = ["--no-default-features"]
129+
display_name = "bare"
130+
elif entry_clean == "no-std":
131+
flags = ["--no-default-features", "--features", "no-std"]
132+
display_name = "no-std"
133+
elif " " in entry_clean:
134+
# Multiple features (space-separated)
135+
features = entry_clean.replace(" ", ",")
136+
flags = ["--no-default-features", "--features", features]
137+
display_name = entry_clean.replace(" ", "+")
138+
else:
139+
# Single feature
140+
flags = ["--no-default-features", "--features", entry_clean]
141+
display_name = entry_clean
142+
143+
github_group_start(f"{crate_name} ({display_name})")
144+
145+
cmd = ["cargo", "check", "-p", crate_name] + flags
146+
result = subprocess.run(cmd)
147+
148+
github_group_end()
149+
150+
if result.returncode != 0:
151+
failed.append(f"{crate_name} ({display_name})")
152+
github_error(f"No-std check failed: {crate_name} with {' '.join(flags)}")
153+
154+
if failed:
155+
print("\n" + "=" * 40)
156+
print("FAILED NO-STD CHECKS:")
157+
for f in failed:
158+
print(f" - {f}")
159+
print("=" * 40)
160+
return 1
161+
162+
return 0
163+
164+
165+
def run_group_tests(args):
166+
"""Run tests for all crates in a group."""
167+
config = load_yaml(args.groups_file)
168+
groups = config.get("groups", {})
169+
170+
if args.group not in groups:
171+
github_error(f"Unknown group: {args.group}")
172+
return 1
173+
174+
crates = groups[args.group] or []
175+
failed = []
176+
177+
for crate in crates:
178+
# Skip dash-fuzz on Windows
179+
if args.os == "windows-latest" and crate == "dash-fuzz":
180+
github_notice(f"Skipping {crate} on Windows (honggfuzz not supported)")
181+
continue
182+
183+
github_group_start(f"Testing {crate}")
184+
185+
cmd = ["cargo", "test", "-p", crate, "--all-features"]
186+
result = subprocess.run(cmd)
187+
188+
github_group_end()
189+
190+
if result.returncode != 0:
191+
failed.append(crate)
192+
github_error(f"Test failed for {crate} on {args.os}")
193+
194+
if failed:
195+
print("\n" + "=" * 40)
196+
print(f"FAILED TESTS ({args.group} on {args.os}):")
197+
for f in failed:
198+
print(f" - {f}")
199+
print("=" * 40)
200+
return 1
201+
202+
return 0
203+
204+
205+
def main():
206+
parser = argparse.ArgumentParser(description="CI configuration management")
207+
parser.add_argument(
208+
"--groups-file",
209+
type=Path,
210+
default=Path(".github/ci-groups.yml"),
211+
help="Path to ci-groups.yml",
212+
)
213+
parser.add_argument(
214+
"--no-std-file",
215+
type=Path,
216+
default=Path(".github/ci-no-std.yml"),
217+
help="Path to ci-no-std.yml",
218+
)
219+
220+
subparsers = parser.add_subparsers(dest="command", required=True)
221+
222+
subparsers.add_parser("verify-groups", help="Verify all crates assigned to groups")
223+
subparsers.add_parser("run-no-std", help="Run no-std checks")
224+
225+
run_group_parser = subparsers.add_parser("run-group", help="Run tests for a group")
226+
run_group_parser.add_argument("group", help="Group name")
227+
run_group_parser.add_argument("--os", default="ubuntu-latest", help="OS name")
228+
229+
args = parser.parse_args()
230+
231+
commands = {
232+
"verify-groups": verify_groups,
233+
"run-no-std": run_no_std,
234+
"run-group": run_group_tests,
235+
}
236+
237+
return commands[args.command](args)
238+
239+
240+
if __name__ == "__main__":
241+
sys.exit(main())
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Build and Test
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
os:
7+
required: true
8+
type: string
9+
groups:
10+
required: true
11+
type: string
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
test:
18+
name: ${{ matrix.group }}
19+
runs-on: ${{ inputs.os }}
20+
strategy:
21+
fail-fast: false
22+
matrix:
23+
group: ${{ fromJson(inputs.groups) }}
24+
steps:
25+
- uses: actions/checkout@v6
26+
- uses: dtolnay/rust-toolchain@stable
27+
- uses: Swatinem/rust-cache@v2
28+
with:
29+
shared-key: "test-${{ inputs.os }}-${{ matrix.group }}"
30+
- run: pip install pyyaml
31+
- name: Run tests
32+
run: python .github/scripts/ci_config.py run-group ${{ matrix.group }} --os ${{ inputs.os }}

0 commit comments

Comments
 (0)