Compare commits
2 Commits
fix/wrappe
...
fix/toolin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cd6c83b9b | ||
|
|
b0b2c20da0 |
@@ -51,3 +51,48 @@ This repository currently has no root `CHANGELOG.md`; the scratchpad and `docs/T
|
|||||||
- PR #1908: `Dry run: would merge PR #1908 on git.uscllc.com with authenticated Gitea API fallback (base=main, method=squash).`
|
- PR #1908: `Dry run: would merge PR #1908 on git.uscllc.com with authenticated Gitea API fallback (base=main, method=squash).`
|
||||||
- PR: `https://git.mosaicstack.dev/mosaicstack/stack/pulls/518`, branch `fix/t-a292e96f-gitea-pr-metadata`.
|
- PR: `https://git.mosaicstack.dev/mosaicstack/stack/pulls/518`, branch `fix/t-a292e96f-gitea-pr-metadata`.
|
||||||
- CI: Recent PR/push pipelines failed before clone/test execution due Woodpecker/Kubernetes PVC API timeout: `dial tcp 10.43.0.1:443: i/o timeout`. No repository test step executed in CI; local targeted verification above remains clean.
|
- CI: Recent PR/push pipelines failed before clone/test execution due Woodpecker/Kubernetes PVC API timeout: `dial tcp 10.43.0.1:443: i/o timeout`. No repository test step executed in CI; local targeted verification above remains clean.
|
||||||
|
|
||||||
|
## 2026-06-18 — PR #549 functional blocker remediation
|
||||||
|
|
||||||
|
### Assignment
|
||||||
|
|
||||||
|
Coordinator `mos-claude` assigned remediation for PR #549: fix `packages/mosaic/framework/tools/git/pr-metadata.sh` tmpfile cleanup where an `EXIT` trap references function-local `body_file` after the function returns inside `RAW=$(...)`, producing `body_file: unbound variable` on the authenticated success path and failing to clean up safely on early `set -e` exits.
|
||||||
|
|
||||||
|
### Plan
|
||||||
|
|
||||||
|
1. Add a non-vacuous Gitea test that exercises `curl_gitea_pull` with stubbed `curl` and `GITEA_TOKEN` instead of `MOSAIC_GITEA_PR_METADATA_RAW_FILE`.
|
||||||
|
2. Prove the new test is RED against the current PR head.
|
||||||
|
3. Replace the function-local `EXIT` cleanup with robust function-scoped tmpfile cleanup.
|
||||||
|
4. Re-run targeted tests, `bash -n`, and review gates; commit and push branch only. Do not merge.
|
||||||
|
|
||||||
|
### Constraints / assumptions
|
||||||
|
|
||||||
|
- Do not modify prior injection/JSON fixes in `issue-edit`, `issue-assign`, or `milestone-create`.
|
||||||
|
- Worker role: do not modify `docs/TASKS.md`; orchestrator remains the single writer.
|
||||||
|
- Budget: no explicit token cap provided; keep scope to shell wrapper + targeted regression harness.
|
||||||
|
|
||||||
|
### Remediation results
|
||||||
|
|
||||||
|
- Rebased `fix/tooling-eval-injection-jq-json` onto `origin/main`; branch was already current.
|
||||||
|
- Added a curl-stub regression path that does not use `MOSAIC_GITEA_PR_METADATA_RAW_FILE`, so it exercises `curl_gitea_pull` and its temp body file.
|
||||||
|
- RED evidence: copied the new harness next to the pre-fix `HEAD` version of `pr-metadata.sh`; `MOSAIC_TEST_WORK_DIR=$PWD/.mosaic-test-work/pr-metadata-red-work .../test-pr-metadata-gitea.sh` failed with `body_file: unbound variable` on the curl success path.
|
||||||
|
- Fix: replaced `EXIT` temp-file cleanup with a `RETURN`-scoped cleanup function that removes the body file while the function-local variable is still in scope, preserves the original return status, and clears the `RETURN` trap.
|
||||||
|
- GREEN evidence:
|
||||||
|
- `MOSAIC_TEST_WORK_DIR=$PWD/.mosaic-test-work/pr-metadata-gitea-current packages/mosaic/framework/tools/git/test-pr-metadata-gitea.sh` passed.
|
||||||
|
- `bash -n packages/mosaic/framework/tools/git/pr-metadata.sh packages/mosaic/framework/tools/git/test-pr-metadata-gitea.sh` passed.
|
||||||
|
- `shellcheck -x -P . -e SC1090 packages/mosaic/framework/tools/git/pr-metadata.sh packages/mosaic/framework/tools/git/test-pr-metadata-gitea.sh` passed.
|
||||||
|
|
||||||
|
### Review remediation
|
||||||
|
|
||||||
|
- Codex review returned one should-fix: the early-exit test used `chmod 000`, which is not root-safe in container CI.
|
||||||
|
- Remediation: changed the stubbed 2xx/cat-failure mode to replace the curl output with a broken symlink, which fails deterministically even as root and still validates cleanup via `rm -f -- "$body_file"`.
|
||||||
|
|
||||||
|
### Second review remediation
|
||||||
|
|
||||||
|
- Codex review found the 2xx `cat "$body_file"` read could be masked under command substitution semantics because the branch returned 0 unconditionally.
|
||||||
|
- Remediation: both authenticated 2xx branches now use `cat "$body_file" || return $?` before returning success.
|
||||||
|
- Strengthened the broken-symlink test to require the body-read failure and reject the later `Gitea API returned non-JSON` parse-failure path, so the test verifies the helper-level failure propagation rather than eventual downstream failure.
|
||||||
|
|
||||||
|
### Final review gate
|
||||||
|
|
||||||
|
- Codex review after remediation: approved (`0 blockers, 0 should-fix, 0 suggestions`).
|
||||||
|
|||||||
@@ -16,12 +16,7 @@
|
|||||||
# After loading, service-specific env vars are exported.
|
# After loading, service-specific env vars are exported.
|
||||||
# Run `load_credentials --help` for details.
|
# Run `load_credentials --help` for details.
|
||||||
|
|
||||||
if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
|
MOSAIC_CREDENTIALS_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}"
|
||||||
for _cand in "$HOME/.config/mosaic/credentials.json" "$HOME/src/jarvis-brain/credentials.json"; do
|
|
||||||
if [[ -f "$_cand" ]]; then MOSAIC_CREDENTIALS_FILE="$_cand"; break; fi
|
|
||||||
done
|
|
||||||
: "${MOSAIC_CREDENTIALS_FILE:=$HOME/src/jarvis-brain/credentials.json}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
_mosaic_require_jq() {
|
_mosaic_require_jq() {
|
||||||
if ! command -v jq &>/dev/null; then
|
if ! command -v jq &>/dev/null; then
|
||||||
@@ -39,19 +34,6 @@ _mosaic_read_cred() {
|
|||||||
jq -r "$jq_path // empty" "$MOSAIC_CREDENTIALS_FILE"
|
jq -r "$jq_path // empty" "$MOSAIC_CREDENTIALS_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Decide curl TLS flag for a target URL: validate public hosts (MITM matters on
|
|
||||||
# WAN); allow self-signed only for private-network IP literals (trusted LAN) or an
|
|
||||||
# explicit $MOSAIC_INSECURE_TLS opt-in. Echoes "-k" or "" (empty).
|
|
||||||
_mosaic_tls_opt() {
|
|
||||||
local url="$1" host
|
|
||||||
[[ -n "${MOSAIC_INSECURE_TLS:-}" ]] && { echo "-k"; return; }
|
|
||||||
host=$(printf '%s' "$url" | sed -E 's#^[a-zA-Z]+://([^/:]+).*#\1#')
|
|
||||||
if [[ "$host" =~ ^(10\.|127\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.) ]]; then
|
|
||||||
echo "-k"; return
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Sync Woodpecker credentials to ~/.woodpecker/<instance>.env
|
# Sync Woodpecker credentials to ~/.woodpecker/<instance>.env
|
||||||
# Only writes when values differ to avoid unnecessary disk writes.
|
# Only writes when values differ to avoid unnecessary disk writes.
|
||||||
_mosaic_sync_woodpecker_env() {
|
_mosaic_sync_woodpecker_env() {
|
||||||
@@ -279,8 +261,7 @@ mosaic_http() {
|
|||||||
local base_url="${4:-}"
|
local base_url="${4:-}"
|
||||||
|
|
||||||
local response
|
local response
|
||||||
local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}")
|
response=$(curl -sk -w "\n%{http_code}" -X "$method" \
|
||||||
response=$(curl -sS $_tls -w "\n%{http_code}" -X "$method" \
|
|
||||||
-H "$auth_header" \
|
-H "$auth_header" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${base_url}${endpoint}")
|
"${base_url}${endpoint}")
|
||||||
@@ -298,8 +279,7 @@ mosaic_http_post() {
|
|||||||
local base_url="${4:-}"
|
local base_url="${4:-}"
|
||||||
|
|
||||||
local response
|
local response
|
||||||
local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}")
|
response=$(curl -sk -w "\n%{http_code}" -X POST \
|
||||||
response=$(curl -sS $_tls -w "\n%{http_code}" -X POST \
|
|
||||||
-H "$auth_header" \
|
-H "$auth_header" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$data" \
|
-d "$data" \
|
||||||
@@ -317,8 +297,7 @@ mosaic_http_patch() {
|
|||||||
local base_url="${4:-}"
|
local base_url="${4:-}"
|
||||||
|
|
||||||
local response
|
local response
|
||||||
local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}")
|
response=$(curl -sk -w "\n%{http_code}" -X PATCH \
|
||||||
response=$(curl -sS $_tls -w "\n%{http_code}" -X PATCH \
|
|
||||||
-H "$auth_header" \
|
-H "$auth_header" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$data" \
|
-d "$data" \
|
||||||
|
|||||||
@@ -98,27 +98,32 @@ case "$PLATFORM" in
|
|||||||
;;
|
;;
|
||||||
gitea)
|
gitea)
|
||||||
# tea issue edit syntax
|
# tea issue edit syntax
|
||||||
REPO_ARGS=$(get_gitea_repo_args) || {
|
REPO_SLUG=$(get_repo_slug) || {
|
||||||
echo "Error: Could not resolve Gitea repo/login args for remote host" >&2
|
echo "Error: Could not resolve Gitea repo slug from remote" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
CMD="tea issue edit $ISSUE $REPO_ARGS"
|
REPO_LOGIN=$(get_gitea_login) || {
|
||||||
|
echo "Error: Could not resolve Gitea login for remote host" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
REPO_ARGS=(--repo "$REPO_SLUG" --login "$REPO_LOGIN")
|
||||||
|
CMD=(tea issue edit "$ISSUE" "${REPO_ARGS[@]}")
|
||||||
NEEDS_EDIT=false
|
NEEDS_EDIT=false
|
||||||
|
|
||||||
if [[ -n "$ASSIGNEE" ]]; then
|
if [[ -n "$ASSIGNEE" ]]; then
|
||||||
# tea uses --assignees flag
|
# tea uses --assignees flag
|
||||||
CMD="$CMD --assignees \"$ASSIGNEE\""
|
CMD+=(--assignees "$ASSIGNEE")
|
||||||
NEEDS_EDIT=true
|
NEEDS_EDIT=true
|
||||||
fi
|
fi
|
||||||
if [[ -n "$LABELS" ]]; then
|
if [[ -n "$LABELS" ]]; then
|
||||||
# tea uses --labels flag (replaces existing)
|
# tea uses --labels flag (replaces existing)
|
||||||
CMD="$CMD --labels \"$LABELS\""
|
CMD+=(--labels "$LABELS")
|
||||||
NEEDS_EDIT=true
|
NEEDS_EDIT=true
|
||||||
fi
|
fi
|
||||||
if [[ -n "$MILESTONE" ]]; then
|
if [[ -n "$MILESTONE" ]]; then
|
||||||
MILESTONE_ID=$(tea milestones list $REPO_ARGS 2>/dev/null | grep -E "^\s*[0-9]+" | grep "$MILESTONE" | awk '{print $1}' | head -1)
|
MILESTONE_ID=$(tea milestones list "${REPO_ARGS[@]}" 2>/dev/null | grep -E "^\s*[0-9]+" | grep "$MILESTONE" | awk '{print $1}' | head -1)
|
||||||
if [[ -n "$MILESTONE_ID" ]]; then
|
if [[ -n "$MILESTONE_ID" ]]; then
|
||||||
CMD="$CMD --milestone $MILESTONE_ID"
|
CMD+=(--milestone "$MILESTONE_ID")
|
||||||
NEEDS_EDIT=true
|
NEEDS_EDIT=true
|
||||||
else
|
else
|
||||||
echo "Warning: Could not find milestone '$MILESTONE'" >&2
|
echo "Warning: Could not find milestone '$MILESTONE'" >&2
|
||||||
@@ -126,7 +131,7 @@ case "$PLATFORM" in
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$NEEDS_EDIT" == true ]]; then
|
if [[ "$NEEDS_EDIT" == true ]]; then
|
||||||
eval "$CMD"
|
"${CMD[@]}"
|
||||||
echo "Issue #$ISSUE updated successfully"
|
echo "Issue #$ISSUE updated successfully"
|
||||||
else
|
else
|
||||||
echo "No changes specified"
|
echo "No changes specified"
|
||||||
|
|||||||
@@ -63,24 +63,28 @@ fi
|
|||||||
detect_platform >/dev/null
|
detect_platform >/dev/null
|
||||||
|
|
||||||
if [[ "$PLATFORM" == "github" ]]; then
|
if [[ "$PLATFORM" == "github" ]]; then
|
||||||
CMD="gh issue edit $ISSUE_NUMBER"
|
CMD=(gh issue edit "$ISSUE_NUMBER")
|
||||||
[[ -n "$TITLE" ]] && CMD="$CMD --title \"$TITLE\""
|
[[ -n "$TITLE" ]] && CMD+=(--title "$TITLE")
|
||||||
[[ -n "$BODY" ]] && CMD="$CMD --body \"$BODY\""
|
[[ -n "$BODY" ]] && CMD+=(--body "$BODY")
|
||||||
[[ -n "$LABELS" ]] && CMD="$CMD --add-label \"$LABELS\""
|
[[ -n "$LABELS" ]] && CMD+=(--add-label "$LABELS")
|
||||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
[[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE")
|
||||||
eval $CMD
|
"${CMD[@]}"
|
||||||
echo "Updated GitHub issue #$ISSUE_NUMBER"
|
echo "Updated GitHub issue #$ISSUE_NUMBER"
|
||||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||||
REPO_ARGS=$(get_gitea_repo_args) || {
|
REPO_SLUG=$(get_repo_slug) || {
|
||||||
echo "Error: Could not resolve Gitea repo/login args for remote host" >&2
|
echo "Error: Could not resolve Gitea repo slug from remote" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
CMD="tea issue edit $ISSUE_NUMBER $REPO_ARGS"
|
REPO_LOGIN=$(get_gitea_login) || {
|
||||||
[[ -n "$TITLE" ]] && CMD="$CMD --title \"$TITLE\""
|
echo "Error: Could not resolve Gitea login for remote host" >&2
|
||||||
[[ -n "$BODY" ]] && CMD="$CMD --description \"$BODY\""
|
exit 1
|
||||||
[[ -n "$LABELS" ]] && CMD="$CMD --add-labels \"$LABELS\""
|
}
|
||||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
CMD=(tea issue edit "$ISSUE_NUMBER" --repo "$REPO_SLUG" --login "$REPO_LOGIN")
|
||||||
eval $CMD
|
[[ -n "$TITLE" ]] && CMD+=(--title "$TITLE")
|
||||||
|
[[ -n "$BODY" ]] && CMD+=(--description "$BODY")
|
||||||
|
[[ -n "$LABELS" ]] && CMD+=(--add-labels "$LABELS")
|
||||||
|
[[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE")
|
||||||
|
"${CMD[@]}"
|
||||||
echo "Updated Gitea issue #$ISSUE_NUMBER"
|
echo "Updated Gitea issue #$ISSUE_NUMBER"
|
||||||
else
|
else
|
||||||
echo "Error: Unknown platform"
|
echo "Error: Unknown platform"
|
||||||
|
|||||||
@@ -99,10 +99,15 @@ fi
|
|||||||
case "$PLATFORM" in
|
case "$PLATFORM" in
|
||||||
github)
|
github)
|
||||||
# GitHub uses the API for milestone creation
|
# GitHub uses the API for milestone creation
|
||||||
JSON_PAYLOAD="{\"title\":\"$TITLE\""
|
# Use jq to safely construct JSON so titles/descriptions containing
|
||||||
[[ -n "$DESCRIPTION" ]] && JSON_PAYLOAD="$JSON_PAYLOAD,\"description\":\"$DESCRIPTION\""
|
# quotes or special characters do not corrupt the payload (F-07).
|
||||||
[[ -n "$DUE_DATE" ]] && JSON_PAYLOAD="$JSON_PAYLOAD,\"due_on\":\"${DUE_DATE}T00:00:00Z\""
|
JSON_PAYLOAD=$(jq -n \
|
||||||
JSON_PAYLOAD="$JSON_PAYLOAD}"
|
--arg t "$TITLE" \
|
||||||
|
--arg d "$DESCRIPTION" \
|
||||||
|
--arg due "${DUE_DATE}" \
|
||||||
|
'{"title": $t}
|
||||||
|
+ (if $d != "" then {"description": $d} else {} end)
|
||||||
|
+ (if $due != "" then {"due_on": ($due + "T00:00:00Z")} else {} end)')
|
||||||
|
|
||||||
gh api repos/:owner/:repo/milestones --method POST --input - <<< "$JSON_PAYLOAD"
|
gh api repos/:owner/:repo/milestones --method POST --input - <<< "$JSON_PAYLOAD"
|
||||||
echo "Milestone '$TITLE' created successfully"
|
echo "Milestone '$TITLE' created successfully"
|
||||||
|
|||||||
@@ -72,11 +72,6 @@ elif values and all(v == "success" for v in values):
|
|||||||
print("success")
|
print("success")
|
||||||
elif any(v in {"pending", "running", "queued", "waiting"} for v in values):
|
elif any(v in {"pending", "running", "queued", "waiting"} for v in values):
|
||||||
print("pending")
|
print("pending")
|
||||||
elif not values and not state:
|
|
||||||
# No pipeline/status of any kind reported for this commit. Distinct from
|
|
||||||
# "unknown" (an ambiguous/unrecognized status that should keep polling):
|
|
||||||
# this signals a repo/commit that simply has no CI configured.
|
|
||||||
print("no-status")
|
|
||||||
else:
|
else:
|
||||||
print("unknown")
|
print("unknown")
|
||||||
PY
|
PY
|
||||||
@@ -147,21 +142,6 @@ gitea_get_commit_status_json() {
|
|||||||
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url"
|
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url"
|
||||||
}
|
}
|
||||||
|
|
||||||
gitea_get_default_branch() {
|
|
||||||
local host="$1"
|
|
||||||
local repo="$2"
|
|
||||||
local token="$3"
|
|
||||||
local url="https://${host}/api/v1/repos/${repo}"
|
|
||||||
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" | python3 -c '
|
|
||||||
import json, sys
|
|
||||||
print((json.load(sys.stdin) or {}).get("default_branch", ""))
|
|
||||||
'
|
|
||||||
}
|
|
||||||
|
|
||||||
github_get_default_branch() {
|
|
||||||
gh api "repos/${OWNER}/${REPO}" --jq '.default_branch'
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-n|--number)
|
-n|--number)
|
||||||
@@ -265,51 +245,6 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# No-CI determination is TWO-TIER (primary: CI history; secondary: empty-poll streak).
|
|
||||||
#
|
|
||||||
# PRIMARY — "does this repo run CI at all?" Probed once, up front, from the DEFAULT
|
|
||||||
# BRANCH's commit status. A repo whose default branch carries CI statuses
|
|
||||||
# demonstrably runs CI, so an EMPTY status on the PR head means the pipeline simply
|
|
||||||
# has not registered YET (webhook/queue lag) — NOT that the repo is CI-less. In that
|
|
||||||
# case we must NEVER fast-green; we keep polling until the pipeline registers or the
|
|
||||||
# timeout fires (both safe). This closes the webhook-lag false-green: a slow-to-
|
|
||||||
# register pipeline feeding a merge gate can no longer be mistaken for "no CI".
|
|
||||||
#
|
|
||||||
# SECONDARY — the empty-poll streak below applies ONLY to genuinely CI-less repos
|
|
||||||
# (default branch also has no CI history, e.g. device-imaging class), where burning
|
|
||||||
# the full timeout would be pure waste. There, NO_CI_MAX empty polls => fast-exit 0.
|
|
||||||
#
|
|
||||||
# Probe failure is treated conservatively as REPO_HAS_CI=1 (assume CI present): we
|
|
||||||
# would rather wait-then-timeout than risk a false-green, per the merge-gate priority.
|
|
||||||
REPO_HAS_CI=1
|
|
||||||
detect_repo_ci() {
|
|
||||||
local def_branch def_status
|
|
||||||
# Every early exit returns 0: a probe miss must leave the conservative
|
|
||||||
# REPO_HAS_CI=1 default in place, never abort the caller under `set -e`.
|
|
||||||
if [[ "$PLATFORM" == "github" ]]; then
|
|
||||||
def_branch=$(github_get_default_branch 2>/dev/null) || {
|
|
||||||
echo "[pr-ci-wait] WARN: default-branch probe failed; assuming CI-enabled (will not fast-green on empty status)."; return 0; }
|
|
||||||
[[ -n "$def_branch" ]] || return 0
|
|
||||||
def_status=$(github_get_commit_status_json "$OWNER" "$REPO" "$def_branch" 2>/dev/null | extract_state_from_status_json) || return 0
|
|
||||||
else
|
|
||||||
def_branch=$(gitea_get_default_branch "$HOST" "$OWNER/$REPO" "$TOKEN" 2>/dev/null) || {
|
|
||||||
echo "[pr-ci-wait] WARN: default-branch probe failed; assuming CI-enabled (will not fast-green on empty status)."; return 0; }
|
|
||||||
[[ -n "$def_branch" ]] || return 0
|
|
||||||
def_status=$(gitea_get_commit_status_json "$HOST" "$OWNER/$REPO" "$TOKEN" "$def_branch" 2>/dev/null | extract_state_from_status_json) || return 0
|
|
||||||
fi
|
|
||||||
if [[ "$def_status" == "no-status" || -z "$def_status" ]]; then
|
|
||||||
REPO_HAS_CI=0
|
|
||||||
echo "[pr-ci-wait] default branch '${def_branch}' has no CI status history — treating repo as CI-less (empty-poll fast-exit enabled)."
|
|
||||||
else
|
|
||||||
REPO_HAS_CI=1
|
|
||||||
echo "[pr-ci-wait] default branch '${def_branch}' has CI history (state=${def_status}) — repo runs CI; empty status on PR head => awaiting registration, will not fast-green."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
detect_repo_ci || true
|
|
||||||
|
|
||||||
NO_CI_STREAK=0
|
|
||||||
NO_CI_MAX=3
|
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
NOW_TS=$(date +%s)
|
NOW_TS=$(date +%s)
|
||||||
if (( NOW_TS > DEADLINE_TS )); then
|
if (( NOW_TS > DEADLINE_TS )); then
|
||||||
@@ -337,35 +272,11 @@ while true; do
|
|||||||
echo "Error: CI reported ${STATE} for PR #$PR_NUMBER." >&2
|
echo "Error: CI reported ${STATE} for PR #$PR_NUMBER." >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
no-status)
|
|
||||||
if [[ "$REPO_HAS_CI" == "1" ]]; then
|
|
||||||
# PRIMARY tier: repo demonstrably runs CI but this commit's pipeline
|
|
||||||
# has not registered yet (webhook/queue lag). Do NOT fast-green — keep
|
|
||||||
# polling until it registers or the timeout fires. Reset the streak so
|
|
||||||
# a later genuine CI-less misread can't accumulate across this state.
|
|
||||||
NO_CI_STREAK=0
|
|
||||||
echo "[pr-ci-wait] empty status on PR head but repo runs CI — awaiting pipeline registration (webhook lag), not fast-greening."
|
|
||||||
else
|
|
||||||
# SECONDARY tier: genuinely CI-less repo (default branch has no CI
|
|
||||||
# history either). Empty polls => fast-exit green after NO_CI_MAX.
|
|
||||||
NO_CI_STREAK=$((NO_CI_STREAK + 1))
|
|
||||||
if (( NO_CI_STREAK >= NO_CI_MAX )); then
|
|
||||||
echo "[INFO] no CI configured for this repo/commit (PR #$PR_NUMBER, ${NO_CI_STREAK} consecutive empty polls, default branch also CI-less); treating as green."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
sleep "$INTERVAL_SEC"
|
|
||||||
;;
|
|
||||||
pending|unknown)
|
pending|unknown)
|
||||||
# A pipeline exists but hasn't reached a terminal state (or is
|
|
||||||
# transiently ambiguous) — keep waiting, and reset the no-CI streak
|
|
||||||
# since this commit is not in the "no CI at all" condition.
|
|
||||||
NO_CI_STREAK=0
|
|
||||||
sleep "$INTERVAL_SEC"
|
sleep "$INTERVAL_SEC"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "[pr-ci-wait] Unrecognized state '${STATE}', continuing to poll..."
|
echo "[pr-ci-wait] Unrecognized state '${STATE}', continuing to poll..."
|
||||||
NO_CI_STREAK=0
|
|
||||||
sleep "$INTERVAL_SEC"
|
sleep "$INTERVAL_SEC"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -57,12 +57,20 @@ curl_gitea_pull() {
|
|||||||
local token basic_auth raw_code body_file http_code
|
local token basic_auth raw_code body_file http_code
|
||||||
body_file=$(mktemp)
|
body_file=$(mktemp)
|
||||||
|
|
||||||
|
# shellcheck disable=SC2329 # Invoked by the RETURN trap below.
|
||||||
|
cleanup_gitea_pull_body() {
|
||||||
|
local status=$?
|
||||||
|
rm -f -- "$body_file"
|
||||||
|
trap - RETURN
|
||||||
|
return "$status"
|
||||||
|
}
|
||||||
|
trap cleanup_gitea_pull_body RETURN
|
||||||
|
|
||||||
token=$(get_gitea_token "$HOST" || true)
|
token=$(get_gitea_token "$HOST" || true)
|
||||||
if [[ -n "$token" ]]; then
|
if [[ -n "$token" ]]; then
|
||||||
raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" -H "User-Agent: curl/8" -H "Authorization: token $token" "$api_url" || true)
|
raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" -H "User-Agent: curl/8" -H "Authorization: token $token" "$api_url" || true)
|
||||||
if [[ "$raw_code" =~ ^2 ]]; then
|
if [[ "$raw_code" =~ ^2 ]]; then
|
||||||
cat "$body_file"
|
cat "$body_file" || return $?
|
||||||
rm -f "$body_file"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
http_code="$raw_code"
|
http_code="$raw_code"
|
||||||
@@ -72,8 +80,7 @@ curl_gitea_pull() {
|
|||||||
if [[ -n "$basic_auth" ]]; then
|
if [[ -n "$basic_auth" ]]; then
|
||||||
raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" -u "$basic_auth" -H "User-Agent: curl/8" "$api_url" || true)
|
raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" -u "$basic_auth" -H "User-Agent: curl/8" "$api_url" || true)
|
||||||
if [[ "$raw_code" =~ ^2 ]]; then
|
if [[ "$raw_code" =~ ^2 ]]; then
|
||||||
cat "$body_file"
|
cat "$body_file" || return $?
|
||||||
rm -f "$body_file"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
http_code="$raw_code"
|
http_code="$raw_code"
|
||||||
@@ -96,7 +103,6 @@ except Exception:
|
|||||||
message = open(path, encoding="utf-8", errors="replace").read()[:200] or "empty response"
|
message = open(path, encoding="utf-8", errors="replace").read()[:200] or "empty response"
|
||||||
print(f"Error: Gitea pull request API request failed with HTTP {code}: {message}")
|
print(f"Error: Gitea pull request API request failed with HTTP {code}: {message}")
|
||||||
PY
|
PY
|
||||||
rm -f "$body_file"
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-metadata-gitea}"
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-metadata-gitea}"
|
||||||
REPO_DIR="$WORK_DIR/repo"
|
REPO_DIR="$WORK_DIR/repo"
|
||||||
FIXTURE_DIR="$WORK_DIR/fixtures"
|
FIXTURE_DIR="$WORK_DIR/fixtures"
|
||||||
|
STUB_DIR="$WORK_DIR/stubs"
|
||||||
|
|
||||||
rm -rf "$WORK_DIR"
|
rm -rf "$WORK_DIR"
|
||||||
mkdir -p "$REPO_DIR" "$FIXTURE_DIR"
|
mkdir -p "$REPO_DIR" "$FIXTURE_DIR" "$STUB_DIR"
|
||||||
|
|
||||||
git -C "$REPO_DIR" init -q
|
git -C "$REPO_DIR" init -q
|
||||||
git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git
|
git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git
|
||||||
@@ -56,6 +57,150 @@ cat > "$FIXTURE_DIR/gitea-error.json" <<'JSON'
|
|||||||
{"message": "user does not exist [uid: 0, name: ]", "url": "https://git.uscllc.com/api/swagger"}
|
{"message": "user does not exist [uid: 0, name: ]", "url": "https://git.uscllc.com/api/swagger"}
|
||||||
JSON
|
JSON
|
||||||
|
|
||||||
|
cat > "$STUB_DIR/curl" <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
output_file=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-o)
|
||||||
|
output_file="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-w|-H|-u)
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-s|-S|-sS)
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$output_file" ]]; then
|
||||||
|
echo "curl stub expected -o <output_file>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${MOSAIC_STUB_CURL_MODE:-success}" in
|
||||||
|
success)
|
||||||
|
cat > "$output_file" <<'JSON'
|
||||||
|
{
|
||||||
|
"number": 1910,
|
||||||
|
"title": "Live curl path",
|
||||||
|
"state": "open",
|
||||||
|
"user": {"login": "edith"},
|
||||||
|
"head": {"ref": "fix/live-curl-path"},
|
||||||
|
"base": {"ref": "main"},
|
||||||
|
"html_url": "https://git.example.test/acme/widgets/pulls/1910"
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
printf '200'
|
||||||
|
;;
|
||||||
|
cat-fails-after-2xx)
|
||||||
|
rm -f -- "$output_file"
|
||||||
|
ln -s /nonexistent/pr-metadata-body "$output_file"
|
||||||
|
printf '200'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown MOSAIC_STUB_CURL_MODE=${MOSAIC_STUB_CURL_MODE:-}" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
SH
|
||||||
|
chmod +x "$STUB_DIR/curl"
|
||||||
|
|
||||||
|
assert_tmpdir_empty() {
|
||||||
|
local tmpdir="$1" leftover
|
||||||
|
leftover=$(find "$tmpdir" -mindepth 1 -print -quit)
|
||||||
|
if [[ -n "$leftover" ]]; then
|
||||||
|
echo "Expected tmpfile cleanup, found leftover: $leftover" >&2
|
||||||
|
find "$tmpdir" -mindepth 1 -maxdepth 1 -ls >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_curl_success_case() {
|
||||||
|
local tmpdir="$WORK_DIR/tmp-success" stderr_file="$WORK_DIR/curl-success.stderr"
|
||||||
|
local output status
|
||||||
|
mkdir -p "$tmpdir"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
output=$(cd "$REPO_DIR" && \
|
||||||
|
PATH="$STUB_DIR:$PATH" \
|
||||||
|
TMPDIR="$tmpdir" \
|
||||||
|
GITEA_TOKEN="stub-token" \
|
||||||
|
GITEA_URL="https://git.example.test" \
|
||||||
|
MOSAIC_STUB_CURL_MODE="success" \
|
||||||
|
"$SCRIPT_DIR/pr-metadata.sh" -n 1910 2>"$stderr_file")
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ "$status" -ne 0 ]]; then
|
||||||
|
echo "Expected curl success path to pass, got status $status" >&2
|
||||||
|
cat "$stderr_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if grep -q "unbound variable" "$stderr_file"; then
|
||||||
|
echo "curl success path emitted unbound-variable cleanup noise" >&2
|
||||||
|
cat "$stderr_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
assert_tmpdir_empty "$tmpdir"
|
||||||
|
|
||||||
|
PR_METADATA_OUTPUT="$output" python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
data = json.loads(os.environ["PR_METADATA_OUTPUT"])
|
||||||
|
assert data["number"] == 1910, data
|
||||||
|
assert data["baseRefName"] == "main", data
|
||||||
|
assert data["headRefName"] == "fix/live-curl-path", data
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
run_curl_early_exit_cleanup_case() {
|
||||||
|
local tmpdir="$WORK_DIR/tmp-early-exit" stderr_file="$WORK_DIR/curl-early-exit.stderr"
|
||||||
|
local output status
|
||||||
|
mkdir -p "$tmpdir"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
output=$(cd "$REPO_DIR" && \
|
||||||
|
PATH="$STUB_DIR:$PATH" \
|
||||||
|
TMPDIR="$tmpdir" \
|
||||||
|
GITEA_TOKEN="stub-token" \
|
||||||
|
GITEA_URL="https://git.example.test" \
|
||||||
|
MOSAIC_STUB_CURL_MODE="cat-fails-after-2xx" \
|
||||||
|
"$SCRIPT_DIR/pr-metadata.sh" -n 1910 2>"$stderr_file")
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ "$status" -eq 0 ]]; then
|
||||||
|
echo "Expected unreadable 2xx body path to fail" >&2
|
||||||
|
printf '%s\n' "$output" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if grep -q "unbound variable" "$stderr_file"; then
|
||||||
|
echo "curl early-exit path emitted unbound-variable cleanup noise" >&2
|
||||||
|
cat "$stderr_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -q "No such file or directory" "$stderr_file"; then
|
||||||
|
echo "Expected body-read failure from broken symlink path" >&2
|
||||||
|
cat "$stderr_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if grep -q "Gitea API returned non-JSON" "$stderr_file"; then
|
||||||
|
echo "curl helper masked body-read failure as later JSON parsing failure" >&2
|
||||||
|
cat "$stderr_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
assert_tmpdir_empty "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
run_case() {
|
run_case() {
|
||||||
local fixture="$1" expected_number="$2" expected_head="$3"
|
local fixture="$1" expected_number="$2" expected_head="$3"
|
||||||
local output
|
local output
|
||||||
@@ -77,6 +222,8 @@ PY
|
|||||||
run_case "$FIXTURE_DIR/gitea-standard.json" 1905 edith/t_39ce717c-authentik-smoke-gate
|
run_case "$FIXTURE_DIR/gitea-standard.json" 1905 edith/t_39ce717c-authentik-smoke-gate
|
||||||
run_case "$FIXTURE_DIR/gitea-fallback.json" 1908 fix/fallback-head
|
run_case "$FIXTURE_DIR/gitea-fallback.json" 1908 fix/fallback-head
|
||||||
run_case "$FIXTURE_DIR/gitea-refs-pull-label.json" 1908 fix/t_23fa9e1d-portal-health-backend
|
run_case "$FIXTURE_DIR/gitea-refs-pull-label.json" 1908 fix/t_23fa9e1d-portal-health-backend
|
||||||
|
run_curl_success_case
|
||||||
|
run_curl_early_exit_cleanup_case
|
||||||
|
|
||||||
if cd "$REPO_DIR" && MOSAIC_GITEA_PR_METADATA_RAW_FILE="$FIXTURE_DIR/gitea-error.json" "$SCRIPT_DIR/pr-metadata.sh" -n 1909 >/dev/null 2>"$WORK_DIR/error.log"; then
|
if cd "$REPO_DIR" && MOSAIC_GITEA_PR_METADATA_RAW_FILE="$FIXTURE_DIR/gitea-error.json" "$SCRIPT_DIR/pr-metadata.sh" -n 1909 >/dev/null 2>"$WORK_DIR/error.log"; then
|
||||||
echo "Expected API error fixture to fail" >&2
|
echo "Expected API error fixture to fail" >&2
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ wp_resolve_repo_id() {
|
|||||||
local full_name="$1"
|
local full_name="$1"
|
||||||
local response http_code body repo_id
|
local response http_code body repo_id
|
||||||
|
|
||||||
response=$(curl -sS -w "\n%{http_code}" \
|
response=$(curl -sk -w "\n%{http_code}" \
|
||||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||||
"${WOODPECKER_URL}/api/repos/lookup/${full_name}")
|
"${WOODPECKER_URL}/api/repos/lookup/${full_name}")
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ fi
|
|||||||
# Resolve owner/repo to numeric ID (Woodpecker v3 API)
|
# Resolve owner/repo to numeric ID (Woodpecker v3 API)
|
||||||
REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
|
REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
|
||||||
|
|
||||||
response=$(curl -sS -w "\n%{http_code}" \
|
response=$(curl -sk -w "\n%{http_code}" \
|
||||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||||
"${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines?perPage=${LIMIT}")
|
"${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines?perPage=${LIMIT}")
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
|
|||||||
_wp_fetch() {
|
_wp_fetch() {
|
||||||
local ep="$1"
|
local ep="$1"
|
||||||
local resp http_code body
|
local resp http_code body
|
||||||
resp=$(curl -sS -w "\n%{http_code}" \
|
resp=$(curl -sk -w "\n%{http_code}" \
|
||||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||||
"$ep")
|
"$ep")
|
||||||
http_code=$(echo "$resp" | tail -n1)
|
http_code=$(echo "$resp" | tail -n1)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
|
|||||||
|
|
||||||
echo "Triggering pipeline for $REPO on branch $BRANCH..."
|
echo "Triggering pipeline for $REPO on branch $BRANCH..."
|
||||||
|
|
||||||
response=$(curl -sS -w "\n%{http_code}" -X POST \
|
response=$(curl -sk -w "\n%{http_code}" -X POST \
|
||||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$(jq -n --arg b "$BRANCH" '{branch: $b}')" \
|
-d "$(jq -n --arg b "$BRANCH" '{branch: $b}')" \
|
||||||
|
|||||||
Reference in New Issue
Block a user