Skip to content

Commit 9c79110

Browse files
committed
feat(ci): Phase 08 — JSON reporter + GitHub Actions workflow + badges
Closes the E2E test plan. Turns the local ba_e2e_test.py runner into a CI-integrated quality gate that runs on every PR touching .agent/, docs, or outputs/. Changes: 1. ba_e2e_test.py — JSON reporter - New emit_json() function with stable schema v1.0.0 - --json OUTPUT_PATH CLI flag - Schema includes: $schema_version, fixture, repo_branch, repo_commit, started_at, duration_ms, exit_code, verdict, totals, per-layer checks with name/target/severity/message/duration_ms - ensure_ascii=False so Vietnamese fixture strings survive round-trip - Output is parseable by CI tooling or custom dashboards - When --json is passed without --report, markdown goes to stdout 2. .github/workflows/e2e-skills.yml — quality gate workflow - Triggers on PR + push to main/feat/** touching .agent/, sprint-spine, workflow-cookbook, outputs/, or the workflow itself - Python 3.11, actions/checkout@v4 with fetch-depth: 0 (ba_retro needs git history) - Runs full suite with --report + --json output - Writes markdown report to GITHUB_STEP_SUMMARY for PR review UX - Uploads reports/ as artifact (retention: 14 days) - Posts sticky PR comment with the markdown report (marocchino/sticky-pull-request-comment@v2) - Fails the job only if verdict is FAIL or CRASH (warnings don't block) - 10-minute timeout - Zero external dependencies (stdlib only, matches ba_*.py convention) 3. README.md + README.vi.md — CI status badge - GitHub Actions badge linked to the e2e-skills workflow - Shows pass/fail state per branch 4. .gitignore — reports/ directory - Add reports/ to ignore (CI runner output, per-run regenerated) - plans/reports/ remains gitignored via existing plans/ rule Verification: - ba_e2e_test.py still compiles and runs green (270/272 pass, 1.8s) - JSON schema smoke-tested: all 5 layers serialized, Vietnamese text round-trips via ensure_ascii=False - Workflow YAML has all required keys (name, on, jobs, steps, actions/checkout, actions/setup-python) — validated via substring grep - reports/test.md smoke test confirms reports/ is gitignored - plans/reports/ negation pattern preserved (still gitignored via plans/) Final E2E plan status: 8/8 phases complete. First GH Action run will execute on next push. Badge will render once the workflow runs at least once on a branch. Known limitations (defer to v3.5): - No trend tracking across runs (JSON history not persisted in CI) - No branch protection rules (manual decision per team policy) - No parallel layer execution (sequential is fast enough at 1.8s) - No matrix testing across Python versions (3.11 pinned)
1 parent 00032e8 commit 9c79110

File tree

5 files changed

+183
-1
lines changed

5 files changed

+183
-1
lines changed

.agent/scripts/ba_e2e_test.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,60 @@ def run_all(layers: list[str], fixture: Path, verbose: bool) -> AggregateVerdict
12101210
)
12111211

12121212

1213+
# ---------------------------------------------------------------------------
1214+
# JSON reporter (stable schema for CI consumption)
1215+
# ---------------------------------------------------------------------------
1216+
1217+
JSON_SCHEMA_VERSION = "1.0.0"
1218+
1219+
1220+
def emit_json(agg: AggregateVerdict) -> str:
1221+
"""Structured JSON for CI integration. Stable schema v1.0.0."""
1222+
payload = {
1223+
"$schema_version": JSON_SCHEMA_VERSION,
1224+
"fixture": agg.fixture,
1225+
"repo_branch": agg.repo_branch,
1226+
"repo_commit": agg.repo_commit,
1227+
"started_at": agg.started_at,
1228+
"duration_ms": round(agg.duration_ms, 1),
1229+
"exit_code": agg.exit_code,
1230+
"verdict": agg.verdict,
1231+
"totals": {
1232+
"passed": sum(l.passed for l in agg.layers),
1233+
"warned": sum(l.warned for l in agg.layers),
1234+
"failed": sum(l.failed for l in agg.layers),
1235+
"skipped": sum(l.skipped for l in agg.layers),
1236+
"total": sum(l.total for l in agg.layers),
1237+
},
1238+
"layers": [
1239+
{
1240+
"layer": l.layer,
1241+
"description": l.description,
1242+
"duration_ms": round(l.duration_ms, 1),
1243+
"totals": {
1244+
"passed": l.passed,
1245+
"warned": l.warned,
1246+
"failed": l.failed,
1247+
"skipped": l.skipped,
1248+
"total": l.total,
1249+
},
1250+
"checks": [
1251+
{
1252+
"name": c.name,
1253+
"target": c.target,
1254+
"severity": c.severity,
1255+
"message": c.message,
1256+
"duration_ms": round(c.duration_ms, 2),
1257+
}
1258+
for c in l.checks
1259+
],
1260+
}
1261+
for l in agg.layers
1262+
],
1263+
}
1264+
return json.dumps(payload, indent=2, ensure_ascii=False)
1265+
1266+
12131267
# ---------------------------------------------------------------------------
12141268
# Markdown reporter
12151269
# ---------------------------------------------------------------------------
@@ -1300,6 +1354,8 @@ def main(argv: list[str] | None = None) -> int:
13001354
help="Fixture project path (default: outputs/mini-app-cham-cong)")
13011355
p.add_argument("--report", default=None,
13021356
help="Markdown report output path (default: print to stdout)")
1357+
p.add_argument("--json", dest="json_out", default=None,
1358+
help="JSON report output path (for CI integration)")
13031359
p.add_argument("--verbose", "-v", action="store_true")
13041360
args = p.parse_args(argv)
13051361

@@ -1308,15 +1364,22 @@ def main(argv: list[str] | None = None) -> int:
13081364

13091365
agg = run_all(layers, fixture, args.verbose)
13101366
report = emit_markdown(agg)
1367+
json_payload = emit_json(agg)
13111368

13121369
if args.report:
13131370
out = Path(args.report)
13141371
out.parent.mkdir(parents=True, exist_ok=True)
13151372
out.write_text(report, encoding="utf-8")
13161373
print(f"Report: {out}", file=sys.stderr)
1317-
else:
1374+
elif not args.json_out:
13181375
print(report)
13191376

1377+
if args.json_out:
1378+
out = Path(args.json_out)
1379+
out.parent.mkdir(parents=True, exist_ok=True)
1380+
out.write_text(json_payload, encoding="utf-8")
1381+
print(f"JSON: {out}", file=sys.stderr)
1382+
13201383
print(f"Verdict: {agg.verdict} (exit {agg.exit_code}) — "
13211384
f"{agg.duration_ms / 1000:.1f}s", file=sys.stderr)
13221385
return agg.exit_code

.github/workflows/e2e-skills.yml

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
name: E2E Skills Test
2+
3+
# BA-Kit skill quality gate. Runs the ba_e2e_test.py orchestrator on every
4+
# PR touching agent skills, scripts, docs, or the fixture. Uses zero external
5+
# dependencies (Python stdlib only) so CI is fast and stable.
6+
7+
on:
8+
pull_request:
9+
paths:
10+
- '.agent/**'
11+
- 'docs/sprint-spine.md'
12+
- 'docs/workflow-cookbook.md'
13+
- 'outputs/**'
14+
- '.github/workflows/e2e-skills.yml'
15+
push:
16+
branches:
17+
- main
18+
- 'feat/**'
19+
paths:
20+
- '.agent/**'
21+
- 'docs/sprint-spine.md'
22+
- 'docs/workflow-cookbook.md'
23+
- 'outputs/**'
24+
- '.github/workflows/e2e-skills.yml'
25+
workflow_dispatch: {}
26+
27+
jobs:
28+
e2e:
29+
name: E2E Skills Suite (L1 → L5)
30+
runs-on: ubuntu-latest
31+
timeout-minutes: 10
32+
steps:
33+
- name: Checkout repo
34+
uses: actions/checkout@v4
35+
with:
36+
fetch-depth: 0 # ba_retro needs git history; shallow breaks it
37+
38+
- name: Set up Python 3.11
39+
uses: actions/setup-python@v5
40+
with:
41+
python-version: '3.11'
42+
43+
- name: Show environment
44+
run: |
45+
python3 --version
46+
git rev-parse --short HEAD
47+
find .agent/skills -maxdepth 1 -type d | wc -l
48+
find outputs -name '*.md' | wc -l
49+
50+
- name: Run E2E suite
51+
id: e2e
52+
run: |
53+
mkdir -p reports
54+
python3 .agent/scripts/ba_e2e_test.py \
55+
--report reports/e2e-report.md \
56+
--json reports/e2e-report.json \
57+
--verbose
58+
continue-on-error: true
59+
60+
- name: Display report in summary
61+
if: always()
62+
run: |
63+
if [ -f reports/e2e-report.md ]; then
64+
echo "## BA-Kit E2E Report" >> "$GITHUB_STEP_SUMMARY"
65+
cat reports/e2e-report.md >> "$GITHUB_STEP_SUMMARY"
66+
fi
67+
68+
- name: Parse verdict
69+
if: always()
70+
id: verdict
71+
run: |
72+
if [ -f reports/e2e-report.json ]; then
73+
VERDICT=$(python3 -c "import json; print(json.load(open('reports/e2e-report.json'))['verdict'])")
74+
EXIT=$(python3 -c "import json; print(json.load(open('reports/e2e-report.json'))['exit_code'])")
75+
echo "verdict=$VERDICT" >> "$GITHUB_OUTPUT"
76+
echo "exit=$EXIT" >> "$GITHUB_OUTPUT"
77+
echo "Verdict: $VERDICT (exit $EXIT)"
78+
else
79+
echo "verdict=CRASH" >> "$GITHUB_OUTPUT"
80+
echo "exit=99" >> "$GITHUB_OUTPUT"
81+
fi
82+
83+
- name: Upload reports as artifact
84+
if: always()
85+
uses: actions/upload-artifact@v4
86+
with:
87+
name: e2e-reports-${{ github.sha }}
88+
path: reports/
89+
retention-days: 14
90+
91+
- name: Post sticky PR comment
92+
if: github.event_name == 'pull_request' && always()
93+
uses: marocchino/sticky-pull-request-comment@v2
94+
continue-on-error: true
95+
with:
96+
header: e2e-skills
97+
path: reports/e2e-report.md
98+
99+
- name: Fail if verdict is FAIL
100+
if: steps.verdict.outputs.verdict == 'FAIL' || steps.verdict.outputs.verdict == 'CRASH'
101+
run: |
102+
echo "E2E suite failed — exit ${{ steps.verdict.outputs.exit }}"
103+
exit 2

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ scratch/
104104
# local .git/info/exclude or a project-specific override.
105105
.ba-kit/
106106

107+
# E2E test runner output (generated per-run)
108+
reports/
109+
!plans/reports/
110+
107111
# ============================================
108112
# Legacy / Archived content
109113
# ============================================

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
<img src="https://img.shields.io/badge/Integration-Jira%20%2B%20Confluence-blue?style=for-the-badge" alt="Jira + Confluence">
1818
</p>
1919

20+
<p align="center">
21+
<a href="https://github.com/olbboy/BA-Kit/actions/workflows/e2e-skills.yml">
22+
<img src="https://github.com/olbboy/BA-Kit/actions/workflows/e2e-skills.yml/badge.svg?branch=feat/mini-app-cham-cong-docs" alt="E2E Skills Test">
23+
</a>
24+
</p>
25+
2026
<h1 align="center">BA-Kit</h1>
2127
<h3 align="center">Agent Squad for Requirements Engineering</h3>
2228

README.vi.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
<img src="https://img.shields.io/badge/Capability-CMMI%20Level%205%20Enabler-purple?style=for-the-badge" alt="CMMI Level 5 Enabler">
1616
</p>
1717

18+
<p align="center">
19+
<a href="https://github.com/olbboy/BA-Kit/actions/workflows/e2e-skills.yml">
20+
<img src="https://github.com/olbboy/BA-Kit/actions/workflows/e2e-skills.yml/badge.svg?branch=feat/mini-app-cham-cong-docs" alt="E2E Skills Test">
21+
</a>
22+
</p>
23+
1824
<h1 align="center">BA-Kit</h1>
1925
<h3 align="center">Biệt Đội Chuyên Gia Phân Tích Nghiệp Vụ</h3>
2026

0 commit comments

Comments
 (0)