3131jobs :
3232 # Job 1: Quality checks (lint, test, build)
3333 quality-checks :
34- name : Lint, Test & Build
34+ name : Quality Checks
3535 runs-on : ubuntu-latest
3636
3737 steps :
@@ -94,43 +94,55 @@ jobs:
9494 path : dist/
9595 retention-days : 1
9696
97- review-thread-gate :
98- name : Review Threads Gate
97+ # Legacy required status check - reports quality-checks result for branch protection
98+ # Added to support migration from old "Lint, Test & Build" ruleset requirement
99+ expected-status :
100+ name : Lint, Test & Build
101+ runs-on : ubuntu-latest
102+ needs : [quality-checks]
103+ if : always()
104+
105+ steps :
106+ - name : Report status
107+ run : |
108+ if [ "${{ needs.quality-checks.result }}" = "success" ]; then
109+ echo "Lint/Test/Build passed."
110+ exit 0
111+ fi
112+ echo "Lint/Test/Build failed."
113+ exit 1
114+
115+ # Job 2: Build and test Docker image
116+ docker-build :
117+ name : Build Docker Image
99118 runs-on : ubuntu-latest
100- timeout-minutes : 5
101- # Only relevant for PRs; non-PR events skip the gate entirely.
102- if : github.event_name == 'pull_request'
119+ timeout-minutes : 30
120+ needs : [quality-checks]
103121
104122 permissions :
105- pull-requests : read
106123 contents : read
107-
108- outputs :
109- allow_heavy : ${{ steps.gate.outputs.allow_heavy }}
124+ pull-requests : read # For review thread check on PRs
110125
111126 steps :
112- - name : Check unresolved review threads
113- id : gate
127+ - name : Check for unresolved review threads
128+ if : github.event_name == 'pull_request'
114129 env :
115130 GH_TOKEN : ${{ github.token }}
116131 PR_NUMBER : ${{ github.event.pull_request.number }}
117132 REPO : ${{ github.repository }}
118133 run : |
119134 if ! command -v gh >/dev/null 2>&1; then
120- echo "gh CLI not available; allowing heavy jobs to proceed (fail-open)." >> "$GITHUB_STEP_SUMMARY"
121- echo "allow_heavy=true" >> "$GITHUB_OUTPUT"
135+ echo "gh CLI not available; skipping thread check (fail-open)." >> "$GITHUB_STEP_SUMMARY"
122136 exit 0
123137 fi
124138 if [ -z "$PR_NUMBER" ]; then
125- echo "allow_heavy=true" >> "$GITHUB_OUTPUT"
126- echo "PR_NUMBER is empty; allowing heavy jobs to proceed (fail-open)." >> "$GITHUB_STEP_SUMMARY"
139+ echo "PR_NUMBER is empty; skipping thread check (fail-open)." >> "$GITHUB_STEP_SUMMARY"
127140 exit 0
128141 fi
129142 OWNER="${REPO%/*}"
130143 NAME="${REPO#*/}"
131144 UNRESOLVED=0
132145 AFTER=""
133- # Hard cap to avoid unbounded pagination; 100 pages * 100 threads = 10,000 threads max.
134146 MAX_PAGES=100
135147 PAGE_COUNT=0
136148 while [ "$PAGE_COUNT" -lt "$MAX_PAGES" ]; do
@@ -151,9 +163,7 @@ jobs:
151163 -f owner="$OWNER" \
152164 -f name="$NAME" \
153165 -F number="$PR_NUMBER") || {
154- # Fail-open on API errors to avoid blocking contributor workflows.
155- echo "GitHub GraphQL API query for review threads failed; allowing heavy jobs to proceed (fail-open)." >> "$GITHUB_STEP_SUMMARY"
156- echo "allow_heavy=true" >> "$GITHUB_OUTPUT"
166+ echo "GraphQL query failed; skipping thread check (fail-open)." >> "$GITHUB_STEP_SUMMARY"
157167 exit 0
158168 }
159169 else
@@ -173,103 +183,41 @@ jobs:
173183 -f name="$NAME" \
174184 -F number="$PR_NUMBER" \
175185 -f after="$AFTER") || {
176- # Fail-open on API errors to avoid blocking contributor workflows.
177- echo "GitHub GraphQL API query for review threads failed; allowing heavy jobs to proceed (fail-open)." >> "$GITHUB_STEP_SUMMARY"
178- echo "allow_heavy=true" >> "$GITHUB_OUTPUT"
186+ echo "GraphQL query failed; skipping thread check (fail-open)." >> "$GITHUB_STEP_SUMMARY"
179187 exit 0
180188 }
181189 fi
182- # Count only active, unresolved, non-outdated review threads.
183- # GitHub marks a thread as "outdated" when the underlying code has changed
184- # (e.g., after new commits). For this gate, we deliberately ignore outdated
185- # threads so that only current, actionable discussions block heavy jobs.
186- # This keeps the gate aligned to actionable feedback and avoids blocking
187- # heavy jobs on stale discussions that no longer apply to the diff.
188- #
189- # If you want unresolved outdated threads to also block heavy jobs, remove
190- # "and .isOutdated == false" from the jq filter below.
190+ # Count only active, unresolved, non-outdated threads.
191+ # Outdated threads (code changed since comment) are excluded.
191192 PAGE_UNRESOLVED=$(echo "$RESP" | jq '[.data.repository.pullRequest.reviewThreads.nodes // [] | .[] | select(.isResolved == false and .isOutdated == false)] | length')
192193 PAGE_NODE_COUNT=$(echo "$RESP" | jq '(.data.repository.pullRequest.reviewThreads.nodes // []) | length')
193- if ! [[ "$PAGE_UNRESOLVED" =~ ^[0-9]+$ ]]; then
194- PAGE_UNRESOLVED=0
195- fi
196- if ! [[ "$PAGE_NODE_COUNT" =~ ^[0-9]+$ ]]; then
197- PAGE_NODE_COUNT=0
198- fi
194+ if ! [[ "$PAGE_UNRESOLVED" =~ ^[0-9]+$ ]]; then PAGE_UNRESOLVED=0; fi
195+ if ! [[ "$PAGE_NODE_COUNT" =~ ^[0-9]+$ ]]; then PAGE_NODE_COUNT=0; fi
199196 UNRESOLVED=$((UNRESOLVED + PAGE_UNRESOLVED))
200- # Only fetch the fields we need to evaluate the gate, to reduce payload size.
201197 HAS_NEXT=$(echo "$RESP" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
202- if [ "$HAS_NEXT" != "true" ]; then
203- break
204- fi
198+ if [ "$HAS_NEXT" != "true" ]; then break; fi
205199 AFTER=$(echo "$RESP" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
206- if [ "$AFTER" = "null" ]; then
207- AFTER=""
208- fi
209- # If endCursor is missing despite hasNextPage=true, stop to avoid looping.
200+ if [ "$AFTER" = "null" ]; then AFTER=""; fi
210201 if [ "$PAGE_NODE_COUNT" -eq 0 ] && [ "$HAS_NEXT" = "true" ]; then
211- echo "Warning: empty page while hasNextPage=true; stopping pagination to avoid loops." >> "$GITHUB_STEP_SUMMARY "
212- break
202+ echo "::error::Thread scan incomplete: empty page with hasNextPage=true. Blocking build for safety. "
203+ exit 1
213204 fi
214205 if [ "$HAS_NEXT" = "true" ] && [ -z "$AFTER" ]; then
215- echo "Warning: missing endCursor while hasNextPage=true; stopping pagination to avoid infinite loop." >> "$GITHUB_STEP_SUMMARY "
216- break
206+ echo "::error::Thread scan incomplete: missing endCursor with hasNextPage=true. Blocking build for safety. "
207+ exit 1
217208 fi
218209 done
219- if [ "$PAGE_COUNT" -ge "$MAX_PAGES" ]; then
220- # Fail-closed when pagination is incomplete: we cannot guarantee the thread count.
221- echo "Warning: reached maximum page limit while scanning review threads." >> "$GITHUB_STEP_SUMMARY"
222- echo "Review thread scan incomplete; blocking heavy jobs for safety." >> "$GITHUB_STEP_SUMMARY"
223- echo "allow_heavy=false" >> "$GITHUB_OUTPUT"
224- exit 0
210+ if [ "$PAGE_COUNT" -ge "$MAX_PAGES" ] && [ "$HAS_NEXT" = "true" ]; then
211+ echo "::error::Thread scan incomplete (exceeded $MAX_PAGES pages). Blocking build for safety."
212+ exit 1
225213 fi
214+ # Intentionally fail (not skip) — PRs with unresolved threads should not show green CI
226215 if [ "$UNRESOLVED" -gt 0 ]; then
227- echo "allow_heavy=false" >> "$GITHUB_OUTPUT"
228- echo "Unresolved review threads: $UNRESOLVED" >> "$GITHUB_STEP_SUMMARY"
229- exit 0
216+ echo "::error::$UNRESOLVED unresolved review thread(s). Resolve them before building."
217+ exit 1
230218 fi
231- echo "allow_heavy=true" >> "$GITHUB_OUTPUT"
232219 echo "Unresolved review threads: 0" >> "$GITHUB_STEP_SUMMARY"
233220
234- # Legacy required status check - reports quality-checks result for branch protection
235- # Added to support migration from old "Lint, Test & Build" ruleset requirement
236- expected-status :
237- name : Lint, Test & Build
238- runs-on : ubuntu-latest
239- needs : [quality-checks]
240- if : always()
241-
242- steps :
243- - name : Report status
244- run : |
245- if [ "${{ needs.quality-checks.result }}" = "success" ]; then
246- echo "Lint/Test/Build passed."
247- exit 0
248- fi
249- echo "Lint/Test/Build failed."
250- exit 1
251-
252- # Job 2: Build and test Docker image
253- docker-build :
254- name : Build Docker Image
255- runs-on : ubuntu-latest
256- needs : [quality-checks, review-thread-gate]
257- # For PRs, only run heavy jobs if the gate succeeded and explicitly allowed them.
258- # Non-PR events (push/workflow_dispatch) run unconditionally.
259- # Note: !cancelled() is required because review-thread-gate is skipped on non-PR events,
260- # and GitHub Actions skips dependent jobs by default when any needs job is skipped.
261- if : |
262- !cancelled() &&
263- needs.quality-checks.result == 'success' &&
264- (github.event_name != 'pull_request' || (needs.review-thread-gate.result == 'success' && needs.review-thread-gate.outputs.allow_heavy == 'true'))
265-
266- permissions :
267- contents : read
268-
269- outputs :
270- image-built : ${{ steps.build.outputs.digest }}
271-
272- steps :
273221 - name : Checkout code
274222 uses : actions/checkout@v6
275223
@@ -534,7 +482,9 @@ jobs:
534482 name : Pipeline Summary
535483 runs-on : ubuntu-latest
536484 permissions : {}
537- needs : [quality-checks, review-thread-gate, docker-build, semantic-release, mcpb-bundle, docker-publish, mcp-registry-publish]
485+ # All jobs listed so summary can read their outputs/results.
486+ # if: always() ensures this runs even when needs jobs are skipped/failed.
487+ needs : [quality-checks, docker-build, semantic-release, mcpb-bundle, docker-publish, mcp-registry-publish]
538488 if : always()
539489
540490 steps :
@@ -551,12 +501,6 @@ jobs:
551501
552502 if [[ "${{ needs.docker-build.result }}" == "success" ]]; then
553503 echo "✅ Docker build: Passed" >> $GITHUB_STEP_SUMMARY
554- elif [[ "${{ needs.docker-build.result }}" == "skipped" && "${{ needs.review-thread-gate.result }}" == "skipped" ]]; then
555- echo "⏭️ Docker build: Skipped (review gate not applicable)" >> $GITHUB_STEP_SUMMARY
556- elif [[ "${{ needs.docker-build.result }}" == "skipped" && "${{ needs.review-thread-gate.result }}" == "failure" ]]; then
557- echo "⏭️ Docker build: Skipped (review gate failed)" >> $GITHUB_STEP_SUMMARY
558- elif [[ "${{ needs.docker-build.result }}" == "skipped" && "${{ needs.review-thread-gate.outputs.allow_heavy }}" == "false" ]]; then
559- echo "⏭️ Docker build: Skipped (unresolved review threads)" >> $GITHUB_STEP_SUMMARY
560504 elif [[ "${{ needs.docker-build.result }}" == "skipped" ]]; then
561505 echo "⏭️ Docker build: Skipped" >> $GITHUB_STEP_SUMMARY
562506 else
0 commit comments