Skip to content

Commit d33d5dd

Browse files
authored
ci(analyzer): adopt liveness primer for all pull requests (#802)
1 parent 06b7fc3 commit d33d5dd

6 files changed

Lines changed: 269 additions & 26 deletions

File tree

.github/workflows/liveness-primer.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ name: Analyzer Blast Radius
33
on:
44
pull_request:
55
types: [opened, synchronize, reopened, ready_for_review]
6-
paths:
7-
- "skylos/**"
8-
- "pyproject.toml"
9-
- "MANIFEST.in"
10-
- ".github/workflows/liveness-primer.yml"
116

127
permissions:
138
contents: read
@@ -19,7 +14,6 @@ concurrency:
1914
jobs:
2015
blast-radius:
2116
name: Python dead-code blast radius
22-
if: github.event.pull_request.draft == false
2317
runs-on: ubuntu-24.04
2418
timeout-minutes: 45
2519

@@ -43,8 +37,6 @@ jobs:
4337
env:
4438
SKYLOS_REPOSITORY: https://github.com/duriantaco/skylos
4539
BASE_SHA: ${{ github.event.pull_request.base.sha }}
46-
# The base repository owns this synthetic merge commit, including
47-
# for fork PRs, so the comparison tests what would actually land.
4840
MERGE_SHA: ${{ github.sha }}
4941
REPORT_JSON: liveness-primer-report.json
5042
REPORT_MARKDOWN: liveness-primer-report.md

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,20 @@ Frozen `golden-v0.2` highlights:
490490
For methodology, commands, competitor rows, and caveats, see
491491
[BENCHMARK.md](./BENCHMARK.md).
492492

493+
### Real-project regression testing
494+
495+
[liveness_primer](https://github.com/mcdigman/liveness_primer), created and
496+
maintained by [Matthew Digman](https://github.com/mcdigman), is Skylos's official
497+
real-project regression testing tool. On every PR, it compares the base and
498+
proposed merge result against the same pinned Python projects and reports
499+
which findings were added, removed, or changed.
500+
501+
Read the **Analyzer Blast Radius** check for the comparison and downloadable
502+
reports. These results complement the labeled benchmarks above; a change in
503+
finding counts alone does not establish accuracy. See the
504+
[liveness_primer guide](./docs/liveness-primer.md) for scope, review steps, and
505+
reproduction commands.
506+
493507
## Project Evidence
494508

495509
Skylos-assisted dead-code cleanup PRs have been merged in

corpus/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ Acceptance rules:
1414
- Avoid whole-project assertions and avoid gating on total finding counts.
1515
- Prefer cases that protect common frameworks, must-not-miss hooks, and critical static-analysis edge cases.
1616

17-
Why this is better than scanning whole upstream repositories on every PR:
17+
How this complements [liveness_primer](../docs/liveness-primer.md):
1818

19-
- Whole-repo scans are noisy because mature libraries may contain legitimate findings unrelated to the regression you are trying to catch.
20-
- Whole-repo scans are slow and brittle because upstream code changes over time.
21-
- Distilled fixtures let us assert exact expectations like "this symbol must not be reported as dead code" without hiding unrelated issues.
19+
- The primer compares two Skylos revisions on the same pinned upstream projects. It shows real-project changes without requiring every existing finding to be labeled.
20+
- Corpus Guard checks small, explicit expectations like "this symbol must not be reported as dead code." Those expectations let CI distinguish a regression from an intended change.
21+
- When primer review uncovers a bug, reduce it to a regression test or corpus fixture here. Keep both checks: broad change detection and precise correctness checks.
2222

2323
How to add a case:
2424

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This repository keeps lightweight local docs for features that need examples clo
44

55
| Topic | Page |
66
|:---|:---|
7+
| Official real-project regression testing and PR comparison reports | [liveness_primer](./liveness-primer.md) |
78
| Generated codebase navigator for contributors | [Skylos Repo Map](./repo-map/index.html) |
89
| CLI output modes, pretty reports, and TUI controls | [CLI Output Modes](./cli-output.md) |
910
| Optional Ruff Python linting through the Skylos CLI | [Python Linting](./python-linting.md) |

docs/liveness-primer.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Real-project regression testing with liveness_primer
2+
3+
[liveness_primer](https://github.com/mcdigman/liveness_primer), created and
4+
maintained by [Matthew Digman](https://github.com/mcdigman), is Skylos's official
5+
real-project regression testing tool. Its source, adapters, and pinned project
6+
corpus live in Matthew's repository. Skylos keeps the CI integration here.
7+
8+
It answers a specific question: **what changes for real projects if we merge
9+
this PR?** It does not decide whether every finding is correct.
10+
11+
## What CI runs
12+
13+
The [Analyzer Blast Radius workflow](../.github/workflows/liveness-primer.yml)
14+
runs when a PR is opened, updated with new commits, reopened, or marked ready
15+
for review. There are no changed-path or draft filters: docs-only and draft
16+
PRs get the same comparison. Fork PRs may need maintainer approval to run, and
17+
GitHub cannot run this `pull_request` workflow while merge conflicts remain.
18+
See [GitHub's event documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request).
19+
20+
The primer builds two Skylos revisions in separate managed environments:
21+
22+
- Base: the PR's exact base commit (`pull_request.base.sha`).
23+
- Head: GitHub's synthetic merge commit (`github.sha`), so the comparison
24+
includes the result of merging the PR into its base branch, including forks.
25+
26+
Both revisions scan the same commit-pinned corpus projects. The workflow uses
27+
the primer's packaged corpus, `--all`, two concurrent detector processes, and
28+
a 300-second default timeout per invocation. The job has a 45-minute limit.
29+
The primer and Actions are pinned to full commits; uv is pinned to `0.12.5`,
30+
Python to the `3.13` series, and the primer's dependencies use its lockfile.
31+
32+
The current primer pin is
33+
[`d6f3118a2cfc465426500eab449005fe56845c58`](https://github.com/mcdigman/liveness_primer/tree/d6f3118a2cfc465426500eab449005fe56845c58).
34+
It selects 15 Python projects for Skylos. Its adapter compares unused
35+
functions, imports, classes, variables, and parameters. **File-level findings
36+
(`SKY-E002` / `SKY-E003`) are not included at this pin.** This job does not
37+
opt into security, secrets, quality, or AI-defect analyses, and is not a
38+
cross-language benchmark.
39+
40+
## Read a PR report
41+
42+
1. Open the PR's **Analyzer Blast Radius** run and read its summary.
43+
2. Check that both revisions completed. A crash, timeout, or unusable detector
44+
output fails the job; it is not a clean comparison.
45+
3. Review added, dropped, and changed findings against the intent of the PR.
46+
Added findings can be useful coverage or false positives. Dropped findings
47+
can be fixed false positives or missed detections. Neither direction is
48+
automatically an improvement.
49+
4. Download the `liveness-primer-report` artifact for
50+
`liveness-primer-report.md` and the complete `liveness-primer-report.json`.
51+
The Markdown display can be truncated; use JSON for the full comparison.
52+
Artifacts are retained for 14 days, so save evidence needed for later work.
53+
54+
Finding changes are advisory: the workflow does not use `--fail-on` gates.
55+
It preserves the primer's nonzero exit status and requires a nonempty JSON
56+
report. Available evidence is uploaded even if the comparison fails. A green
57+
check means the comparison completed, not that someone has approved its
58+
findings or proved the PR regression-free.
59+
60+
If a change looks wrong, inspect the pinned source and turn the confirmed bug
61+
into a regression test or [Corpus Guard fixture](../corpus/README.md). Do not
62+
accept or reject a PR solely because its total finding count went down or up.
63+
64+
## Reproduce a comparison
65+
66+
Use a disposable Linux environment without credentials, with Git, Python 3.13,
67+
and uv 0.12.5 available. Managed runs build and execute the detector revisions;
68+
do not run an unfamiliar PR on your everyday development machine. Corpus
69+
projects are static-analysis inputs, not projects whose tests should be run.
70+
71+
Clone the same primer revision:
72+
73+
```bash
74+
git clone https://github.com/mcdigman/liveness_primer.git liveness-primer-check
75+
git -C liveness-primer-check checkout --detach d6f3118a2cfc465426500eab449005fe56845c58
76+
```
77+
78+
Replace the two revision placeholders below with the full base and head SHAs
79+
from the report, not mutable branch names. The head is the reported merge SHA,
80+
not necessarily the PR branch tip.
81+
82+
```bash
83+
uv run --project liveness-primer-check --python 3.13 --locked liveness-primer run \
84+
--tool skylos \
85+
--repo https://github.com/duriantaco/skylos \
86+
--old BASE_SHA_FROM_REPORT \
87+
--new MERGE_SHA_FROM_REPORT \
88+
--all \
89+
--output github \
90+
--json-out liveness-primer-report.json \
91+
--jobs 2 \
92+
--timeout 300
93+
```
94+
95+
The pinned primer requires enforced network isolation for managed runs on
96+
Linux and fails if it cannot establish it. CI uses a fresh GitHub-hosted
97+
runner, read-only repository permission, no passed secrets, no persisted
98+
checkout credentials, and no shared uv cache. Network isolation is not a
99+
complete filesystem sandbox; do not add credentials or use a persistent
100+
self-hosted runner for this job.
101+
102+
The report records dependency versions and environment differences. A later
103+
run may resolve different detector dependencies even with the same source
104+
commits, so retain the original JSON when investigating a discrepancy.
105+
106+
## Benchmarks and maintenance
107+
108+
Primer reports can support change reviews and supply cases for the public
109+
[skylos-demo](https://github.com/duriantaco/skylos-demo) benchmarks. They do not
110+
supply ground-truth labels or precision/recall scores. Public accuracy claims
111+
still need labeled cases, pinned inputs, and the methodology in
112+
[BENCHMARK.md](../BENCHMARK.md). Corpus Guard and the existing benchmark gates
113+
remain in place.
114+
115+
Keep corpus and adapter improvements upstream with Matthew, rather than
116+
copying the primer into Skylos. Propose threshold changes with examples and
117+
review them together; this integration does not impose new finding-count
118+
thresholds or transfer repository ownership.
119+
120+
Update the primer pin in a separate reviewed change, run a fresh comparison,
121+
and inspect corpus, adapter, and report-schema changes before accepting it.
122+
Do not replace the pin with `main`. For example, upstream v0.1.1 adds file-level
123+
findings, so adopting it changes what this CI report covers.

test/test_liveness_primer_workflow.py

Lines changed: 127 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
import json
2+
import os
13
from pathlib import Path
4+
import shutil
5+
import subprocess
6+
import sys
27

8+
import pytest
39
import yaml
410

511
from skylos.rules.config.cicd.github_actions import scan_github_actions_file
@@ -20,7 +26,7 @@ def _comparison_step(workflow):
2026
)
2127

2228

23-
def test_liveness_primer_workflow_is_scoped_read_only_and_advisory():
29+
def test_liveness_primer_workflow_covers_all_prs_read_only_and_advisory():
2430
workflow = _workflow()
2531
triggers = workflow.get("on", workflow.get(True))
2632

@@ -32,20 +38,16 @@ def test_liveness_primer_workflow_is_scoped_read_only_and_advisory():
3238
"reopened",
3339
"ready_for_review",
3440
]
35-
assert set(pull_request["paths"]) == {
36-
"skylos/**",
37-
"pyproject.toml",
38-
"MANIFEST.in",
39-
".github/workflows/liveness-primer.yml",
40-
}
41+
# Docs-only, packaging, tests, and fork PRs all need the same check.
42+
assert set(pull_request) == {"types"}
4143
assert workflow["permissions"] == {"contents": "read"}
4244
assert workflow["concurrency"] == {
4345
"group": "liveness-primer-${{ github.event.pull_request.number }}",
4446
"cancel-in-progress": True,
4547
}
4648

4749
job = workflow["jobs"]["blast-radius"]
48-
assert job["if"] == "github.event.pull_request.draft == false"
50+
assert "if" not in job # Draft PRs get evidence too.
4951
assert job["runs-on"] == "ubuntu-24.04"
5052
assert job["timeout-minutes"] == 45
5153

@@ -72,9 +74,7 @@ def test_liveness_primer_workflow_pins_actions_and_toolchain():
7274
assert all(character in "0123456789abcdef" for character in action_ref)
7375

7476
checkout = next(
75-
step
76-
for step in steps
77-
if step.get("name") == "Check out pinned liveness_primer"
77+
step for step in steps if step.get("name") == "Check out pinned liveness_primer"
7878
)
7979
assert checkout["with"] == {
8080
"repository": "mcdigman/liveness_primer",
@@ -124,9 +124,7 @@ def test_liveness_primer_workflow_preserves_evidence_without_write_access():
124124
steps = workflow["jobs"]["blast-radius"]["steps"]
125125

126126
artifact = next(
127-
step
128-
for step in steps
129-
if step.get("name") == "Upload blast-radius evidence"
127+
step for step in steps if step.get("name") == "Upload blast-radius evidence"
130128
)
131129
assert artifact["if"] == "always()"
132130
assert artifact["with"]["name"] == "liveness-primer-report"
@@ -142,3 +140,118 @@ def test_liveness_primer_workflow_preserves_evidence_without_write_access():
142140
assert "pull-requests: write" not in workflow_source
143141
assert "gh pr comment" not in workflow_source
144142
assert scan_github_actions_file(WORKFLOW_PATH, root=".") == []
143+
144+
145+
def _run_comparison(
146+
tmp_path,
147+
*,
148+
exit_code=0,
149+
report_state="present",
150+
base_sha="a" * 40,
151+
merge_sha="b" * 40,
152+
):
153+
bash = shutil.which("bash")
154+
if bash is None:
155+
pytest.skip("workflow shell checks require bash")
156+
157+
# A shell function intercepts uv in the real workflow command. No primer,
158+
# detector revisions, network requests, or corpus code are executed, and
159+
# no executable fixture needs to be created on disk.
160+
stub = """uv() {
161+
"$PRIMER_STUB_PYTHON" - "$@" <<'PY'
162+
import json, os, sys
163+
with open('invocation.json', 'x', encoding='utf-8') as out:
164+
json.dump(sys.argv[1:], out)
165+
if os.environ['PRIMER_STUB_REPORT'] != 'missing':
166+
with open(os.environ['REPORT_JSON'], 'x', encoding='utf-8') as out:
167+
if os.environ['PRIMER_STUB_REPORT'] == 'present':
168+
json.dump({'fixture': 'offline workflow test'}, out)
169+
print('# Offline primer report')
170+
sys.exit(int(os.environ['PRIMER_STUB_EXIT']))
171+
PY
172+
}
173+
"""
174+
env = {
175+
"PATH": os.defpath,
176+
"SKYLOS_REPOSITORY": "https://github.com/duriantaco/skylos",
177+
"BASE_SHA": base_sha,
178+
"MERGE_SHA": merge_sha,
179+
# Spaces exercise quoting of the report destinations.
180+
"REPORT_JSON": str(tmp_path / "report data.json"),
181+
"REPORT_MARKDOWN": str(tmp_path / "report summary.md"),
182+
"GITHUB_STEP_SUMMARY": str(tmp_path / "step summary.md"),
183+
"PRIMER_STUB_EXIT": str(exit_code),
184+
"PRIMER_STUB_REPORT": report_state,
185+
"PRIMER_STUB_PYTHON": sys.executable,
186+
}
187+
return subprocess.run(
188+
[bash, "-c", stub + _comparison_step(_workflow())["run"]],
189+
cwd=tmp_path,
190+
env=env,
191+
capture_output=True,
192+
text=True,
193+
timeout=10,
194+
check=False,
195+
)
196+
197+
198+
def test_comparison_shell_passes_exact_revisions_and_keeps_both_reports(tmp_path):
199+
result = _run_comparison(tmp_path)
200+
201+
assert result.returncode == 0, result.stderr
202+
assert json.loads((tmp_path / "invocation.json").read_text()) == [
203+
"run",
204+
"--project",
205+
"_liveness_primer",
206+
"--locked",
207+
"liveness-primer",
208+
"run",
209+
"--tool",
210+
"skylos",
211+
"--repo",
212+
"https://github.com/duriantaco/skylos",
213+
"--old",
214+
"a" * 40,
215+
"--new",
216+
"b" * 40,
217+
"--all",
218+
"--output",
219+
"github",
220+
"--json-out",
221+
str(tmp_path / "report data.json"),
222+
"--jobs",
223+
"2",
224+
"--timeout",
225+
"300",
226+
]
227+
assert json.loads((tmp_path / "report data.json").read_text()) == {
228+
"fixture": "offline workflow test"
229+
}
230+
assert (tmp_path / "report summary.md").read_text() == "# Offline primer report\n"
231+
assert (tmp_path / "step summary.md").read_text() == "# Offline primer report\n"
232+
233+
234+
@pytest.mark.parametrize("exit_code", [1, 2, 3])
235+
def test_comparison_shell_does_not_hide_primer_failure_behind_tee(tmp_path, exit_code):
236+
result = _run_comparison(tmp_path, exit_code=exit_code)
237+
238+
assert result.returncode == exit_code
239+
assert (tmp_path / "report data.json").is_file()
240+
assert (tmp_path / "report summary.md").read_text() == "# Offline primer report\n"
241+
242+
243+
@pytest.mark.parametrize("report_state", ["missing", "empty"])
244+
def test_comparison_shell_rejects_success_without_report_data(tmp_path, report_state):
245+
result = _run_comparison(tmp_path, report_state=report_state)
246+
247+
assert result.returncode != 0
248+
assert (tmp_path / "invocation.json").is_file()
249+
250+
251+
@pytest.mark.parametrize("revision", ["base_sha", "merge_sha"])
252+
def test_comparison_shell_rejects_non_commit_refs_before_running(tmp_path, revision):
253+
result = _run_comparison(tmp_path, **{revision: "main"})
254+
255+
assert result.returncode != 0
256+
assert "Invalid comparison revision" in result.stderr
257+
assert not (tmp_path / "invocation.json").exists()

0 commit comments

Comments
 (0)