Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,81 @@ jobs:
pip install --upgrade pip
pip install -e .[dev]

- name: Collect pytest count
run: |
set -o pipefail
python -m pytest tests/ --collect-only -q 2>&1 | tee pytest-collect.log

- name: Run full release verification
run: bash scripts/verify-release.sh
run: |
set -o pipefail
bash scripts/verify-release.sh 2>&1 | tee verify-release.log

- name: Verify collected == passed or reported skips
run: |
python - <<'PY'
from __future__ import annotations

import re
import sys
from pathlib import Path

collect_log = Path("pytest-collect.log").read_text(encoding="utf-8", errors="replace")
run_log = Path("verify-release.log").read_text(encoding="utf-8", errors="replace")

collected_matches = re.findall(r"(\d+)\s+tests?\s+collected", collect_log)
if not collected_matches:
print("::error::Could not parse pytest collection count from pytest-collect.log.")
sys.exit(1)
collected = int(collected_matches[-1])

summary_matches = re.findall(
r"=+\s*([^=\n]*\b(?:passed|failed|error|skipped|xfailed|xpassed|deselected)\b[^=\n]*)\s*=+",
run_log,
)
if not summary_matches:
print("::error::Could not parse pytest result summary from verify-release.log.")
sys.exit(1)
summary = summary_matches[0]

def count(label: str) -> int:
match = re.search(rf"(\d+)\s+{label}", summary)
return int(match.group(1)) if match else 0

passed = count("passed")
skipped = count("skipped")
deselected = count("deselected")
failed = count("failed")
errors = count("errors?")
xfailed = count("xfailed")
xpassed = count("xpassed")
accounted = passed + skipped + deselected

print(f"collected={collected}")
print(f"passed={passed}")
print(f"skipped={skipped}")
print(f"deselected={deselected}")
if failed or errors or xfailed or xpassed:
print(
f"::error::Unexpected pytest non-pass outcomes: failed={failed} "
f"errors={errors} xfailed={xfailed} xpassed={xpassed}."
)
sys.exit(1)
if collected != accounted:
print(
f"::error::Collection/run mismatch: collected={collected} "
f"accounted={accounted} passed={passed} skipped={skipped} deselected={deselected}."
)
print("::error::Investigate silent test loss, early exit, or unreported pytest outcomes.")
sys.exit(1)
if collected != passed:
print(
f"::notice::Collected count differs from passed only by reported skips/deselections "
f"(skipped={skipped}, deselected={deselected})."
)
PY

- name: Accessibility gate (no browser surface)
run: |
echo "CivicCore exposes shared backend contracts and has no operator-facing browser surface in this repo."
echo "Per-PR axe gates run in CivicRecords AI, CivicClerk, CivicCode, and the CivicSuite launcher."
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ desktop.ini

# Generated release-provenance fixture scratch
tests/fixtures/release_provenance/.generated/
.agent-runs/
Loading