mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
fix(v3.9.4.2): harden 4 CI discipline gates from PR #149 codex post-ship review
Codex post-ship review on the PR #149 squash (commit be49a42) surfaced 4 P2
findings where the new gates would silently fail to enforce their intent.
Fixes:
F1 — harness-retirement-monthly.yml: scheduled runs lacked repo context for
`gh issue create`, so the monthly audit issue could not be opened.
Add `GH_REPO: ${{ github.repository }}`.
F2 — release-cooldown.yml: PREV_TAG lookup walked all tags, so a non-release
tag (e.g., the legacy `academic-research-skills--v3.7.0` plugin tag) could
shadow the previous release and bypass classification. Filter to `v*` tags.
F3 — release-cooldown.yml: hotfix detection read only the pointed commit
subject with a regex requiring a 4-segment tag for the `hot-fix` spelling.
v3.9.2 (subject: "Phase scope inflation hot-fix") was classified as
non-hotfix under the old logic. Now also reads annotated tag subject via
`git for-each-ref` and accepts both `hotfix` and `hot-fix` spellings.
F4 — test-count-monotonic.yml: `pytest --collect-only -q | grep -c '::'`
swallowed pytest's non-zero exit on collection errors. A broken import
during collection could still produce a count high enough to pass the
monotonic check. Run pytest separately, fail on any non-zero exit other
than 5 (no tests collected), then count from the captured output.
Smoke test (local, against repo's real tags) confirms:
- v3.9.4.1 → hotfix=true (4-segment + commit + annotated tag all match)
- v3.9.2 → hotfix=true (now caught via `hot-fix` spelling; old regex missed)
- v3.9.3, v3.9.4, v3.8.0 → hotfix=false (correct, normal releases)
- `git tag -l 'v*'` excludes the 1 known non-release tag
Closes #152.
This commit is contained in:
@@ -37,6 +37,7 @@ jobs:
|
||||
- name: Open audit tracking issue
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
MONTH: ${{ steps.month.outputs.label }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -37,22 +37,33 @@ jobs:
|
||||
|
||||
echo "New tag: $NEW_TAG"
|
||||
|
||||
# Find previous tag (by creator date, exclude current)
|
||||
PREV_TAG=$(git tag --sort=-creatordate | grep -v "^${NEW_TAG}$" | head -1 || true)
|
||||
# Find previous release tag (v*, by creator date, exclude current).
|
||||
# Filtering to v* avoids non-release tags (e.g., test, internal markers)
|
||||
# being picked as PREV_TAG and bypassing the cooldown classification.
|
||||
PREV_TAG=$(git tag -l 'v*' --sort=-creatordate | grep -v "^${NEW_TAG}$" | head -1 || true)
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
echo "No previous tag — first release, skip cooldown."
|
||||
echo "No previous release tag — first release, skip cooldown."
|
||||
exit 0
|
||||
fi
|
||||
echo "Previous tag: $PREV_TAG"
|
||||
|
||||
# Was the previous tag a hotfix?
|
||||
# Heuristic: tag name has 4 dot-segments (v3.9.4.1) OR pointed commit subject matches hotfix pattern.
|
||||
PREV_SUBJECT=$(git log -1 --format=%s "$PREV_TAG")
|
||||
# Three signals (any one is sufficient):
|
||||
# a) Tag name has 4 dot-segments (e.g., v3.9.4.1)
|
||||
# b) Pointed commit subject matches hotfix pattern
|
||||
# c) Annotated tag subject (if any) matches hotfix pattern
|
||||
# The regex accepts both 'hotfix' and 'hot-fix' spellings.
|
||||
PREV_COMMIT_SUBJECT=$(git log -1 --format=%s "$PREV_TAG")
|
||||
PREV_TAG_SUBJECT=$(git for-each-ref "refs/tags/${PREV_TAG}" --format='%(subject)')
|
||||
HOTFIX_REGEX='(hot-?fix|post-ship|^fix\(v)'
|
||||
IS_HOTFIX="false"
|
||||
if [[ "$PREV_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
IS_HOTFIX="true"
|
||||
fi
|
||||
if echo "$PREV_SUBJECT" | grep -qiE '(hotfix|post-ship|^fix\(v)'; then
|
||||
if echo "$PREV_COMMIT_SUBJECT" | grep -qiE "$HOTFIX_REGEX"; then
|
||||
IS_HOTFIX="true"
|
||||
fi
|
||||
if [ -n "$PREV_TAG_SUBJECT" ] && echo "$PREV_TAG_SUBJECT" | grep -qiE "$HOTFIX_REGEX"; then
|
||||
IS_HOTFIX="true"
|
||||
fi
|
||||
echo "Previous tag classified as hotfix: $IS_HOTFIX"
|
||||
|
||||
@@ -50,7 +50,23 @@ jobs:
|
||||
id: head
|
||||
run: |
|
||||
set -euo pipefail
|
||||
COUNT=$(pytest --collect-only -q 2>/dev/null | grep -c '::' || true)
|
||||
# Run pytest collection separately so collection errors fail the gate.
|
||||
# Without this, `pytest ... | grep -c` would mask non-zero pytest exits
|
||||
# (e.g., import errors during collection) and let a broken PR pass.
|
||||
set +e
|
||||
pytest --collect-only -q > pytest-collect.txt 2> pytest-collect.err
|
||||
PYTEST_EXIT=$?
|
||||
set -e
|
||||
# Tolerate exit 5 (no tests collected — empty repo edge case);
|
||||
# any other non-zero exit is a real collection failure.
|
||||
if [ "$PYTEST_EXIT" -ne 0 ] && [ "$PYTEST_EXIT" -ne 5 ]; then
|
||||
echo "::error::pytest collection failed on head (exit $PYTEST_EXIT)"
|
||||
cat pytest-collect.err >&2 || true
|
||||
tail -20 pytest-collect.txt >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
# grep -c returns 1 when no matches — tolerate only that case.
|
||||
COUNT=$(grep -c '::' pytest-collect.txt || true)
|
||||
echo "Head test count: $COUNT"
|
||||
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -76,7 +92,18 @@ jobs:
|
||||
working-directory: ../base
|
||||
run: |
|
||||
set -euo pipefail
|
||||
COUNT=$(pytest --collect-only -q 2>/dev/null | grep -c '::' || true)
|
||||
# Same discipline as the head count: collection errors fail the gate.
|
||||
set +e
|
||||
pytest --collect-only -q > pytest-collect.txt 2> pytest-collect.err
|
||||
PYTEST_EXIT=$?
|
||||
set -e
|
||||
if [ "$PYTEST_EXIT" -ne 0 ] && [ "$PYTEST_EXIT" -ne 5 ]; then
|
||||
echo "::error::pytest collection failed on base (exit $PYTEST_EXIT)"
|
||||
cat pytest-collect.err >&2 || true
|
||||
tail -20 pytest-collect.txt >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
COUNT=$(grep -c '::' pytest-collect.txt || true)
|
||||
echo "Base test count: $COUNT"
|
||||
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user