Skip to content

Commit 0dd4b59

Browse files
authored
ci: add dependency health checks (#62)
2 parents 2e31b1b + 6a34e82 commit 0dd4b59

6 files changed

Lines changed: 144 additions & 1 deletion

File tree

.github/workflows/pr-check.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,24 @@ jobs:
2929
- name: Build ${{ matrix.configuration }}
3030
run: dotnet build ModularityKit.Mutator.slnx -c ${{ matrix.configuration }} --no-restore
3131

32+
dependency-health:
33+
name: dependency health
34+
runs-on: ubuntu-latest
35+
needs: build
36+
37+
steps:
38+
- uses: actions/checkout@v5
39+
40+
- uses: actions/setup-dotnet@v5
41+
with:
42+
dotnet-version: 10.0.x
43+
44+
- name: Restore
45+
run: dotnet restore ModularityKit.Mutator.slnx
46+
47+
- name: Check dependency health
48+
run: python3 -m scripts.dependencies.check_package_health --solution ModularityKit.Mutator.slnx
49+
3250
tests:
3351
name: ${{ matrix.name }}
3452
runs-on: ubuntu-latest
@@ -158,6 +176,7 @@ jobs:
158176
runs-on: ubuntu-latest
159177
needs:
160178
- build
179+
- dependency-health
161180
- tests
162181
- examples-core
163182
- examples-governance
@@ -168,12 +187,14 @@ jobs:
168187
- name: Report job results
169188
run: |
170189
echo "build: ${{ needs.build.result }}"
190+
echo "dependency-health: ${{ needs.dependency-health.result }}"
171191
echo "tests: ${{ needs.tests.result }}"
172192
echo "examples-core: ${{ needs.examples-core.result }}"
173193
echo "examples-governance: ${{ needs.examples-governance.result }}"
174194
echo "examples-governance-redis: ${{ needs.examples-governance-redis.result }}"
175195
176196
if [ "${{ needs.build.result }}" != "success" ] || \
197+
[ "${{ needs.dependency-health.result }}" != "success" ] || \
177198
[ "${{ needs.tests.result }}" != "success" ] || \
178199
[ "${{ needs.examples-core.result }}" != "success" ] || \
179200
[ "${{ needs.examples-governance.result }}" != "success" ] || \

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
.idea/
22
bin/
3-
obj/
3+
obj/
4+
__pycache__/
5+
*.pyc

Directory.Build.props

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<Project>
2+
<PropertyGroup>
3+
<BaseIntermediateOutputPath>$(MSBuildProjectDirectory)/obj/$(MSBuildProjectName)/</BaseIntermediateOutputPath>
4+
</PropertyGroup>
5+
</Project>

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,13 @@
2626
```bash
2727
dotnet build ModularityKit.Mutator.slnx -c Release
2828
```
29+
30+
## Dependency checks
31+
32+
Run these after `dotnet restore ModularityKit.Mutator.slnx`:
33+
34+
```bash
35+
python3 -m scripts.dependencies.check_package_health --solution ModularityKit.Mutator.slnx
36+
```
37+
38+
The check reports vulnerable packages as a failing condition and prints outdated packages for review. When a package needs attention, update the affected `PackageReference` version in the owning project and rerun the check.

scripts/dependencies/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Dependency check helpers."""
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env python3
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import os
7+
import pathlib
8+
import subprocess
9+
import sys
10+
from typing import Sequence
11+
12+
13+
VULNERABLE_HEADING = "has the following vulnerable packages"
14+
OUTDATED_HEADING = "has the following updates to its packages"
15+
16+
17+
def repository_root() -> pathlib.Path:
18+
return pathlib.Path(__file__).resolve().parents[2]
19+
20+
21+
def run_dotnet_list(solution: str, mode: str) -> subprocess.CompletedProcess[str]:
22+
env = os.environ.copy()
23+
env.setdefault("DOTNET_CLI_UI_LANGUAGE", "en-US")
24+
25+
return subprocess.run(
26+
[
27+
"dotnet",
28+
"list",
29+
solution,
30+
"package",
31+
f"--{mode}",
32+
"--include-transitive",
33+
"--format",
34+
"console",
35+
],
36+
cwd=repository_root(),
37+
env=env,
38+
text=True,
39+
capture_output=True,
40+
)
41+
42+
43+
def emit_output(result: subprocess.CompletedProcess[str]) -> None:
44+
if result.stdout:
45+
sys.stdout.write(result.stdout)
46+
if not result.stdout.endswith("\n"):
47+
sys.stdout.write("\n")
48+
49+
if result.stderr:
50+
sys.stderr.write(result.stderr)
51+
if not result.stderr.endswith("\n"):
52+
sys.stderr.write("\n")
53+
54+
55+
def check_mode(solution: str, mode: str, heading: str) -> tuple[bool, int]:
56+
result = run_dotnet_list(solution, mode)
57+
emit_output(result)
58+
59+
found = heading in result.stdout
60+
if result.returncode != 0 and not found:
61+
return False, result.returncode
62+
63+
return found, 0
64+
65+
66+
def parse_args(argv: Sequence[str]) -> argparse.Namespace:
67+
parser = argparse.ArgumentParser(description="Check package health for a solution.")
68+
parser.add_argument(
69+
"--solution",
70+
default="ModularityKit.Mutator.slnx",
71+
help="Path to the solution file to inspect.",
72+
)
73+
return parser.parse_args(argv)
74+
75+
76+
def main(argv: Sequence[str]) -> int:
77+
args = parse_args(argv)
78+
79+
vulnerable_found, vulnerable_error = check_mode(args.solution, "vulnerable", VULNERABLE_HEADING)
80+
if vulnerable_error:
81+
return vulnerable_error
82+
83+
outdated_found, outdated_error = check_mode(args.solution, "outdated", OUTDATED_HEADING)
84+
if outdated_error:
85+
return outdated_error
86+
87+
if vulnerable_found:
88+
print(
89+
"Vulnerable packages were reported above. Update the affected package reference and rerun the check.",
90+
file=sys.stderr,
91+
)
92+
return 1
93+
94+
if outdated_found:
95+
print(
96+
"Outdated packages were reported above. Update the affected package references when you are ready.",
97+
file=sys.stderr,
98+
)
99+
100+
return 0
101+
102+
103+
if __name__ == "__main__":
104+
raise SystemExit(main(sys.argv[1:]))

0 commit comments

Comments
 (0)