88 - " packages/ats/**"
99 - " apps/ats/**"
1010 - " package.json"
11+ - " codecov.yml"
1112 - " .github/workflows/*ats*.yaml"
1213 workflow_dispatch :
1314
@@ -17,6 +18,7 @@ defaults:
1718
1819permissions :
1920 contents : read
21+ statuses : read # gate step reads the codecov/project commit status
2022
2123jobs :
2224 test-ats :
@@ -99,3 +101,33 @@ jobs:
99101 - name : Upload coverage report
100102 if : ${{ steps.generate_coverage.outcome == 'success' }}
101103 uses : codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v5.4.0
104+
105+ # Codecov evaluates coverage server-side and posts the `codecov/project`
106+ # commit status asynchronously, after the upload returns. This step waits
107+ # for that status on the PR head commit and fails the job if coverage
108+ # dropped versus the base branch's last uploaded report (target: auto +
109+ # threshold: 0% in codecov.yml).
110+ - name : Enforce coverage did not decrease
111+ if : ${{ github.event_name == 'pull_request' && steps.generate_coverage.outcome == 'success' }}
112+ env :
113+ GH_TOKEN : ${{ github.token }}
114+ HEAD_SHA : ${{ github.event.pull_request.head.sha }}
115+ REPO : ${{ github.repository }}
116+ run : |
117+ echo "Waiting for Codecov to post codecov/project for ${HEAD_SHA}..."
118+ state=""
119+ for attempt in $(seq 1 30); do
120+ state=$(gh api "repos/${REPO}/commits/${HEAD_SHA}/statuses" \
121+ --jq 'map(select(.context == "codecov/project")) | .[0].state // ""')
122+ if [ -n "${state}" ] && [ "${state}" != "pending" ]; then
123+ break
124+ fi
125+ echo " attempt ${attempt}/30: state='${state:-<none>}' — retrying in 10s"
126+ sleep 10
127+ done
128+ echo "Final codecov/project state: '${state:-<none>}'"
129+ if [ "${state}" != "success" ]; then
130+ echo "::error::Coverage decreased versus the base branch's last report (codecov/project='${state:-none/timed-out}')."
131+ exit 1
132+ fi
133+ echo "✓ Coverage did not decrease"
0 commit comments