fix(ci-queue-wait): treat absent branch (404) as queue-clear (#872)
Closes #872 Mos (id-11) Gate-16 merge: independent review APPROVE @23cbdaf8, author jason.woltje(id2) != approver Mos(id11), CI green wp1974. Co-authored-by: jason.woltje <jason@diversecanvas.com> Co-committed-by: jason.woltje <jason@diversecanvas.com>
This commit was merged in pull request #874.
This commit is contained in:
@@ -185,6 +185,16 @@ switch ($platform) {
|
||||
$headSha = ($branchPayload.commit.id | Out-String).Trim()
|
||||
}
|
||||
catch {
|
||||
# A not-yet-pushed feature branch has no in-flight pipeline, so the
|
||||
# pre-push queue guard must treat 404 as "queue clear", not crash.
|
||||
$statusCode = $null
|
||||
if ($_.Exception.Response) {
|
||||
$statusCode = [int]$_.Exception.Response.StatusCode
|
||||
}
|
||||
if ($statusCode -eq 404) {
|
||||
Write-Host "[ci-queue-wait] branch $Branch not yet on remote — no in-flight pipeline; queue clear."
|
||||
exit 0
|
||||
}
|
||||
Write-Error "Could not resolve $Branch head SHA from Gitea API."
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -137,7 +137,21 @@ gitea_get_branch_head_sha() {
|
||||
local branch="$3"
|
||||
local token="$4"
|
||||
local url="https://${host}/api/v1/repos/${repo}/branches/${branch}"
|
||||
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" | python3 -c '
|
||||
# Capture HTTP status so an absent branch (404) is distinguished from an API
|
||||
# error. A not-yet-pushed feature branch has no in-flight pipeline, so the
|
||||
# pre-push queue guard must treat 404 as "queue clear", not crash.
|
||||
local resp code body
|
||||
resp=$(curl -sS -H "User-Agent: curl/8" -H "Authorization: token ${token}" -w $'\n%{http_code}' "$url")
|
||||
code="${resp##*$'\n'}"
|
||||
body="${resp%$'\n'*}"
|
||||
if [[ "$code" == "404" ]]; then
|
||||
echo "__BRANCH_ABSENT__"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$code" != "200" ]]; then
|
||||
return 1
|
||||
fi
|
||||
printf '%s' "$body" | python3 -c '
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
commit = data.get("commit") or {}
|
||||
@@ -219,6 +233,10 @@ elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
exit 1
|
||||
}
|
||||
HEAD_SHA=$(gitea_get_branch_head_sha "$HOST" "$OWNER/$REPO" "$BRANCH" "$TOKEN")
|
||||
if [[ "$HEAD_SHA" == "__BRANCH_ABSENT__" ]]; then
|
||||
echo "[ci-queue-wait] branch ${BRANCH} not yet on remote — no in-flight pipeline; queue clear."
|
||||
exit 0
|
||||
fi
|
||||
if [[ -z "$HEAD_SHA" ]]; then
|
||||
echo "Error: Could not resolve ${BRANCH} head SHA." >&2
|
||||
exit 1
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression harness for ci-queue-wait.sh's 404-branch-absent handling.
|
||||
#
|
||||
# gitea_get_branch_head_sha() resolves a branch's head SHA before the
|
||||
# pre-push queue guard runs. A branch that has never been pushed doesn't
|
||||
# exist on the remote yet, so Gitea's branches/<branch> endpoint 404s.
|
||||
# Before the fix, `curl -fsSL` failed on the 404, its empty stdout was piped
|
||||
# into `python3 -c 'json.load(sys.stdin)'`, and the resulting
|
||||
# JSONDecodeError crashed the guard -- blocking every new feature branch's
|
||||
# first push. The fix must treat 404 as "no in-flight pipeline" (queue
|
||||
# clear) while still failing closed on a genuine API error.
|
||||
#
|
||||
# Covers:
|
||||
# (a) 404 branch-absent -> exit 0, "queue clear" message.
|
||||
# (b) 200 existing branch + a terminal CI state -> unchanged behavior.
|
||||
# (c) genuine API error (500) -> still fail-closed (nonzero exit).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/ci-queue-wait-branch-absent}"
|
||||
REPO_DIR="$WORK_DIR/repo"
|
||||
STUB_DIR="$WORK_DIR/stubs"
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$REPO_DIR" "$STUB_DIR"
|
||||
|
||||
git -C "$REPO_DIR" init -q
|
||||
git -C "$REPO_DIR" remote add origin https://git.example.test/acme/widgets.git
|
||||
|
||||
# Minimal curl stub. Selects a canned response by inspecting which Gitea
|
||||
# endpoint is being hit (branches/<branch> vs commits/<sha>/status) and
|
||||
# whether -w '%{http_code}' was requested. Only the patched branch-lookup
|
||||
# call passes -w; the unpatched call and the (unchanged) status call both
|
||||
# use plain `curl -fsSL` semantics -- exit nonzero and print nothing on a
|
||||
# non-2xx response. This lets the same stub exercise both the pre-fix and
|
||||
# post-fix branch-lookup code paths faithfully.
|
||||
cat > "$STUB_DIR/curl" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
has_w=0
|
||||
url=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-w) has_w=1 ;;
|
||||
http://*|https://*) url="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$url" in
|
||||
*/branches/*) mode="${MOSAIC_STUB_BRANCH_MODE:?MOSAIC_STUB_BRANCH_MODE not set}" ;;
|
||||
*/status) mode="${MOSAIC_STUB_STATUS_MODE:-terminal-success}" ;;
|
||||
*)
|
||||
echo "curl stub: unrecognized URL: $url" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$mode" in
|
||||
404) code=404; body="" ;;
|
||||
200) code=200; body='{"commit":{"id":"deadbeefcafef00d0123456789abcdef01234567"}}' ;;
|
||||
500) code=500; body='{"message":"internal server error"}' ;;
|
||||
no-status) code=200; body='{}' ;;
|
||||
terminal-success) code=200; body='{"state":"success"}' ;;
|
||||
*)
|
||||
echo "curl stub: unknown mode=$mode" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$has_w" == 1 ]]; then
|
||||
printf '%s\n%s' "$body" "$code"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Unpatched branch-lookup call / status-endpoint call: real curl -fsSL
|
||||
# exits nonzero and emits nothing on stdout for a non-2xx response.
|
||||
if [[ "$code" != "200" ]]; then
|
||||
exit 22
|
||||
fi
|
||||
printf '%s' "$body"
|
||||
SH
|
||||
chmod +x "$STUB_DIR/curl"
|
||||
|
||||
run_ci_queue_wait() {
|
||||
local branch="$1"
|
||||
(
|
||||
cd "$REPO_DIR"
|
||||
export PATH="$STUB_DIR:$PATH"
|
||||
export MOSAIC_CREDENTIALS_FILE="$WORK_DIR/no-credentials.json"
|
||||
export GITEA_TOKEN="stub-token"
|
||||
export GITEA_URL="https://git.example.test"
|
||||
"$SCRIPT_DIR/ci-queue-wait.sh" -B "$branch" --purpose push -t 5 -i 1
|
||||
)
|
||||
}
|
||||
|
||||
fail=0
|
||||
|
||||
# (a) 404 branch-absent -> queue clear, exit 0.
|
||||
set +e
|
||||
out_a=$(MOSAIC_STUB_BRANCH_MODE=404 run_ci_queue_wait "feat/not-pushed-yet" 2>&1)
|
||||
status_a=$?
|
||||
set -e
|
||||
if [[ "$status_a" -ne 0 ]]; then
|
||||
echo "FAIL(a): expected exit 0 for 404 branch-absent, got $status_a" >&2
|
||||
echo "$out_a" >&2
|
||||
fail=1
|
||||
elif [[ "$out_a" != *"queue clear"* ]]; then
|
||||
echo "FAIL(a): expected a queue-clear message, got:" >&2
|
||||
echo "$out_a" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
# (b) 200 existing branch + terminal CI state -> unchanged behavior, exit 0.
|
||||
set +e
|
||||
out_b=$(MOSAIC_STUB_BRANCH_MODE=200 MOSAIC_STUB_STATUS_MODE=terminal-success run_ci_queue_wait "main" 2>&1)
|
||||
status_b=$?
|
||||
set -e
|
||||
if [[ "$status_b" -ne 0 ]]; then
|
||||
echo "FAIL(b): expected exit 0 for existing branch with terminal status, got $status_b" >&2
|
||||
echo "$out_b" >&2
|
||||
fail=1
|
||||
elif [[ "$out_b" != *"sha=deadbeefcafef00d0123456789abcdef01234567"* ]]; then
|
||||
echo "FAIL(b): expected the resolved HEAD SHA to be logged, got:" >&2
|
||||
echo "$out_b" >&2
|
||||
fail=1
|
||||
elif [[ "$out_b" == *"queue clear"* ]]; then
|
||||
echo "FAIL(b): an existing branch must not take the branch-absent path" >&2
|
||||
echo "$out_b" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
# (c) genuine API error (500) -> still fail-closed, exit nonzero.
|
||||
set +e
|
||||
out_c=$(MOSAIC_STUB_BRANCH_MODE=500 run_ci_queue_wait "feat/some-branch" 2>&1)
|
||||
status_c=$?
|
||||
set -e
|
||||
if [[ "$status_c" -eq 0 ]]; then
|
||||
echo "FAIL(c): expected a nonzero exit for a genuine 500 API error, got 0" >&2
|
||||
echo "$out_c" >&2
|
||||
fail=1
|
||||
elif [[ "$out_c" == *"queue clear"* ]]; then
|
||||
echo "FAIL(c): a genuine API error must not be reported as queue-clear" >&2
|
||||
echo "$out_c" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
if [[ "$fail" -eq 0 ]]; then
|
||||
echo "ci-queue-wait branch-absent regression passed (3/3 cases)"
|
||||
fi
|
||||
|
||||
exit "$fail"
|
||||
@@ -4,18 +4,18 @@ Helper scripts for r0 coordinator / orchestrator sessions — mission lifecycle,
|
||||
session health, continuation, and board maintenance. See
|
||||
`framework/guides/ORCHESTRATOR-PROTOCOL.md` for the surrounding process.
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `mission-init.sh` | Initialize a new orchestration mission (manifest, scratchpad, TASKS.md). |
|
||||
| `mission-status.sh` | Show the mission progress dashboard. |
|
||||
| `session-run.sh` | Generate continuation context and launch the target runtime. |
|
||||
| `session-resume.sh` | Crash recovery for dead orchestrator sessions. |
|
||||
| `session-status.sh` | Check agent session health. |
|
||||
| `continue-prompt.sh` | Generate the continuation prompt for the next session. |
|
||||
| `board-roll.sh` | Keep a LIVE orchestration board under its byte cap by rolling the oldest entries to its LEDGER. |
|
||||
| `smoke-test.sh` | Behavior smoke checks for the coord continue/run workflows. |
|
||||
| `test-board-roll.sh` | Regression harness for `board-roll.sh`. |
|
||||
| `_lib.sh` | Shared functions sourced by the above (state files, TASKS.md parsing, locks). |
|
||||
| Script | Purpose |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `mission-init.sh` | Initialize a new orchestration mission (manifest, scratchpad, TASKS.md). |
|
||||
| `mission-status.sh` | Show the mission progress dashboard. |
|
||||
| `session-run.sh` | Generate continuation context and launch the target runtime. |
|
||||
| `session-resume.sh` | Crash recovery for dead orchestrator sessions. |
|
||||
| `session-status.sh` | Check agent session health. |
|
||||
| `continue-prompt.sh` | Generate the continuation prompt for the next session. |
|
||||
| `board-roll.sh` | Keep a LIVE orchestration board under its byte cap by rolling the oldest entries to its LEDGER. |
|
||||
| `smoke-test.sh` | Behavior smoke checks for the coord continue/run workflows. |
|
||||
| `test-board-roll.sh` | Regression harness for `board-roll.sh`. |
|
||||
| `_lib.sh` | Shared functions sourced by the above (state files, TASKS.md parsing, locks). |
|
||||
|
||||
## board-roll.sh
|
||||
|
||||
@@ -35,16 +35,23 @@ always-current `##` sections) is pinned and never touched:
|
||||
|
||||
```markdown
|
||||
# MOS ORCHESTRATION BOARD — LIVE state
|
||||
|
||||
> protocol blockquote … (pinned)
|
||||
|
||||
## 🟦 Curated always-current section (pinned)
|
||||
|
||||
…
|
||||
|
||||
<!-- BOARD-ROLL:START -->
|
||||
|
||||
### 2026-07-22 (mid²²) — newest tick, stays longest
|
||||
|
||||
…
|
||||
|
||||
### 2026-07-20 (dawn) — oldest tick, rolled first
|
||||
|
||||
…
|
||||
|
||||
<!-- BOARD-ROLL:END -->
|
||||
```
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
|
||||
"test:framework-shell": "python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh"
|
||||
"test:framework-shell": "python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaicstack/brain": "workspace:*",
|
||||
|
||||
Reference in New Issue
Block a user