Skip to content

Commit 648485d

Browse files
committed
ci: overhaul CI
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 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 x86_64 runner to fuzz workflow alongside ARM for cross-architecture coverage - Add enable concurrency control in `pre-commit.yml` to cancel redundant runs ### 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: 4 → ~25 (parallel) - 20 test jobs (5 groups (core, spv, wallet, rpc, tools) × 4 platforms (ubuntu arm, ubuntu x86_64, macos, windows)) - security, msrv, docs build, verify-coverage
1 parent b5fd25b commit 648485d

File tree

14 files changed

+452
-581
lines changed

14 files changed

+452
-581
lines changed

.github/ci-groups.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
- dash-network-ffi
11+
12+
spv:
13+
- dash-spv
14+
- dash-spv-ffi
15+
16+
wallet:
17+
- key-wallet
18+
- key-wallet-ffi
19+
- key-wallet-manager
20+
21+
rpc:
22+
- dashcore-rpc
23+
- dashcore-rpc-json
24+
25+
tools:
26+
- dashcore-test-utils
27+
- dash-fuzz
28+
29+
# Crates intentionally not tested (with reason)
30+
excluded:
31+
- 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: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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 subprocess
15+
import sys
16+
from pathlib import Path
17+
18+
import yaml
19+
20+
21+
def get_workspace_metadata():
22+
"""Get workspace metadata from cargo."""
23+
result = subprocess.run(
24+
["cargo", "metadata", "--no-deps", "--format-version", "1"],
25+
capture_output=True,
26+
text=True,
27+
check=True,
28+
)
29+
return json.loads(result.stdout)
30+
31+
32+
def load_yaml(path: Path):
33+
"""Load YAML file."""
34+
with open(path) as f:
35+
return yaml.safe_load(f)
36+
37+
38+
def github_error(msg: str):
39+
"""Print GitHub Actions error annotation."""
40+
print(f"::error::{msg}")
41+
42+
43+
def github_notice(msg: str):
44+
"""Print GitHub Actions notice annotation."""
45+
print(f"::notice::{msg}")
46+
47+
48+
def github_group_start(name: str):
49+
"""Start a GitHub Actions log group."""
50+
print(f"::group::{name}")
51+
52+
53+
def github_group_end():
54+
"""End a GitHub Actions log group."""
55+
print("::endgroup::")
56+
57+
58+
def verify_groups(args):
59+
"""Verify all workspace crates are assigned to test groups."""
60+
metadata = get_workspace_metadata()
61+
workspace_crates = {pkg["name"] for pkg in metadata["packages"]}
62+
63+
config = load_yaml(args.groups_file)
64+
65+
assigned = set()
66+
for group_crates in config.get("groups", {}).values():
67+
if group_crates:
68+
assigned.update(group_crates)
69+
assigned.update(config.get("excluded", []) or [])
70+
71+
unassigned = workspace_crates - assigned
72+
if unassigned:
73+
github_error(
74+
f"Crates not assigned to any test group: {', '.join(sorted(unassigned))}"
75+
)
76+
print("\nPlease add them to a group or 'excluded' section in ci-groups.yml")
77+
return 1
78+
79+
print(f"All {len(workspace_crates)} workspace crates are assigned to test groups")
80+
return 0
81+
82+
83+
def run_no_std(args):
84+
"""Run no-std build checks from ci-no-std.yml.
85+
86+
Format: crate_name: [list of configs]
87+
Each config runs: cargo check -p crate --no-default-features --features <config>
88+
Special: 'bare' means just --no-default-features (no features)
89+
"""
90+
config = load_yaml(args.no_std_file) or {}
91+
92+
failed = []
93+
94+
for crate_name, entries in config.items():
95+
if not entries:
96+
continue
97+
98+
for entry in entries:
99+
if not isinstance(entry, str) or not entry.strip():
100+
continue
101+
102+
entry_clean = entry.strip()
103+
104+
# Build cargo flags
105+
if entry_clean == "bare":
106+
flags = ["--no-default-features"]
107+
display_name = "bare"
108+
elif entry_clean == "no-std":
109+
flags = ["--no-default-features", "--features", "no-std"]
110+
display_name = "no-std"
111+
elif " " in entry_clean:
112+
# Multiple features (space-separated)
113+
features = entry_clean.replace(" ", ",")
114+
flags = ["--no-default-features", "--features", features]
115+
display_name = entry_clean.replace(" ", "+")
116+
else:
117+
# Single feature
118+
flags = ["--no-default-features", "--features", entry_clean]
119+
display_name = entry_clean
120+
121+
github_group_start(f"{crate_name} ({display_name})")
122+
123+
cmd = ["cargo", "check", "-p", crate_name] + flags
124+
result = subprocess.run(cmd)
125+
126+
github_group_end()
127+
128+
if result.returncode != 0:
129+
failed.append(f"{crate_name} ({display_name})")
130+
github_error(f"No-std check failed: {crate_name} with {' '.join(flags)}")
131+
132+
if failed:
133+
print("\n" + "=" * 40)
134+
print("FAILED NO-STD CHECKS:")
135+
for f in failed:
136+
print(f" - {f}")
137+
print("=" * 40)
138+
return 1
139+
140+
return 0
141+
142+
143+
def run_group_tests(args):
144+
"""Run tests for all crates in a group."""
145+
config = load_yaml(args.groups_file)
146+
groups = config.get("groups", {})
147+
148+
if args.group not in groups:
149+
github_error(f"Unknown group: {args.group}")
150+
return 1
151+
152+
crates = groups[args.group] or []
153+
failed = []
154+
155+
for crate in crates:
156+
# Skip dash-fuzz on Windows
157+
if args.os == "windows-latest" and crate == "dash-fuzz":
158+
github_notice(f"Skipping {crate} on Windows (honggfuzz not supported)")
159+
continue
160+
161+
github_group_start(f"Testing {crate}")
162+
163+
# On Windows, skip --all-features to avoid x11 feature
164+
if args.os == "windows-latest":
165+
cmd = ["cargo", "test", "-p", crate]
166+
else:
167+
cmd = ["cargo", "test", "-p", crate, "--all-features"]
168+
169+
result = subprocess.run(cmd)
170+
171+
github_group_end()
172+
173+
if result.returncode != 0:
174+
failed.append(crate)
175+
github_error(f"Test failed for {crate} on {args.os}")
176+
177+
if failed:
178+
print("\n" + "=" * 40)
179+
print(f"FAILED TESTS ({args.group} on {args.os}):")
180+
for f in failed:
181+
print(f" - {f}")
182+
print("=" * 40)
183+
return 1
184+
185+
return 0
186+
187+
188+
def main():
189+
parser = argparse.ArgumentParser(description="CI configuration management")
190+
parser.add_argument(
191+
"--groups-file",
192+
type=Path,
193+
default=Path(".github/ci-groups.yml"),
194+
help="Path to ci-groups.yml",
195+
)
196+
parser.add_argument(
197+
"--no-std-file",
198+
type=Path,
199+
default=Path(".github/ci-no-std.yml"),
200+
help="Path to ci-no-std.yml",
201+
)
202+
203+
subparsers = parser.add_subparsers(dest="command", required=True)
204+
205+
subparsers.add_parser("verify-groups", help="Verify all crates assigned to groups")
206+
subparsers.add_parser("run-no-std", help="Run no-std checks")
207+
208+
run_group_parser = subparsers.add_parser("run-group", help="Run tests for a group")
209+
run_group_parser.add_argument("group", help="Group name")
210+
run_group_parser.add_argument("--os", default="ubuntu-latest", help="OS name")
211+
212+
args = parser.parse_args()
213+
214+
commands = {
215+
"verify-groups": verify_groups,
216+
"run-no-std": run_no_std,
217+
"run-group": run_group_tests,
218+
}
219+
220+
return commands[args.command](args)
221+
222+
223+
if __name__ == "__main__":
224+
sys.exit(main())

