fix(ship-it): wait for every workflow run in tea-ci-watch #13

Merged
rootiest merged 1 commits from fix/ship-it-ci-watch-all-runs into main 2026-10-01 08:30:20 +00:00
13 changed files with 249 additions and 113 deletions

No files matched your search

+12 -1
View File
@@ -167,7 +167,7 @@ or want an auditable record of what changed and why.
**Purpose:** Run a comprehensive pre-flight audit and publish changes to a new
PR in a single command.
Two sequential phases — Phase 2 is blocked until Phase 1 succeeds:
Sequential phases — each is blocked until the previous one succeeds:
1. **Documentation Sync & Code Audit** — acts as `/docs-sync-audit`: scans all
file changes since the last documentation edit, updates the docs/README to
@@ -177,6 +177,17 @@ Two sequential phases — Phase 2 is blocked until Phase 1 succeeds:
descriptively named branch, commits all pending changes (including the
README updates from Phase 1), pushes to the remote, and opens a Pull Request
against `main`.
3. **CI Verification** — waits on the PR head commit with a single background
watcher instead of sleep-polling. On GitHub it uses
`gh pr checks --watch --fail-fast`; on Gitea it runs the bundled
`scripts/tea-ci-watch.sh` (requires `tea` and `jq`), which waits until every
Actions run for the commit — including runs still queued, which Gitea does
not yet report as commit statuses — has completed and the combined commit
status is no longer pending. Fails fast on the first failed or cancelled
check. Then reports the PR link and CI result and stops; you merge.
4. **Post-Merge Sync** — only when you say the PR is merged (or ask it to
merge): watches CI on the merge commit the same way, then fast-forwards
local `main` to pick up any automated commits.
**Use when:** you say "Ship this," "Publish my changes," or invoke `/ship-it`.
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: ship-it
description: Runs a comprehensive pre-flight audit, syncs the README, and publishes the changes to a new PR.
version: 1.2.0
version: 1.2.1
user-invocable: true
author: Rootiest
---
@@ -15,10 +15,16 @@ SYNOPSIS
tea-ci-watch [OPTIONS] <owner/repo> <commit-sha> [timeout_seconds]
DESCRIPTION
Polls the Gitea combined commit status API (via 'tea api') and blocks until
every check on the commit has finished. Designed for automated CI pipelines
and agent workflows: run it once, in the background, and act on the exit
code instead of sleep-polling.
Polls Gitea (via 'tea api') and blocks until every check on the commit has
finished. Designed for automated CI pipelines and agent workflows: run it
once, in the background, and act on the exit code instead of sleep-polling.
Two sources are combined. Gitea posts an Actions job's commit status only
once a runner picks the job up, so queued workflows are invisible to the
combined status; the Actions runs API lists them from the moment they are
triggered. The commit is done only when every Actions run for it has
completed AND the combined status (which also covers external CI) is no
longer pending. The first failure in either source fails fast.
The combined status reports the latest state of each check, so earlier
'pending' entries for re-run jobs do not affect the result. Skipped jobs
@@ -41,7 +47,8 @@ AUTHENTICATION
RETURNS
0 All checks passed (success, warning, or skipped)
1 One or more checks failed; failing jobs are printed
1 One or more checks failed or were cancelled; failing
checks are printed
2 Usage, dependency, or API error (bad repo, auth, network)
3 No checks registered within the grace period
124 Timeout reached with checks still pending
@@ -81,30 +88,48 @@ INTERVAL=5
echo "Waiting for CI completion on ${REPO}@${SHA:0:7}..." >&2
while ((SECONDS < TIMEOUT)); do
# 'tea api' exits 0 even on HTTP errors, so a missing .state is the error signal
if ! RESPONSE=$(tea api "/repos/${REPO}/commits/${SHA}/status" 2>&1) ||
! STATE=$(jq -er '.state' <<<"$RESPONSE" 2>/dev/null); then
echo "ERROR: Status request failed for ${REPO}@${SHA:0:7}: ${RESPONSE}" >&2
# Fetch a Gitea API path as JSON. 'tea api' exits 0 even on HTTP errors, so the
# caller-supplied jq probe (which must succeed on a valid body) is the error signal.
fetch() {
local path="$1" probe="$2" body
if ! body=$(tea api "$path" 2>&1) || ! jq -e "$probe" <<<"$body" >/dev/null 2>&1; then
echo "ERROR: Request ${path} failed for ${REPO}@${SHA:0:7}: ${body}" >&2
exit 2
fi
COUNT=$(jq -r '.total_count' <<<"$RESPONSE")
printf '%s' "$body"
}
case "$STATE" in
success | warning)
echo "SUCCESS: All ${COUNT} checks passed for ${SHA:0:7}." >&2
exit 0
;;
failure | error)
while ((SECONDS < TIMEOUT)); do
STATUS=$(fetch "/repos/${REPO}/commits/${SHA}/status" '.state | strings') || exit 2
RUNS=$(fetch "/repos/${REPO}/actions/runs?head_sha=${SHA}&limit=50" '.workflow_runs | arrays') || exit 2
STATE=$(jq -r '.state' <<<"$STATUS")
COUNT=$(jq -r '.total_count' <<<"$STATUS")
RUN_COUNT=$(jq -r '.workflow_runs | length' <<<"$RUNS")
RUNS_PENDING=$(jq -r '[.workflow_runs[] | select(.status != "completed")] | length' <<<"$RUNS")
RUNS_FAILED=$(jq -r '[.workflow_runs[] | select(.status == "completed"
and (.conclusion == "failure" or .conclusion == "cancelled"))] | length' <<<"$RUNS")
if [[ "$STATE" == failure || "$STATE" == error ]] || ((RUNS_FAILED > 0)); then
echo "ERROR: One or more checks failed for ${SHA:0:7}:" >&2
jq -r '.statuses[] | select(.status == "failure" or .status == "error")
| " \(.context): \(.description) (\(.target_url))"' <<<"$RESPONSE" >&2
| " \(.context): \(.description) (\(.target_url))"' <<<"$STATUS" >&2
jq -r '.workflow_runs[] | select(.status == "completed"
and (.conclusion == "failure" or .conclusion == "cancelled"))
| " \(.path) [\(.event)]: \(.conclusion) (\(.html_url))"' <<<"$RUNS" >&2
exit 1
;;
esac
fi
# Done only when no run is queued/running and every posted status is terminal.
# Zero statuses with completed runs means every job was skipped (no status posted).
if ((RUNS_PENDING == 0)) && { [[ "$STATE" == success || "$STATE" == warning ]] ||
((COUNT == 0 && RUN_COUNT > 0)); }; then
echo "SUCCESS: All ${RUN_COUNT} workflow runs and ${COUNT} checks passed for ${SHA:0:7}." >&2
exit 0
fi
# A commit with no workflows reports "pending" with zero checks forever
if ((COUNT == 0 && SECONDS >= GRACE)); then
if ((COUNT == 0 && RUN_COUNT == 0 && SECONDS >= GRACE)); then
echo "NO CI: No checks registered for ${SHA:0:7} after ${GRACE}s." >&2
exit 3
fi
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: ship-it
description: Runs a comprehensive pre-flight audit, syncs the README, and publishes the changes to a new PR.
version: 1.2.0
version: 1.2.1
user-invocable: true
author: Rootiest
---
+46 -21
View File
@@ -15,10 +15,16 @@ SYNOPSIS
tea-ci-watch [OPTIONS] <owner/repo> <commit-sha> [timeout_seconds]
DESCRIPTION
Polls the Gitea combined commit status API (via 'tea api') and blocks until
every check on the commit has finished. Designed for automated CI pipelines
and agent workflows: run it once, in the background, and act on the exit
code instead of sleep-polling.
Polls Gitea (via 'tea api') and blocks until every check on the commit has
finished. Designed for automated CI pipelines and agent workflows: run it
once, in the background, and act on the exit code instead of sleep-polling.
Two sources are combined. Gitea posts an Actions job's commit status only
once a runner picks the job up, so queued workflows are invisible to the
combined status; the Actions runs API lists them from the moment they are
triggered. The commit is done only when every Actions run for it has
completed AND the combined status (which also covers external CI) is no
longer pending. The first failure in either source fails fast.
The combined status reports the latest state of each check, so earlier
'pending' entries for re-run jobs do not affect the result. Skipped jobs
@@ -41,7 +47,8 @@ AUTHENTICATION
RETURNS
0 All checks passed (success, warning, or skipped)
1 One or more checks failed; failing jobs are printed
1 One or more checks failed or were cancelled; failing
checks are printed
2 Usage, dependency, or API error (bad repo, auth, network)
3 No checks registered within the grace period
124 Timeout reached with checks still pending
@@ -81,30 +88,48 @@ INTERVAL=5
echo "Waiting for CI completion on ${REPO}@${SHA:0:7}..." >&2
while ((SECONDS < TIMEOUT)); do
# 'tea api' exits 0 even on HTTP errors, so a missing .state is the error signal
if ! RESPONSE=$(tea api "/repos/${REPO}/commits/${SHA}/status" 2>&1) ||
! STATE=$(jq -er '.state' <<<"$RESPONSE" 2>/dev/null); then
echo "ERROR: Status request failed for ${REPO}@${SHA:0:7}: ${RESPONSE}" >&2
# Fetch a Gitea API path as JSON. 'tea api' exits 0 even on HTTP errors, so the
# caller-supplied jq probe (which must succeed on a valid body) is the error signal.
fetch() {
local path="$1" probe="$2" body
if ! body=$(tea api "$path" 2>&1) || ! jq -e "$probe" <<<"$body" >/dev/null 2>&1; then
echo "ERROR: Request ${path} failed for ${REPO}@${SHA:0:7}: ${body}" >&2
exit 2
fi
COUNT=$(jq -r '.total_count' <<<"$RESPONSE")
printf '%s' "$body"
}
case "$STATE" in
success | warning)
echo "SUCCESS: All ${COUNT} checks passed for ${SHA:0:7}." >&2
exit 0
;;
failure | error)
while ((SECONDS < TIMEOUT)); do
STATUS=$(fetch "/repos/${REPO}/commits/${SHA}/status" '.state | strings') || exit 2
RUNS=$(fetch "/repos/${REPO}/actions/runs?head_sha=${SHA}&limit=50" '.workflow_runs | arrays') || exit 2
STATE=$(jq -r '.state' <<<"$STATUS")
COUNT=$(jq -r '.total_count' <<<"$STATUS")
RUN_COUNT=$(jq -r '.workflow_runs | length' <<<"$RUNS")
RUNS_PENDING=$(jq -r '[.workflow_runs[] | select(.status != "completed")] | length' <<<"$RUNS")
RUNS_FAILED=$(jq -r '[.workflow_runs[] | select(.status == "completed"
and (.conclusion == "failure" or .conclusion == "cancelled"))] | length' <<<"$RUNS")
if [[ "$STATE" == failure || "$STATE" == error ]] || ((RUNS_FAILED > 0)); then
echo "ERROR: One or more checks failed for ${SHA:0:7}:" >&2
jq -r '.statuses[] | select(.status == "failure" or .status == "error")
| " \(.context): \(.description) (\(.target_url))"' <<<"$RESPONSE" >&2
| " \(.context): \(.description) (\(.target_url))"' <<<"$STATUS" >&2
jq -r '.workflow_runs[] | select(.status == "completed"
and (.conclusion == "failure" or .conclusion == "cancelled"))
| " \(.path) [\(.event)]: \(.conclusion) (\(.html_url))"' <<<"$RUNS" >&2
exit 1
;;
esac
fi
# Done only when no run is queued/running and every posted status is terminal.
# Zero statuses with completed runs means every job was skipped (no status posted).
if ((RUNS_PENDING == 0)) && { [[ "$STATE" == success || "$STATE" == warning ]] ||
((COUNT == 0 && RUN_COUNT > 0)); }; then
echo "SUCCESS: All ${RUN_COUNT} workflow runs and ${COUNT} checks passed for ${SHA:0:7}." >&2
exit 0
fi
# A commit with no workflows reports "pending" with zero checks forever
if ((COUNT == 0 && SECONDS >= GRACE)); then
if ((COUNT == 0 && RUN_COUNT == 0 && SECONDS >= GRACE)); then
echo "NO CI: No checks registered for ${SHA:0:7} after ${GRACE}s." >&2
exit 3
fi
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: ship-it
description: Runs a comprehensive pre-flight audit, syncs the README, and publishes the changes to a new PR.
version: 1.2.0
version: 1.2.1
user-invocable: true
author: Rootiest
---
@@ -15,10 +15,16 @@ SYNOPSIS
tea-ci-watch [OPTIONS] <owner/repo> <commit-sha> [timeout_seconds]
DESCRIPTION
Polls the Gitea combined commit status API (via 'tea api') and blocks until
every check on the commit has finished. Designed for automated CI pipelines
and agent workflows: run it once, in the background, and act on the exit
code instead of sleep-polling.
Polls Gitea (via 'tea api') and blocks until every check on the commit has
finished. Designed for automated CI pipelines and agent workflows: run it
once, in the background, and act on the exit code instead of sleep-polling.
Two sources are combined. Gitea posts an Actions job's commit status only
once a runner picks the job up, so queued workflows are invisible to the
combined status; the Actions runs API lists them from the moment they are
triggered. The commit is done only when every Actions run for it has
completed AND the combined status (which also covers external CI) is no
longer pending. The first failure in either source fails fast.
The combined status reports the latest state of each check, so earlier
'pending' entries for re-run jobs do not affect the result. Skipped jobs
@@ -41,7 +47,8 @@ AUTHENTICATION
RETURNS
0 All checks passed (success, warning, or skipped)
1 One or more checks failed; failing jobs are printed
1 One or more checks failed or were cancelled; failing
checks are printed
2 Usage, dependency, or API error (bad repo, auth, network)
3 No checks registered within the grace period
124 Timeout reached with checks still pending
@@ -81,30 +88,48 @@ INTERVAL=5
echo "Waiting for CI completion on ${REPO}@${SHA:0:7}..." >&2
while ((SECONDS < TIMEOUT)); do
# 'tea api' exits 0 even on HTTP errors, so a missing .state is the error signal
if ! RESPONSE=$(tea api "/repos/${REPO}/commits/${SHA}/status" 2>&1) ||
! STATE=$(jq -er '.state' <<<"$RESPONSE" 2>/dev/null); then
echo "ERROR: Status request failed for ${REPO}@${SHA:0:7}: ${RESPONSE}" >&2
# Fetch a Gitea API path as JSON. 'tea api' exits 0 even on HTTP errors, so the
# caller-supplied jq probe (which must succeed on a valid body) is the error signal.
fetch() {
local path="$1" probe="$2" body
if ! body=$(tea api "$path" 2>&1) || ! jq -e "$probe" <<<"$body" >/dev/null 2>&1; then
echo "ERROR: Request ${path} failed for ${REPO}@${SHA:0:7}: ${body}" >&2
exit 2
fi
COUNT=$(jq -r '.total_count' <<<"$RESPONSE")
printf '%s' "$body"
}
case "$STATE" in
success | warning)
echo "SUCCESS: All ${COUNT} checks passed for ${SHA:0:7}." >&2
exit 0
;;
failure | error)
while ((SECONDS < TIMEOUT)); do
STATUS=$(fetch "/repos/${REPO}/commits/${SHA}/status" '.state | strings') || exit 2
RUNS=$(fetch "/repos/${REPO}/actions/runs?head_sha=${SHA}&limit=50" '.workflow_runs | arrays') || exit 2
STATE=$(jq -r '.state' <<<"$STATUS")
COUNT=$(jq -r '.total_count' <<<"$STATUS")
RUN_COUNT=$(jq -r '.workflow_runs | length' <<<"$RUNS")
RUNS_PENDING=$(jq -r '[.workflow_runs[] | select(.status != "completed")] | length' <<<"$RUNS")
RUNS_FAILED=$(jq -r '[.workflow_runs[] | select(.status == "completed"
and (.conclusion == "failure" or .conclusion == "cancelled"))] | length' <<<"$RUNS")
if [[ "$STATE" == failure || "$STATE" == error ]] || ((RUNS_FAILED > 0)); then
echo "ERROR: One or more checks failed for ${SHA:0:7}:" >&2
jq -r '.statuses[] | select(.status == "failure" or .status == "error")
| " \(.context): \(.description) (\(.target_url))"' <<<"$RESPONSE" >&2
| " \(.context): \(.description) (\(.target_url))"' <<<"$STATUS" >&2
jq -r '.workflow_runs[] | select(.status == "completed"
and (.conclusion == "failure" or .conclusion == "cancelled"))
| " \(.path) [\(.event)]: \(.conclusion) (\(.html_url))"' <<<"$RUNS" >&2
exit 1
;;
esac
fi
# Done only when no run is queued/running and every posted status is terminal.
# Zero statuses with completed runs means every job was skipped (no status posted).
if ((RUNS_PENDING == 0)) && { [[ "$STATE" == success || "$STATE" == warning ]] ||
((COUNT == 0 && RUN_COUNT > 0)); }; then
echo "SUCCESS: All ${RUN_COUNT} workflow runs and ${COUNT} checks passed for ${SHA:0:7}." >&2
exit 0
fi
# A commit with no workflows reports "pending" with zero checks forever
if ((COUNT == 0 && SECONDS >= GRACE)); then
if ((COUNT == 0 && RUN_COUNT == 0 && SECONDS >= GRACE)); then
echo "NO CI: No checks registered for ${SHA:0:7} after ${GRACE}s." >&2
exit 3
fi
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "ship-it",
"description": "Runs a comprehensive pre-flight audit, syncs the README, and publishes the changes to a new PR.",
"version": "1.2.0",
"version": "1.2.1",
"author": {
"name": "Rootiest"
}
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: ship-it
description: Runs a comprehensive pre-flight audit, syncs the README, and publishes the changes to a new PR.
version: 1.2.0
version: 1.2.1
user-invocable: true
author: Rootiest
---
@@ -15,10 +15,16 @@ SYNOPSIS
tea-ci-watch [OPTIONS] <owner/repo> <commit-sha> [timeout_seconds]
DESCRIPTION
Polls the Gitea combined commit status API (via 'tea api') and blocks until
every check on the commit has finished. Designed for automated CI pipelines
and agent workflows: run it once, in the background, and act on the exit
code instead of sleep-polling.
Polls Gitea (via 'tea api') and blocks until every check on the commit has
finished. Designed for automated CI pipelines and agent workflows: run it
once, in the background, and act on the exit code instead of sleep-polling.
Two sources are combined. Gitea posts an Actions job's commit status only
once a runner picks the job up, so queued workflows are invisible to the
combined status; the Actions runs API lists them from the moment they are
triggered. The commit is done only when every Actions run for it has
completed AND the combined status (which also covers external CI) is no
longer pending. The first failure in either source fails fast.
The combined status reports the latest state of each check, so earlier
'pending' entries for re-run jobs do not affect the result. Skipped jobs
@@ -41,7 +47,8 @@ AUTHENTICATION
RETURNS
0 All checks passed (success, warning, or skipped)
1 One or more checks failed; failing jobs are printed
1 One or more checks failed or were cancelled; failing
checks are printed
2 Usage, dependency, or API error (bad repo, auth, network)
3 No checks registered within the grace period
124 Timeout reached with checks still pending
@@ -81,30 +88,48 @@ INTERVAL=5
echo "Waiting for CI completion on ${REPO}@${SHA:0:7}..." >&2
while ((SECONDS < TIMEOUT)); do
# 'tea api' exits 0 even on HTTP errors, so a missing .state is the error signal
if ! RESPONSE=$(tea api "/repos/${REPO}/commits/${SHA}/status" 2>&1) ||
! STATE=$(jq -er '.state' <<<"$RESPONSE" 2>/dev/null); then
echo "ERROR: Status request failed for ${REPO}@${SHA:0:7}: ${RESPONSE}" >&2
# Fetch a Gitea API path as JSON. 'tea api' exits 0 even on HTTP errors, so the
# caller-supplied jq probe (which must succeed on a valid body) is the error signal.
fetch() {
local path="$1" probe="$2" body
if ! body=$(tea api "$path" 2>&1) || ! jq -e "$probe" <<<"$body" >/dev/null 2>&1; then
echo "ERROR: Request ${path} failed for ${REPO}@${SHA:0:7}: ${body}" >&2
exit 2
fi
COUNT=$(jq -r '.total_count' <<<"$RESPONSE")
printf '%s' "$body"
}
case "$STATE" in
success | warning)
echo "SUCCESS: All ${COUNT} checks passed for ${SHA:0:7}." >&2
exit 0
;;
failure | error)
while ((SECONDS < TIMEOUT)); do
STATUS=$(fetch "/repos/${REPO}/commits/${SHA}/status" '.state | strings') || exit 2
RUNS=$(fetch "/repos/${REPO}/actions/runs?head_sha=${SHA}&limit=50" '.workflow_runs | arrays') || exit 2
STATE=$(jq -r '.state' <<<"$STATUS")
COUNT=$(jq -r '.total_count' <<<"$STATUS")
RUN_COUNT=$(jq -r '.workflow_runs | length' <<<"$RUNS")
RUNS_PENDING=$(jq -r '[.workflow_runs[] | select(.status != "completed")] | length' <<<"$RUNS")
RUNS_FAILED=$(jq -r '[.workflow_runs[] | select(.status == "completed"
and (.conclusion == "failure" or .conclusion == "cancelled"))] | length' <<<"$RUNS")
if [[ "$STATE" == failure || "$STATE" == error ]] || ((RUNS_FAILED > 0)); then
echo "ERROR: One or more checks failed for ${SHA:0:7}:" >&2
jq -r '.statuses[] | select(.status == "failure" or .status == "error")
| " \(.context): \(.description) (\(.target_url))"' <<<"$RESPONSE" >&2
| " \(.context): \(.description) (\(.target_url))"' <<<"$STATUS" >&2
jq -r '.workflow_runs[] | select(.status == "completed"
and (.conclusion == "failure" or .conclusion == "cancelled"))
| " \(.path) [\(.event)]: \(.conclusion) (\(.html_url))"' <<<"$RUNS" >&2
exit 1
;;
esac
fi
# Done only when no run is queued/running and every posted status is terminal.
# Zero statuses with completed runs means every job was skipped (no status posted).
if ((RUNS_PENDING == 0)) && { [[ "$STATE" == success || "$STATE" == warning ]] ||
((COUNT == 0 && RUN_COUNT > 0)); }; then
echo "SUCCESS: All ${RUN_COUNT} workflow runs and ${COUNT} checks passed for ${SHA:0:7}." >&2
exit 0
fi
# A commit with no workflows reports "pending" with zero checks forever
if ((COUNT == 0 && SECONDS >= GRACE)); then
if ((COUNT == 0 && RUN_COUNT == 0 && SECONDS >= GRACE)); then
echo "NO CI: No checks registered for ${SHA:0:7} after ${GRACE}s." >&2
exit 3
fi
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ship-it",
"description": "Runs a comprehensive pre-flight audit, syncs the README, and publishes the changes to a new PR.",
"version": "1.2.0",
"version": "1.2.1",
"author": { "name": "Rootiest" }
}
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: ship-it
description: Runs a comprehensive pre-flight audit, syncs the README, and publishes the changes to a new PR.
version: 1.2.0
version: 1.2.1
user-invocable: true
author: Rootiest
---
@@ -15,10 +15,16 @@ SYNOPSIS
tea-ci-watch [OPTIONS] <owner/repo> <commit-sha> [timeout_seconds]
DESCRIPTION
Polls the Gitea combined commit status API (via 'tea api') and blocks until
every check on the commit has finished. Designed for automated CI pipelines
and agent workflows: run it once, in the background, and act on the exit
code instead of sleep-polling.
Polls Gitea (via 'tea api') and blocks until every check on the commit has
finished. Designed for automated CI pipelines and agent workflows: run it
once, in the background, and act on the exit code instead of sleep-polling.
Two sources are combined. Gitea posts an Actions job's commit status only
once a runner picks the job up, so queued workflows are invisible to the
combined status; the Actions runs API lists them from the moment they are
triggered. The commit is done only when every Actions run for it has
completed AND the combined status (which also covers external CI) is no
longer pending. The first failure in either source fails fast.
The combined status reports the latest state of each check, so earlier
'pending' entries for re-run jobs do not affect the result. Skipped jobs
@@ -41,7 +47,8 @@ AUTHENTICATION
RETURNS
0 All checks passed (success, warning, or skipped)
1 One or more checks failed; failing jobs are printed
1 One or more checks failed or were cancelled; failing
checks are printed
2 Usage, dependency, or API error (bad repo, auth, network)
3 No checks registered within the grace period
124 Timeout reached with checks still pending
@@ -81,30 +88,48 @@ INTERVAL=5
echo "Waiting for CI completion on ${REPO}@${SHA:0:7}..." >&2
while ((SECONDS < TIMEOUT)); do
# 'tea api' exits 0 even on HTTP errors, so a missing .state is the error signal
if ! RESPONSE=$(tea api "/repos/${REPO}/commits/${SHA}/status" 2>&1) ||
! STATE=$(jq -er '.state' <<<"$RESPONSE" 2>/dev/null); then
echo "ERROR: Status request failed for ${REPO}@${SHA:0:7}: ${RESPONSE}" >&2
# Fetch a Gitea API path as JSON. 'tea api' exits 0 even on HTTP errors, so the
# caller-supplied jq probe (which must succeed on a valid body) is the error signal.
fetch() {
local path="$1" probe="$2" body
if ! body=$(tea api "$path" 2>&1) || ! jq -e "$probe" <<<"$body" >/dev/null 2>&1; then
echo "ERROR: Request ${path} failed for ${REPO}@${SHA:0:7}: ${body}" >&2
exit 2
fi
COUNT=$(jq -r '.total_count' <<<"$RESPONSE")
printf '%s' "$body"
}
case "$STATE" in
success | warning)
echo "SUCCESS: All ${COUNT} checks passed for ${SHA:0:7}." >&2
exit 0
;;
failure | error)
while ((SECONDS < TIMEOUT)); do
STATUS=$(fetch "/repos/${REPO}/commits/${SHA}/status" '.state | strings') || exit 2
RUNS=$(fetch "/repos/${REPO}/actions/runs?head_sha=${SHA}&limit=50" '.workflow_runs | arrays') || exit 2
STATE=$(jq -r '.state' <<<"$STATUS")
COUNT=$(jq -r '.total_count' <<<"$STATUS")
RUN_COUNT=$(jq -r '.workflow_runs | length' <<<"$RUNS")
RUNS_PENDING=$(jq -r '[.workflow_runs[] | select(.status != "completed")] | length' <<<"$RUNS")
RUNS_FAILED=$(jq -r '[.workflow_runs[] | select(.status == "completed"
and (.conclusion == "failure" or .conclusion == "cancelled"))] | length' <<<"$RUNS")
if [[ "$STATE" == failure || "$STATE" == error ]] || ((RUNS_FAILED > 0)); then
echo "ERROR: One or more checks failed for ${SHA:0:7}:" >&2
jq -r '.statuses[] | select(.status == "failure" or .status == "error")
| " \(.context): \(.description) (\(.target_url))"' <<<"$RESPONSE" >&2
| " \(.context): \(.description) (\(.target_url))"' <<<"$STATUS" >&2
jq -r '.workflow_runs[] | select(.status == "completed"
and (.conclusion == "failure" or .conclusion == "cancelled"))
| " \(.path) [\(.event)]: \(.conclusion) (\(.html_url))"' <<<"$RUNS" >&2
exit 1
;;
esac
fi
# Done only when no run is queued/running and every posted status is terminal.
# Zero statuses with completed runs means every job was skipped (no status posted).
if ((RUNS_PENDING == 0)) && { [[ "$STATE" == success || "$STATE" == warning ]] ||
((COUNT == 0 && RUN_COUNT > 0)); }; then
echo "SUCCESS: All ${RUN_COUNT} workflow runs and ${COUNT} checks passed for ${SHA:0:7}." >&2
exit 0
fi
# A commit with no workflows reports "pending" with zero checks forever
if ((COUNT == 0 && SECONDS >= GRACE)); then
if ((COUNT == 0 && RUN_COUNT == 0 && SECONDS >= GRACE)); then
echo "NO CI: No checks registered for ${SHA:0:7} after ${GRACE}s." >&2
exit 3
fi