chore(release): v9.2.0 - read-only MCP, tamper-evident audit signing, SIEM export #116
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Regression Test Required | |
| # Implements the W12 rule from the Quality Freeze epic (#1697 / QF-19): | |
| # every bug-fix PR must include a test at the layer that failed. | |
| # | |
| # A PR is treated as a bug fix when: | |
| # * its title matches the Conventional Commits "fix" type — i.e. starts with | |
| # "fix:", "fix(scope):", "fix!:" (breaking), or "fix(scope)!:" — OR | |
| # * it carries the "bug" label | |
| # | |
| # Escape hatch: the "regression-test-exempt" label skips the gate. Use only | |
| # when a regression test is genuinely impractical (e.g. infra-only fix, build | |
| # config, generated artifact) and justify in the PR body. | |
| # | |
| # See CONTRIBUTING.md ("Regression-test-per-bug") for the full rationale and | |
| # accepted test-file patterns. | |
| on: | |
| pull_request: | |
| branches: [main] | |
| # Limited to events that materially change diff content or gating eligibility: | |
| # opened — first eval | |
| # synchronize — new commits pushed | |
| # labeled — `bug` / `regression-test-exempt` toggles classification | |
| # Dropped (no diff change): edited (title/body), unlabeled, reopened. | |
| types: [opened, synchronize, labeled] | |
| permissions: | |
| contents: read | |
| jobs: | |
| check-regression-test: | |
| name: Check Regression Test Present | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout PR head with merge-base history | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Determine if PR is a bug fix | |
| id: classify | |
| env: | |
| PR_TITLE: ${{ github.event.pull_request.title }} | |
| LABELS_JSON: ${{ toJson(github.event.pull_request.labels.*.name) }} | |
| run: | | |
| set -euo pipefail | |
| echo "PR title: $PR_TITLE" | |
| echo "Labels: $LABELS_JSON" | |
| is_bug_fix=false | |
| has_exempt=false | |
| # Match the Conventional Commits "fix" type, including the breaking | |
| # variants without scope ("fix!:") that the previous glob check missed. | |
| # Pattern must live in a variable: an inline regex with parentheses | |
| # in `[[ =~ ... ]]` triggers a bash parser error on unescaped ')'. | |
| fix_title_regex='^fix(\([^)]*\))?!?:' | |
| if [[ "$PR_TITLE" =~ $fix_title_regex ]]; then | |
| is_bug_fix=true | |
| fi | |
| if echo "$LABELS_JSON" | grep -q '"bug"'; then | |
| is_bug_fix=true | |
| fi | |
| if echo "$LABELS_JSON" | grep -q '"regression-test-exempt"'; then | |
| has_exempt=true | |
| fi | |
| echo "is_bug_fix=$is_bug_fix" >> "$GITHUB_OUTPUT" | |
| echo "has_exempt=$has_exempt" >> "$GITHUB_OUTPUT" | |
| - name: Skip — not a bug fix | |
| if: steps.classify.outputs.is_bug_fix != 'true' | |
| run: | | |
| echo "PR is not classified as a bug fix (title does not match a Conventional Commits 'fix' type, and 'bug' label not applied)." | |
| echo "Regression-test-per-bug gate does not apply." | |
| - name: Skip — exempt label applied | |
| if: steps.classify.outputs.is_bug_fix == 'true' && steps.classify.outputs.has_exempt == 'true' | |
| run: | | |
| echo "::warning::PR carries 'regression-test-exempt' label — regression-test-per-bug gate skipped." | |
| echo "Reviewers: confirm the PR body justifies why a regression test is impractical." | |
| echo "See CONTRIBUTING.md ('Regression-test-per-bug') for accepted exemption rationales." | |
| - name: Verify a test file is present in the diff | |
| if: steps.classify.outputs.is_bug_fix == 'true' && steps.classify.outputs.has_exempt != 'true' | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| echo "Diffing $BASE_SHA...$HEAD_SHA" | |
| # We capture TWO diffs: | |
| # 1. All changed paths (for the diagnostic listing and the | |
| # "deletion/rename does not satisfy the gate" warning) | |
| # 2. Added or Modified paths only — the gate ONLY passes on test | |
| # files that show up in this set. Deletions (D), pure renames | |
| # (R, with --no-renames forcing them to A+D so D is filtered), | |
| # and copies (C) don't count as new regression coverage. | |
| # | |
| # Why: the previous bare `git diff --name-only` accepted any changed | |
| # test-ish path, so the gate could be satisfied by deleting | |
| # foo_test.go, by a rename of an unrelated test, or by touching a | |
| # comment-only line in a tests/ fixture. Bug fixes need ADDED or | |
| # MODIFIED regression coverage at the failing layer. | |
| all_diff=$(mktemp) | |
| am_diff=$(mktemp) | |
| if ! git diff --name-only "$BASE_SHA...$HEAD_SHA" > "$all_diff"; then | |
| echo "::error::git diff failed for $BASE_SHA...$HEAD_SHA — the base or head SHA may be unreachable (force-push?). Re-running the workflow on a fresh PR sync usually fixes this." | |
| exit 1 | |
| fi | |
| if ! git diff --diff-filter=AM --no-renames --name-only "$BASE_SHA...$HEAD_SHA" > "$am_diff"; then | |
| echo "::error::git diff (--diff-filter=AM) failed for $BASE_SHA...$HEAD_SHA." | |
| exit 1 | |
| fi | |
| mapfile -t changed_files < "$all_diff" | |
| mapfile -t am_files < "$am_diff" | |
| rm -f "$all_diff" "$am_diff" | |
| if (( ${#changed_files[@]} == 0 )); then | |
| echo "::error::Diff produced no changed files. Refusing to pass — re-run after pushing or syncing the PR." | |
| exit 1 | |
| fi | |
| echo "Changed files (all operations):" | |
| printf ' %s\n' "${changed_files[@]}" | |
| echo | |
| echo "Added or Modified files (eligible for regression-test credit):" | |
| if (( ${#am_files[@]} == 0 )); then | |
| echo " (none)" | |
| else | |
| printf ' %s\n' "${am_files[@]}" | |
| fi | |
| echo | |
| # The "under tests/ directory" branch is restricted to code extensions | |
| # so a bug-fix PR can't satisfy the gate by editing a JSON snapshot, a | |
| # YAML fixture, a markdown helper, or any other non-executable file | |
| # under tests/. The naming-convention branches (foo_test.go, FooTest.java, | |
| # *.test.tsx, etc.) already imply code; only the directory fallback | |
| # needed narrowing. | |
| test_pattern='(_test\.go$|_test\.py$|\.test\.tsx?$|\.spec\.tsx?$|Test\.java$|Tests\.java$|IT\.java$|(^|/)tests?/.*\.(go|py|tsx?|java|sh|rb|rs|kt)$)' | |
| matched=() | |
| for f in "${am_files[@]}"; do | |
| if [[ "$f" =~ $test_pattern ]]; then | |
| matched+=("$f") | |
| fi | |
| done | |
| if (( ${#matched[@]} > 0 )); then | |
| echo "Found ${#matched[@]} added or modified test file(s) in the diff:" | |
| printf ' %s\n' "${matched[@]}" | |
| echo | |
| echo "Regression-test-per-bug gate: PASS" | |
| exit 0 | |
| fi | |
| # Surface deletion/rename traps so authors know why they didn't pass. | |
| touched_but_not_am=() | |
| for f in "${changed_files[@]}"; do | |
| if [[ "$f" =~ $test_pattern ]]; then | |
| found_in_am=false | |
| for am in "${am_files[@]}"; do | |
| if [[ "$am" == "$f" ]]; then | |
| found_in_am=true | |
| break | |
| fi | |
| done | |
| if [[ "$found_in_am" == "false" ]]; then | |
| touched_but_not_am+=("$f") | |
| fi | |
| fi | |
| done | |
| if (( ${#touched_but_not_am[@]} > 0 )); then | |
| echo "::warning::Test paths touched only via deletion or rename (not eligible):" | |
| printf ' %s\n' "${touched_but_not_am[@]}" | |
| echo "Deletions and pure renames don't satisfy the gate — add or modify a test." | |
| echo | |
| fi | |
| echo "::error::No added or modified regression test found in this bug-fix PR." | |
| echo | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| echo "REGRESSION-TEST-PER-BUG GATE FAILED" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| echo | |
| echo "This PR is classified as a bug fix but adds no ADDED or MODIFIED test" | |
| echo "at the layer that failed. Per the W12 rule (Quality Freeze epic #1697" | |
| echo "/ QF-19), every bug fix must include a regression test that would have" | |
| echo "caught the bug. Deletions and pure renames of existing tests don't count." | |
| echo | |
| echo "Accepted test-file patterns:" | |
| echo " *_test.go Go tests" | |
| echo " *_test.py Python tests" | |
| echo " *.test.ts(x) TypeScript tests" | |
| echo " *.spec.ts(x) TypeScript spec tests" | |
| echo " *Test.java JUnit tests" | |
| echo " *Tests.java JUnit tests" | |
| echo " *IT.java Java integration tests" | |
| echo " tests/** or test/** with a code extension .go .py .ts .tsx .java" | |
| echo " .sh .rb .rs .kt only" | |
| echo | |
| echo "Non-code files under tests/ (JSON snapshots, YAML fixtures, markdown," | |
| echo "etc.) DO NOT satisfy the gate — the test must exercise the failing layer." | |
| echo | |
| echo "If a regression test is genuinely impractical (infra-only fix," | |
| echo "build config, generated artifact, etc.), apply the" | |
| echo "'regression-test-exempt' label and justify in the PR body." | |
| echo | |
| echo "See CONTRIBUTING.md ('Regression-test-per-bug') for the full guidance." | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| exit 1 |