Compare commits
2 Commits
fix/toolin
...
fix/wrappe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e8a9cfa8d | ||
|
|
b90aec2024 |
@@ -51,48 +51,3 @@ 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,7 +16,12 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
MOSAIC_CREDENTIALS_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}"
|
if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
|
||||||
|
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
|
||||||
@@ -34,6 +39,19 @@ _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() {
|
||||||
@@ -261,7 +279,8 @@ mosaic_http() {
|
|||||||
local base_url="${4:-}"
|
local base_url="${4:-}"
|
||||||
|
|
||||||
local response
|
local response
|
||||||
response=$(curl -sk -w "\n%{http_code}" -X "$method" \
|
local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}")
|
||||||
|
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}")
|
||||||
@@ -279,7 +298,8 @@ mosaic_http_post() {
|
|||||||
local base_url="${4:-}"
|
local base_url="${4:-}"
|
||||||
|
|
||||||
local response
|
local response
|
||||||
response=$(curl -sk -w "\n%{http_code}" -X POST \
|
local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}")
|
||||||
|
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" \
|
||||||
@@ -297,7 +317,8 @@ mosaic_http_patch() {
|
|||||||
local base_url="${4:-}"
|
local base_url="${4:-}"
|
||||||
|
|
||||||
local response
|
local response
|
||||||
response=$(curl -sk -w "\n%{http_code}" -X PATCH \
|
local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}")
|
||||||
|
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,32 +98,27 @@ case "$PLATFORM" in
|
|||||||
;;
|
;;
|
||||||
gitea)
|
gitea)
|
||||||
# tea issue edit syntax
|
# tea issue edit syntax
|
||||||
REPO_SLUG=$(get_repo_slug) || {
|
REPO_ARGS=$(get_gitea_repo_args) || {
|
||||||
echo "Error: Could not resolve Gitea repo slug from remote" >&2
|
echo "Error: Could not resolve Gitea repo/login args for remote host" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
REPO_LOGIN=$(get_gitea_login) || {
|
CMD="tea issue edit $ISSUE $REPO_ARGS"
|
||||||
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+=(--assignees "$ASSIGNEE")
|
CMD="$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+=(--labels "$LABELS")
|
CMD="$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+=(--milestone "$MILESTONE_ID")
|
CMD="$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
|
||||||
@@ -131,7 +126,7 @@ case "$PLATFORM" in
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$NEEDS_EDIT" == true ]]; then
|
if [[ "$NEEDS_EDIT" == true ]]; then
|
||||||
"${CMD[@]}"
|
eval "$CMD"
|
||||||
echo "Issue #$ISSUE updated successfully"
|
echo "Issue #$ISSUE updated successfully"
|
||||||
else
|
else
|
||||||
echo "No changes specified"
|
echo "No changes specified"
|
||||||
|
|||||||
@@ -63,28 +63,24 @@ 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+=(--title "$TITLE")
|
[[ -n "$TITLE" ]] && CMD="$CMD --title \"$TITLE\""
|
||||||
[[ -n "$BODY" ]] && CMD+=(--body "$BODY")
|
[[ -n "$BODY" ]] && CMD="$CMD --body \"$BODY\""
|
||||||
[[ -n "$LABELS" ]] && CMD+=(--add-label "$LABELS")
|
[[ -n "$LABELS" ]] && CMD="$CMD --add-label \"$LABELS\""
|
||||||
[[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE")
|
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||||
"${CMD[@]}"
|
eval $CMD
|
||||||
echo "Updated GitHub issue #$ISSUE_NUMBER"
|
echo "Updated GitHub issue #$ISSUE_NUMBER"
|
||||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||||
REPO_SLUG=$(get_repo_slug) || {
|
REPO_ARGS=$(get_gitea_repo_args) || {
|
||||||
echo "Error: Could not resolve Gitea repo slug from remote" >&2
|
echo "Error: Could not resolve Gitea repo/login args for remote host" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
REPO_LOGIN=$(get_gitea_login) || {
|
CMD="tea issue edit $ISSUE_NUMBER $REPO_ARGS"
|
||||||
echo "Error: Could not resolve Gitea login for remote host" >&2
|
[[ -n "$TITLE" ]] && CMD="$CMD --title \"$TITLE\""
|
||||||
exit 1
|
[[ -n "$BODY" ]] && CMD="$CMD --description \"$BODY\""
|
||||||
}
|
[[ -n "$LABELS" ]] && CMD="$CMD --add-labels \"$LABELS\""
|
||||||
CMD=(tea issue edit "$ISSUE_NUMBER" --repo "$REPO_SLUG" --login "$REPO_LOGIN")
|
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||||
[[ -n "$TITLE" ]] && CMD+=(--title "$TITLE")
|
eval $CMD
|
||||||
[[ -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,15 +99,10 @@ fi
|
|||||||
case "$PLATFORM" in
|
case "$PLATFORM" in
|
||||||
github)
|
github)
|
||||||
# GitHub uses the API for milestone creation
|
# GitHub uses the API for milestone creation
|
||||||
# Use jq to safely construct JSON so titles/descriptions containing
|
JSON_PAYLOAD="{\"title\":\"$TITLE\""
|
||||||
# quotes or special characters do not corrupt the payload (F-07).
|
[[ -n "$DESCRIPTION" ]] && JSON_PAYLOAD="$JSON_PAYLOAD,\"description\":\"$DESCRIPTION\""
|
||||||
JSON_PAYLOAD=$(jq -n \
|
[[ -n "$DUE_DATE" ]] && JSON_PAYLOAD="$JSON_PAYLOAD,\"due_on\":\"${DUE_DATE}T00:00:00Z\""
|
||||||
--arg t "$TITLE" \
|
JSON_PAYLOAD="$JSON_PAYLOAD}"
|
||||||
--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,6 +72,11 @@ 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
|
||||||
@@ -142,6 +147,21 @@ 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)
|
||||||
@@ -245,6 +265,51 @@ 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
|
||||||
@@ -272,11 +337,35 @@ 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,20 +57,12 @@ 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" || return $?
|
cat "$body_file"
|
||||||
|
rm -f "$body_file"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
http_code="$raw_code"
|
http_code="$raw_code"
|
||||||
@@ -80,7 +72,8 @@ 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" || return $?
|
cat "$body_file"
|
||||||
|
rm -f "$body_file"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
http_code="$raw_code"
|
http_code="$raw_code"
|
||||||
@@ -103,6 +96,7 @@ 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,10 +7,9 @@ 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" "$STUB_DIR"
|
mkdir -p "$REPO_DIR" "$FIXTURE_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
|
||||||
@@ -57,150 +56,6 @@ 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
|
||||||
@@ -222,8 +77,6 @@ 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 -sk -w "\n%{http_code}" \
|
response=$(curl -sS -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 -sk -w "\n%{http_code}" \
|
response=$(curl -sS -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 -sk -w "\n%{http_code}" \
|
resp=$(curl -sS -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 -sk -w "\n%{http_code}" -X POST \
|
response=$(curl -sS -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