.github/scripts/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pyyaml

.github/workflows/fuzz.yml

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ concurrency:
1818
jobs:
1919
fuzz:
2020
if: ${{ !github.event.act }}
21-
runs-on: ubuntu-22.04-arm
21+
runs-on: ${{ matrix.os }}
2222
strategy:
2323
fail-fast: false
2424
matrix:
25+
os: [ubuntu-latest, ubuntu-22.04-arm]
2526
fuzz_target: [
2627
dash_outpoint_string,
2728
dash_deserialize_amount,
@@ -46,7 +47,7 @@ jobs:
4647
steps:
4748
- name: Install test dependencies
4849
run: sudo apt-get update -y && sudo apt-get install -y binutils-dev libunwind8-dev libcurl4-openssl-dev libelf-dev libdw-dev cmake gcc libiberty-dev
49-
- uses: actions/checkout@v4
50+
- uses: actions/checkout@v6
5051
- name: Setup Rust toolchain
5152
uses: dtolnay/rust-toolchain@stable
5253
with:
@@ -57,20 +58,23 @@ jobs:
5758
workspaces: "fuzz -> target"
5859
- name: fuzz
5960
run: cd fuzz && ./fuzz.sh "${{ matrix.fuzz_target }}"
60-
- run: echo "${{ matrix.fuzz_target }}" >executed_${{ matrix.fuzz_target }}
61-
- uses: actions/upload-artifact@v4
61+
- run: echo "${{ matrix.fuzz_target }}" >executed_${{ matrix.fuzz_target }}_${{ matrix.os }}
62+
- uses: actions/upload-artifact@v6
6263
with:
63-
name: executed_${{ matrix.fuzz_target }}
64-
path: executed_${{ matrix.fuzz_target }}
64+
name: executed_${{ matrix.fuzz_target }}_${{ matrix.os }}
65+
path: executed_${{ matrix.fuzz_target }}_${{ matrix.os }}
6566

6667
verify-execution:
6768
if: ${{ !github.event.act }}
6869
needs: fuzz
69-
runs-on: ubuntu-22.04-arm
70+
runs-on: ubuntu-latest
7071
steps:
71-
- uses: actions/checkout@v4
72-
- uses: actions/download-artifact@v4
72+
- uses: actions/checkout@v6
73+
- uses: actions/download-artifact@v6
7374
- name: Display structure of downloaded files
7475
run: ls -R
75-
- run: find executed_* -type f -exec cat {} + | sort > executed
76-
- run: source ./fuzz/fuzz-util.sh && listTargetNames | sort | diff - executed
76+
- name: Verify all fuzz targets were executed
77+
run: |
78+
# Each target runs on multiple platforms, so deduplicate
79+
find executed_* -type f -exec cat {} + | sort -u > executed
80+
source ./fuzz/fuzz-util.sh && listTargetNames | sort | diff - executed

.github/workflows/pre-commit.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,21 @@ on:
77
- 'v**-dev'
88
pull_request:
99

10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
1014
jobs:
1115
pre-commit:
1216
name: Pre-commit (${{ matrix.os }})
1317
runs-on: ${{ matrix.os }}
1418
strategy:
1519
fail-fast: false
1620
matrix:
17-
# TODO: Windows excluded - rs-x11-hash requires POSIX headers (unistd.h)
18-
os: [ubuntu-latest, macos-latest]
21+
os: [ubuntu-latest, macos-latest, windows-latest]
1922
steps:
2023
- name: Checkout repository
21-
uses: actions/checkout@v5
24+
uses: actions/checkout@v6
2225

2326
- name: Set up Python
2427
uses: actions/setup-python@v6

0 commit comments

Comments
 (0)