2026-03-18 02:05:54 +00:00
|
|
|
#!/usr/bin/env bash
|
2026-03-21 19:59:55 +00:00
|
|
|
set -euo pipefail
|
2026-03-18 02:05:54 +00:00
|
|
|
# ci-helpers.sh — Shared CI helper functions
|
|
|
|
|
#
|
|
|
|
|
# Source from any script: source "$(dirname "$0")/../lib/ci-helpers.sh"
|
2026-03-19 09:03:03 +00:00
|
|
|
# ci_passed() requires: WOODPECKER_REPO_ID (from env.sh / project config)
|
2026-03-23 17:19:01 +00:00
|
|
|
# ci_commit_status() / ci_pipeline_number() require: woodpecker_api(), forge_api() (from env.sh)
|
2026-03-19 09:03:03 +00:00
|
|
|
# classify_pipeline_failure() requires: woodpecker_api() (defined in env.sh)
|
2026-03-18 02:05:54 +00:00
|
|
|
|
2026-03-21 05:06:12 +00:00
|
|
|
# ensure_blocked_label_id — look up (or create) the "blocked" label, print its ID.
|
|
|
|
|
# Caches the result in _BLOCKED_LABEL_ID to avoid repeated API calls.
|
fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
creates admin + bot users (dev-bot, review-bot), generates API
tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
→forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:57:12 +00:00
|
|
|
# Requires: FORGE_TOKEN, FORGE_API (from env.sh), forge_api()
|
2026-03-21 05:06:12 +00:00
|
|
|
ensure_blocked_label_id() {
|
|
|
|
|
if [ -n "${_BLOCKED_LABEL_ID:-}" ]; then
|
|
|
|
|
printf '%s' "$_BLOCKED_LABEL_ID"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
creates admin + bot users (dev-bot, review-bot), generates API
tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
→forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:57:12 +00:00
|
|
|
_BLOCKED_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
|
2026-03-21 05:06:12 +00:00
|
|
|
| jq -r '.[] | select(.name == "blocked") | .id' 2>/dev/null || true)
|
|
|
|
|
if [ -z "$_BLOCKED_LABEL_ID" ]; then
|
|
|
|
|
_BLOCKED_LABEL_ID=$(curl -sf -X POST \
|
fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
creates admin + bot users (dev-bot, review-bot), generates API
tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
→forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:57:12 +00:00
|
|
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
2026-03-21 05:06:12 +00:00
|
|
|
-H "Content-Type: application/json" \
|
fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
creates admin + bot users (dev-bot, review-bot), generates API
tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
→forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:57:12 +00:00
|
|
|
"${FORGE_API}/labels" \
|
2026-03-21 05:06:12 +00:00
|
|
|
-d '{"name":"blocked","color":"#e11d48"}' 2>/dev/null \
|
|
|
|
|
| jq -r '.id // empty' 2>/dev/null || true)
|
|
|
|
|
fi
|
|
|
|
|
printf '%s' "$_BLOCKED_LABEL_ID"
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 13:41:57 +00:00
|
|
|
# ensure_priority_label — look up (or create) the "priority" label, print its ID.
|
|
|
|
|
# Caches the result in _PRIORITY_LABEL_ID to avoid repeated API calls.
|
fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
creates admin + bot users (dev-bot, review-bot), generates API
tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
→forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:57:12 +00:00
|
|
|
# Requires: FORGE_TOKEN, FORGE_API (from env.sh), forge_api()
|
2026-03-22 13:41:57 +00:00
|
|
|
ensure_priority_label() {
|
|
|
|
|
if [ -n "${_PRIORITY_LABEL_ID:-}" ]; then
|
|
|
|
|
printf '%s' "$_PRIORITY_LABEL_ID"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
creates admin + bot users (dev-bot, review-bot), generates API
tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
→forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:57:12 +00:00
|
|
|
_PRIORITY_LABEL_ID=$(forge_api GET "/labels" 2>/dev/null \
|
2026-03-22 13:41:57 +00:00
|
|
|
| jq -r '.[] | select(.name == "priority") | .id' 2>/dev/null || true)
|
|
|
|
|
if [ -z "$_PRIORITY_LABEL_ID" ]; then
|
|
|
|
|
_PRIORITY_LABEL_ID=$(curl -sf -X POST \
|
fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
creates admin + bot users (dev-bot, review-bot), generates API
tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
→forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:57:12 +00:00
|
|
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
2026-03-22 13:41:57 +00:00
|
|
|
-H "Content-Type: application/json" \
|
fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
creates admin + bot users (dev-bot, review-bot), generates API
tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
→forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:57:12 +00:00
|
|
|
"${FORGE_API}/labels" \
|
2026-03-22 13:41:57 +00:00
|
|
|
-d '{"name":"priority","color":"#f59e0b"}' 2>/dev/null \
|
|
|
|
|
| jq -r '.id // empty' 2>/dev/null || true)
|
|
|
|
|
fi
|
|
|
|
|
printf '%s' "$_PRIORITY_LABEL_ID"
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 13:48:00 +00:00
|
|
|
# diff_has_code_files — check if file list (stdin, one per line) contains code files
|
|
|
|
|
# Non-code paths: docs/*, formulas/*, evidence/*, *.md
|
|
|
|
|
# Returns 0 if any code file found, 1 if all files are non-code.
|
|
|
|
|
diff_has_code_files() {
|
|
|
|
|
while IFS= read -r f; do
|
|
|
|
|
[ -z "$f" ] && continue
|
|
|
|
|
case "$f" in
|
|
|
|
|
docs/*|formulas/*|evidence/*) continue ;;
|
|
|
|
|
*.md) continue ;;
|
|
|
|
|
*) return 0 ;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ci_required_for_pr <pr_number> — check if CI is needed for this PR
|
|
|
|
|
# Returns 0 if PR has code files (CI required), 1 if non-code only (CI not required).
|
|
|
|
|
ci_required_for_pr() {
|
|
|
|
|
local pr_num="$1"
|
2026-03-20 06:55:00 +00:00
|
|
|
local files all_json
|
fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
creates admin + bot users (dev-bot, review-bot), generates API
tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
→forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:57:12 +00:00
|
|
|
all_json=$(forge_api_all "/pulls/${pr_num}/files") || return 0
|
2026-03-20 06:55:00 +00:00
|
|
|
files=$(printf '%s' "$all_json" | jq -r '.[].filename' 2>/dev/null) || return 0
|
2026-03-19 13:48:00 +00:00
|
|
|
if [ -z "$files" ]; then
|
|
|
|
|
return 0 # empty file list — require CI as safety default
|
|
|
|
|
fi
|
|
|
|
|
echo "$files" | diff_has_code_files
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 02:05:54 +00:00
|
|
|
# ci_passed <state> — check if CI is passing (or no CI configured)
|
|
|
|
|
# Returns 0 if state is "success", or if no CI is configured and
|
|
|
|
|
# state is empty/pending/unknown.
|
|
|
|
|
ci_passed() {
|
|
|
|
|
local state="$1"
|
|
|
|
|
if [ "$state" = "success" ]; then return 0; fi
|
|
|
|
|
if [ "${WOODPECKER_REPO_ID:-2}" = "0" ] && { [ -z "$state" ] || [ "$state" = "pending" ] || [ "$state" = "unknown" ]; }; then
|
|
|
|
|
return 0 # no CI configured
|
|
|
|
|
fi
|
|
|
|
|
return 1
|
|
|
|
|
}
|
2026-03-19 08:51:30 +00:00
|
|
|
|
2026-03-21 06:10:39 +00:00
|
|
|
# ci_failed <state> — check if CI has definitively failed
|
|
|
|
|
# Returns 0 if state indicates a real failure (not success, not pending,
|
|
|
|
|
# not unknown, not empty).
|
|
|
|
|
ci_failed() {
|
|
|
|
|
local state="$1"
|
|
|
|
|
if [ -z "$state" ] || [ "$state" = "pending" ] || [ "$state" = "unknown" ]; then
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
if ci_passed "$state"; then
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 17:19:01 +00:00
|
|
|
# ci_commit_status <sha> — get CI state for a commit
|
|
|
|
|
# Queries Woodpecker API directly, falls back to forge commit status API.
|
|
|
|
|
ci_commit_status() {
|
|
|
|
|
local sha="$1"
|
|
|
|
|
local state=""
|
|
|
|
|
|
|
|
|
|
# Primary: ask Woodpecker directly
|
|
|
|
|
if [ -n "${WOODPECKER_REPO_ID:-}" ] && [ "${WOODPECKER_REPO_ID}" != "0" ]; then
|
|
|
|
|
state=$(woodpecker_api "/repos/${WOODPECKER_REPO_ID}/pipelines" \
|
|
|
|
|
| jq -r --arg sha "$sha" \
|
|
|
|
|
'[.[] | select(.commit == $sha)] | sort_by(.number) | last | .status // empty' \
|
|
|
|
|
2>/dev/null) || true
|
|
|
|
|
# Map Woodpecker status to Gitea/Forgejo status names
|
|
|
|
|
case "$state" in
|
|
|
|
|
success) echo "success"; return ;;
|
|
|
|
|
failure|error|killed) echo "failure"; return ;;
|
|
|
|
|
running|pending|blocked) echo "pending"; return ;;
|
|
|
|
|
esac
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Fallback: forge commit status API (works with any Gitea/Forgejo)
|
|
|
|
|
forge_api GET "/commits/${sha}/status" 2>/dev/null \
|
|
|
|
|
| jq -r '.state // "unknown"'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ci_pipeline_number <sha> — get Woodpecker pipeline number for a commit
|
|
|
|
|
# Queries Woodpecker API directly, falls back to forge status target_url parsing.
|
|
|
|
|
ci_pipeline_number() {
|
|
|
|
|
local sha="$1"
|
|
|
|
|
|
|
|
|
|
# Primary: ask Woodpecker directly
|
|
|
|
|
if [ -n "${WOODPECKER_REPO_ID:-}" ] && [ "${WOODPECKER_REPO_ID}" != "0" ]; then
|
|
|
|
|
local num
|
|
|
|
|
num=$(woodpecker_api "/repos/${WOODPECKER_REPO_ID}/pipelines" \
|
|
|
|
|
| jq -r --arg sha "$sha" \
|
|
|
|
|
'[.[] | select(.commit == $sha)] | sort_by(.number) | last | .number // empty' \
|
|
|
|
|
2>/dev/null) || true
|
|
|
|
|
if [ -n "$num" ]; then
|
|
|
|
|
echo "$num"
|
|
|
|
|
return
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Fallback: extract from forge status target_url
|
|
|
|
|
forge_api GET "/commits/${sha}/status" 2>/dev/null \
|
|
|
|
|
| jq -r '.statuses[0].target_url // ""' \
|
|
|
|
|
| grep -oP 'pipeline/\K[0-9]+' | head -1 || true
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 19:19:29 +00:00
|
|
|
# is_infra_step <step_name> <exit_code> [log_data]
|
|
|
|
|
# Checks whether a single CI step failure matches infra heuristics.
|
|
|
|
|
# Returns 0 (infra) with reason on stdout, or 1 (not infra).
|
|
|
|
|
#
|
|
|
|
|
# Heuristics (union of P2e and classify_pipeline_failure patterns):
|
|
|
|
|
# - Clone/git step with exit 128 → connection failure / rate limit
|
|
|
|
|
# - Any step with exit 137 → OOM / killed by signal 9
|
|
|
|
|
# - Log patterns: connection timeout, docker pull timeout, TLS handshake timeout
|
|
|
|
|
is_infra_step() {
|
|
|
|
|
local sname="$1" ecode="$2" log_data="${3:-}"
|
|
|
|
|
|
fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
creates admin + bot users (dev-bot, review-bot), generates API
tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
→forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:57:12 +00:00
|
|
|
# Clone/git step exit 128 → forge connection failure / rate limit
|
2026-03-20 19:19:29 +00:00
|
|
|
if { [[ "$sname" == *clone* ]] || [[ "$sname" == git* ]]; } && [ "$ecode" = "128" ]; then
|
|
|
|
|
echo "${sname} exit 128 (connection failure)"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Exit 137 → OOM / killed by signal 9
|
|
|
|
|
if [ "$ecode" = "137" ]; then
|
|
|
|
|
echo "${sname} exit 137 (OOM/signal 9)"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Log-pattern matching for infra issues
|
|
|
|
|
if [ -n "$log_data" ] && \
|
|
|
|
|
printf '%s' "$log_data" | grep -qiE 'Failed to connect|connection timed out|docker pull.*timeout|TLS handshake timeout'; then
|
|
|
|
|
echo "${sname}: log matches infra pattern (timeout/connection)"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 08:51:30 +00:00
|
|
|
# classify_pipeline_failure <repo_id> <pipeline_num>
|
2026-03-20 19:19:29 +00:00
|
|
|
# Classifies a pipeline's failure type by inspecting failed steps.
|
|
|
|
|
# Uses is_infra_step() for per-step classification (exit codes + log patterns).
|
|
|
|
|
# Outputs "infra <reason>" if any failed step matches infra heuristics.
|
2026-03-19 08:51:30 +00:00
|
|
|
# Outputs "code" otherwise (including when steps cannot be determined).
|
|
|
|
|
# Returns 0 for infra, 1 for code or unclassifiable.
|
|
|
|
|
classify_pipeline_failure() {
|
|
|
|
|
local repo_id="$1" pip_num="$2"
|
2026-03-20 19:19:29 +00:00
|
|
|
local pip_json failed_steps _sname _ecode _spid _reason _log_data
|
2026-03-19 08:51:30 +00:00
|
|
|
|
|
|
|
|
pip_json=$(woodpecker_api "/repos/${repo_id}/pipelines/${pip_num}" 2>/dev/null) || {
|
|
|
|
|
echo "code"; return 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 19:19:29 +00:00
|
|
|
# Extract failed steps: name, exit_code, pid
|
2026-03-19 08:51:30 +00:00
|
|
|
failed_steps=$(printf '%s' "$pip_json" | jq -r '
|
|
|
|
|
.workflows[]?.children[]? |
|
|
|
|
|
select(.state == "failure" or .state == "error" or .state == "killed") |
|
2026-03-20 19:19:29 +00:00
|
|
|
"\(.name)\t\(.exit_code)\t\(.pid)"' 2>/dev/null)
|
2026-03-19 08:51:30 +00:00
|
|
|
|
|
|
|
|
if [ -z "$failed_steps" ]; then
|
|
|
|
|
echo "code"; return 1
|
|
|
|
|
fi
|
|
|
|
|
|
2026-03-20 19:19:29 +00:00
|
|
|
while IFS=$'\t' read -r _sname _ecode _spid; do
|
2026-03-19 08:51:30 +00:00
|
|
|
[ -z "$_sname" ] && continue
|
2026-03-20 19:19:29 +00:00
|
|
|
|
|
|
|
|
# Check name+exit_code patterns (no log fetch needed)
|
|
|
|
|
if _reason=$(is_infra_step "$_sname" "$_ecode"); then
|
|
|
|
|
echo "infra ${_reason}"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Fetch step logs and check log patterns
|
|
|
|
|
if [ -n "$_spid" ] && [ "$_spid" != "null" ]; then
|
|
|
|
|
_log_data=$(woodpecker_api "/repos/${repo_id}/logs/${pip_num}/${_spid}" \
|
|
|
|
|
--max-time 15 2>/dev/null \
|
|
|
|
|
| jq -r '.[].data // empty' 2>/dev/null | tail -200 || true)
|
|
|
|
|
if [ -n "$_log_data" ]; then
|
|
|
|
|
if _reason=$(is_infra_step "$_sname" "$_ecode" "$_log_data"); then
|
|
|
|
|
echo "infra ${_reason}"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
fi
|
2026-03-19 08:51:30 +00:00
|
|
|
fi
|
|
|
|
|
done <<< "$failed_steps"
|
|
|
|
|
|
|
|
|
|
echo "code"
|
|
|
|
|
return 1
|
|
|
|
|
}
|
2026-03-26 17:16:39 +00:00
|
|
|
|
|
|
|
|
# ci_promote <repo_id> <pipeline_number> <environment>
|
|
|
|
|
# Calls the Woodpecker promote API to trigger a deployment pipeline.
|
|
|
|
|
# The promote endpoint creates a new pipeline with event=deployment and
|
|
|
|
|
# deploy_to=<environment>, which fires pipelines filtered on that environment.
|
|
|
|
|
# Requires: WOODPECKER_TOKEN, WOODPECKER_SERVER (from env.sh)
|
|
|
|
|
# Returns 0 on success, 1 on failure. Prints the new pipeline number on success.
|
|
|
|
|
ci_promote() {
|
|
|
|
|
local repo_id="$1" pipeline_num="$2" environment="$3"
|
|
|
|
|
|
|
|
|
|
if [ -z "$repo_id" ] || [ -z "$pipeline_num" ] || [ -z "$environment" ]; then
|
|
|
|
|
echo "Usage: ci_promote <repo_id> <pipeline_number> <environment>" >&2
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
local resp new_num
|
|
|
|
|
resp=$(woodpecker_api "/repos/${repo_id}/pipelines/${pipeline_num}" \
|
|
|
|
|
-X POST \
|
|
|
|
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
|
|
|
-d "event=deployment&deploy_to=${environment}" 2>/dev/null) || {
|
|
|
|
|
echo "ERROR: promote API call failed" >&2
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
new_num=$(printf '%s' "$resp" | jq -r '.number // empty' 2>/dev/null)
|
|
|
|
|
if [ -z "$new_num" ]; then
|
|
|
|
|
echo "ERROR: promote returned no pipeline number" >&2
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "$new_num"
|
|
|
|
|
}
|