Skip to content

Commit a643e3a

Browse files
committed
feat: add PR score-diff GitHub Action (slice #17)
Add a marketplace-ready GitHub Action and PR workflow that runs deterministic RADE analysis on base/head refs and posts a reusability/accessibility score diff comment to reduce CI adoption friction. Made-with: Cursor
1 parent 4fe4235 commit a643e3a

10 files changed

Lines changed: 297 additions & 8 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: PR Score Diff
2+
3+
on:
4+
pull_request:
5+
types: [opened, reopened, synchronize]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: write
10+
11+
jobs:
12+
rade-pr-score-diff:
13+
runs-on: ubuntu-latest
14+
env:
15+
PR_NUMBER: ${{ github.event.pull_request.number }}
16+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
17+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
24+
- name: RADE score diff comment
25+
uses: ./
26+
with:
27+
github-token: ${{ secrets.GITHUB_TOKEN }}
28+
pr-number: ${{ env.PR_NUMBER }}
29+
base-sha: ${{ env.BASE_SHA }}
30+
head-sha: ${{ env.HEAD_SHA }}
31+
input-path: examples/sample_ios_output.json
32+
app-id: com.example.legacyapp

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ RADE is in **early alpha**.
8181
- Deterministic scoring, recommendations, and roadmap generation
8282
- Interactive HTML report output (`--html-output`)
8383
- PII scrubbing with preserved structural identifiers
84+
- Marketplace-ready GitHub Action for PR score-diff comments
8485

8586
**Exploratory or secondary surfaces:**
8687
- Accessibility-tree-to-SVG blueprint pipeline
@@ -91,7 +92,6 @@ RADE is in **early alpha**.
9192
**Not built yet:**
9293
- Hosted auth, tenants, and persisted history
9394
- Queue-backed execution
94-
- GitHub Action for CI/CD integration
9595

9696
See [docs/APP_SCOPE.md](docs/APP_SCOPE.md) for the current implementation boundary.
9797

action.yml

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
name: "RADE PR Score Diff"
2+
description: "Run RADE on PR base/head refs and comment reusability/accessibility score deltas."
3+
author: "Buildrr89"
4+
branding:
5+
icon: "bar-chart-2"
6+
color: "blue"
7+
inputs:
8+
github-token:
9+
description: "GitHub token used to read PR metadata and post comments."
10+
required: true
11+
pr-number:
12+
description: "Pull request number."
13+
required: true
14+
base-sha:
15+
description: "Base commit SHA for comparison."
16+
required: true
17+
head-sha:
18+
description: "Head commit SHA for comparison."
19+
required: true
20+
input-path:
21+
description: "Path to RADE input JSON fixture inside the repository."
22+
required: false
23+
default: "examples/sample_ios_output.json"
24+
app-id:
25+
description: "Application identifier passed to RADE CLI."
26+
required: false
27+
default: "com.example.legacyapp"
28+
python-version:
29+
description: "Python runtime version used to run RADE CLI."
30+
required: false
31+
default: "3.14"
32+
runs:
33+
using: "composite"
34+
steps:
35+
- name: Setup Python
36+
uses: actions/setup-python@v5
37+
with:
38+
python-version: ${{ inputs.python-version }}
39+
40+
- name: Install RADE runtime dependencies
41+
shell: bash
42+
run: python -m pip install --disable-pip-version-check neo4j playwright pyyaml
43+
44+
- name: Generate base/head reports
45+
shell: bash
46+
run: |
47+
set -euo pipefail
48+
git checkout --quiet "${{ inputs.base-sha }}"
49+
python -m src.core.cli analyze \
50+
--input "${{ inputs.input-path }}" \
51+
--app-id "${{ inputs.app-id }}" \
52+
--json-output "$RUNNER_TEMP/rade-base.json"
53+
git checkout --quiet "${{ inputs.head-sha }}"
54+
python -m src.core.cli analyze \
55+
--input "${{ inputs.input-path }}" \
56+
--app-id "${{ inputs.app-id }}" \
57+
--json-output "$RUNNER_TEMP/rade-head.json"
58+
59+
- name: Build PR comment body
60+
shell: bash
61+
run: |
62+
set -euo pipefail
63+
python scripts/pr_score_comment.py \
64+
--base-report "$RUNNER_TEMP/rade-base.json" \
65+
--head-report "$RUNNER_TEMP/rade-head.json" \
66+
--base-ref "${{ inputs.base-sha }}" \
67+
--head-ref "${{ inputs.head-sha }}" \
68+
--output "$RUNNER_TEMP/rade-comment.md"
69+
70+
- name: Post or update PR comment
71+
uses: actions/github-script@v7
72+
with:
73+
github-token: ${{ inputs.github-token }}
74+
script: |
75+
const fs = require("fs");
76+
const marker = "<!-- rade-pr-score-comment -->";
77+
const issue_number = Number("${{ inputs.pr-number }}");
78+
const body = fs.readFileSync(`${process.env.RUNNER_TEMP}/rade-comment.md`, "utf8");
79+
80+
const { data: comments } = await github.rest.issues.listComments({
81+
owner: context.repo.owner,
82+
repo: context.repo.repo,
83+
issue_number,
84+
per_page: 100
85+
});
86+
87+
const existing = comments.find((comment) => comment.body && comment.body.includes(marker));
88+
89+
if (existing) {
90+
await github.rest.issues.updateComment({
91+
owner: context.repo.owner,
92+
repo: context.repo.repo,
93+
comment_id: existing.id,
94+
body
95+
});
96+
} else {
97+
await github.rest.issues.createComment({
98+
owner: context.repo.owner,
99+
repo: context.repo.repo,
100+
issue_number,
101+
body
102+
});
103+
}

docs/APP_SCOPE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Current stage is a local proof slice with one real authenticated API surface, th
4040
- tested Neo4j Aura ingest library boundary for scrubbed construction graphs
4141
- sample proof runs from fixtures and shell smoke tests
4242
- API key auth middleware with constant-time comparison and fail-safe for unconfigured keys
43+
- GitHub Action boundary for PR score-diff comments (fixture-based RADE run on base/head refs)
4344

4445
## Implemented but explicitly not full product surfaces yet
4546

docs/ARCHITECTURE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Before write, report artifacts are scrubbed by `src/scrubber/pii_scrubber.py`.
6767
- `src/api/wsgi.py` is the served entrypoint for `/`, `/healthz`, and `POST /analyze`; it wraps the core `src/api/app.py` handler with API key auth middleware
6868
- `src/worker/main.py` emits staged telemetry but performs no real queue work
6969
- `web/lib/shell.mjs` serves the active web shell; `web/app/` is dormant scaffold only
70+
- Root `action.yml` defines a GitHub Action boundary that compares PR base/head fixture reports and posts score deltas for `reusability` and `accessibility_risk`
7071

7172
## Secondary blueprint path
7273

docs/BUILD_SHEET.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,14 @@ Accessibility-like tree -> construction graph -> deterministic SVG blueprint ->
3939
- `pnpm --dir web lint` -> `RADE web shell lint passed`
4040
- `pnpm --dir web test` -> `RADE web shell smoke test passed against http://127.0.0.1:56432`
4141
- `make proof` -> `All proof gates passed.`
42-
- `make analyze` -> produces `output/modernization_report.{json,md,html}`
42+
- `make analyze` -> produces `output/modernization_report.{json,md,html}`
43+
44+
### Milestone: GitHub Action PR score diff
45+
46+
- Added root `action.yml` (`RADE PR Score Diff`) with Marketplace metadata (`name`, `description`, `branding`) and composite steps to compare PR `base_sha` vs `head_sha`.
47+
- Added `.github/workflows/pr-score-diff.yml` to run on PR open/reopen/synchronize and invoke the local action.
48+
- Added `src/core/pr_score_diff.py` plus `scripts/pr_score_comment.py` to produce deterministic markdown comments with `reusability` and `accessibility_risk` deltas.
49+
- Added `tests/test_pr_score_diff.py` to lock comment marker/table format and score-delta computation.
4350

4451
### Milestone: Three real-world fixture pack
4552

@@ -100,10 +107,11 @@ The ignored `rade-repo/` subtree remains outside canonical repo truth and should
100107
- 2026-03-26 - public repo alignment: created `buildrr89/rade-engine`, switched repository posture to AGPL-3.0, updated public metadata/output wording, and re-generated checked-in proof artifacts to match the new public alpha story.
101108
- 2026-03-27 - interactive HTML report: `render_html_report()` produces self-contained HTML with score bars, expandable findings/recommendations, category filters, and priority badges. `--html-output` on CLI and agent CLI. Golden fixture and 15 new tests. 137 total tests passing.
102109
- 2026-03-27 - public alpha onboarding: improved README quickstart (Makefile-first, multiline commands, HTML output), expanded CONTRIBUTING with prerequisites/quickstart/formatting/where-to-start, added `make proof` target running all 6 gates via `.venv/bin/python`, added `--html-output` to Makefile analyze and CI workflow, added checked-in HTML example for python.org, fixed README API entry point to wsgi.py. 138 total tests passing.
110+
- 2026-03-27 - GitHub Action CI/CD integration: added root `action.yml` and `.github/workflows/pr-score-diff.yml` to run RADE on PR base/head refs and post/update a deterministic score-diff comment for `reusability` and `accessibility_risk`. Added supporting helper module/script and regression tests.
103111

104112
## Next immediate action
105113

106-
Build slice #17: GitHub Action for CI/CD integration.
114+
Define slice #18 in `docs/NEXT_EXECUTION_BACKLOG.md` (currently `UNKNOWN / NEEDS DECISION`) before implementation.
107115

108116
## Stop conditions
109117

docs/NEXT_EXECUTION_BACKLOG.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,16 @@
8989
- Status: implemented 2026-03-27
9090
- Result: README refactored with Makefile-first quickstart, multiline CLI commands, HTML output coverage, and corrected API entry point (`wsgi.py`). CONTRIBUTING expanded with prerequisites, clone-to-proof quickstart, formatting instructions, and where-to-start guidance. Makefile gains `make proof` target (all 6 gates via `.venv/bin/python`), `--html-output` in `make analyze`, and `*.html` in `make clean`. CI workflow updated to produce HTML output. Checked-in HTML example at `examples/python_org_homepage_report.html` with contract test. Examples section in README now links all three output formats. 138 total tests passing.
9191

92+
### 17. GitHub Action
93+
94+
- Status: implemented 2026-03-27
95+
- Result: added marketplace-ready root `action.yml` (`RADE PR Score Diff`) and PR workflow `.github/workflows/pr-score-diff.yml` to run on PR open/reopen/synchronize. The action compares base/head commits by running `rade analyze` against the fixture input and posts/updates a PR comment with deterministic `reusability` and `accessibility_risk` score deltas. Added helper module `src/core/pr_score_diff.py`, comment builder script `scripts/pr_score_comment.py`, and tests in `tests/test_pr_score_diff.py`.
96+
9297
## Backlog
9398

94-
### 17. GitHub Action
99+
### 18. UNKNOWN / NEEDS DECISION
95100

96-
- Risk reduced: developer adoption (no CI/CD integration exists)
97-
- Scope: GitHub Action that runs RADE on PRs and comments with a diff of accessibility/reusability scores
98-
- Acceptance: installable from GitHub Marketplace, runs on PR open/update, posts comment with scores
99-
- Does NOT include: Figma plugin, IDE extensions
101+
- Risk reduced: UNKNOWN / NEEDS DECISION
102+
- Scope: define the next smallest proof slice after GitHub Action adoption telemetry
103+
- Acceptance: explicit slice statement with deterministic proof gates
104+
- Does NOT include: unscoped platform expansion

scripts/pr_score_comment.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# SPDX-License-Identifier: AGPL-3.0-only
2+
from __future__ import annotations
3+
4+
import argparse
5+
from pathlib import Path
6+
7+
from src.core.pr_score_diff import build_score_diff, load_report, render_pr_comment
8+
9+
10+
def build_parser() -> argparse.ArgumentParser:
11+
parser = argparse.ArgumentParser(
12+
prog="pr-score-comment",
13+
description="Build a PR markdown comment for RADE score deltas.",
14+
)
15+
parser.add_argument("--base-report", type=Path, required=True)
16+
parser.add_argument("--head-report", type=Path, required=True)
17+
parser.add_argument("--base-ref", required=True)
18+
parser.add_argument("--head-ref", required=True)
19+
parser.add_argument("--output", type=Path, required=True)
20+
return parser
21+
22+
23+
def main() -> int:
24+
args = build_parser().parse_args()
25+
base_report = load_report(args.base_report)
26+
head_report = load_report(args.head_report)
27+
diff = build_score_diff(base_report, head_report)
28+
comment = render_pr_comment(diff, args.base_ref, args.head_ref)
29+
args.output.write_text(comment, encoding="utf-8")
30+
print(f"wrote: {args.output}")
31+
return 0
32+
33+
34+
if __name__ == "__main__":
35+
raise SystemExit(main())

src/core/pr_score_diff.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# SPDX-License-Identifier: AGPL-3.0-only
2+
"""Helpers for PR score diffs in GitHub Action comments."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
from pathlib import Path
8+
9+
COMMENT_MARKER = "<!-- rade-pr-score-comment -->"
10+
TRACKED_SCORES = ("reusability", "accessibility_risk")
11+
12+
13+
def load_report(path: Path) -> dict:
14+
return json.loads(path.read_text(encoding="utf-8"))
15+
16+
17+
def extract_score(report: dict, score_name: str) -> int:
18+
return int(report["scores"][score_name]["value"])
19+
20+
21+
def build_score_diff(base_report: dict, head_report: dict) -> dict[str, dict[str, int]]:
22+
diff: dict[str, dict[str, int]] = {}
23+
for score_name in TRACKED_SCORES:
24+
base_value = extract_score(base_report, score_name)
25+
head_value = extract_score(head_report, score_name)
26+
diff[score_name] = {
27+
"base": base_value,
28+
"head": head_value,
29+
"delta": head_value - base_value,
30+
}
31+
return diff
32+
33+
34+
def _format_delta(delta: int) -> str:
35+
if delta > 0:
36+
return f"+{delta}"
37+
return str(delta)
38+
39+
40+
def render_pr_comment(
41+
diff: dict[str, dict[str, int]], base_ref: str, head_ref: str
42+
) -> str:
43+
lines = [
44+
COMMENT_MARKER,
45+
"## RADE score diff",
46+
"",
47+
f"Compared `{base_ref}` -> `{head_ref}`.",
48+
"",
49+
"| Metric | Base | Head | Delta |",
50+
"|---|---:|---:|---:|",
51+
]
52+
for score_name in TRACKED_SCORES:
53+
values = diff[score_name]
54+
lines.append(
55+
f"| `{score_name}` | {values['base']} | {values['head']} | {_format_delta(values['delta'])} |"
56+
)
57+
lines.extend(
58+
[
59+
"",
60+
"_Generated by RADE GitHub Action._",
61+
]
62+
)
63+
return "\n".join(lines)

tests/test_pr_score_diff.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# SPDX-License-Identifier: AGPL-3.0-only
2+
from __future__ import annotations
3+
4+
from src.core.pr_score_diff import build_score_diff, render_pr_comment
5+
6+
7+
def _report(reusability: int, accessibility_risk: int) -> dict:
8+
return {
9+
"scores": {
10+
"reusability": {"value": reusability},
11+
"accessibility_risk": {"value": accessibility_risk},
12+
}
13+
}
14+
15+
16+
def test_build_score_diff_tracks_expected_metrics():
17+
base_report = _report(reusability=80, accessibility_risk=30)
18+
head_report = _report(reusability=85, accessibility_risk=42)
19+
20+
diff = build_score_diff(base_report, head_report)
21+
22+
assert diff == {
23+
"reusability": {"base": 80, "head": 85, "delta": 5},
24+
"accessibility_risk": {"base": 30, "head": 42, "delta": 12},
25+
}
26+
27+
28+
def test_render_pr_comment_has_stable_marker_and_table():
29+
comment = render_pr_comment(
30+
{
31+
"reusability": {"base": 80, "head": 75, "delta": -5},
32+
"accessibility_risk": {"base": 30, "head": 35, "delta": 5},
33+
},
34+
base_ref="base-sha",
35+
head_ref="head-sha",
36+
)
37+
38+
assert "<!-- rade-pr-score-comment -->" in comment
39+
assert "Compared `base-sha` -> `head-sha`." in comment
40+
assert "| `reusability` | 80 | 75 | -5 |" in comment
41+
assert "| `accessibility_risk` | 30 | 35 | +5 |" in comment

0 commit comments

Comments
 (0)