Skip to content

Commit 7a1de3c

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 c907c4a commit 7a1de3c

File tree

15 files changed

+538
-582
lines changed

15 files changed

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

.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

0 commit comments

Comments
 (0)