Merge pull request 'fix: Wire Woodpecker CI to local Forgejo (#612)' (#616) from fix/issue-612 into main
This commit is contained in:
commit
a2016db5c3
9 changed files with 250 additions and 25 deletions
45
BOOTSTRAP.md
45
BOOTSTRAP.md
|
|
@ -139,6 +139,51 @@ Verify no root-owned files exist in agent temp directories:
|
||||||
find /tmp/dev-* /tmp/harb-* /tmp/review-* -not -user debian 2>/dev/null
|
find /tmp/dev-* /tmp/harb-* /tmp/review-* -not -user debian 2>/dev/null
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 4b. Woodpecker CI + Forgejo Integration
|
||||||
|
|
||||||
|
`disinto init` automatically configures Woodpecker to use the local Forgejo instance as its forge backend if `WOODPECKER_SERVER` is set in `.env`. This includes:
|
||||||
|
|
||||||
|
1. Creating an OAuth2 application on Forgejo for Woodpecker
|
||||||
|
2. Writing `WOODPECKER_FORGEJO_*` env vars to `.env`
|
||||||
|
3. Activating the repo in Woodpecker
|
||||||
|
|
||||||
|
### Manual setup (if Woodpecker runs outside of `disinto init`)
|
||||||
|
|
||||||
|
If you manage Woodpecker separately, configure these env vars in its server config:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
WOODPECKER_FORGEJO=true
|
||||||
|
WOODPECKER_FORGEJO_URL=http://localhost:3000
|
||||||
|
WOODPECKER_FORGEJO_CLIENT=<oauth2-client-id>
|
||||||
|
WOODPECKER_FORGEJO_SECRET=<oauth2-client-secret>
|
||||||
|
```
|
||||||
|
|
||||||
|
To create the OAuth2 app on Forgejo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create OAuth2 application (redirect URI = Woodpecker authorize endpoint)
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"http://localhost:3000/api/v1/user/applications/oauth2" \
|
||||||
|
-d '{"name":"woodpecker-ci","redirect_uris":["http://localhost:8000/authorize"],"confidential_client":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The response contains `client_id` and `client_secret` for `WOODPECKER_FORGEJO_CLIENT` / `WOODPECKER_FORGEJO_SECRET`.
|
||||||
|
|
||||||
|
To activate the repo in Woodpecker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
woodpecker-cli repo add <org>/<repo>
|
||||||
|
# Or via API:
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer ${WOODPECKER_TOKEN}" \
|
||||||
|
"http://localhost:8000/api/repos" \
|
||||||
|
-d '{"forge_remote_id":"<org>/<repo>"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Woodpecker will now trigger pipelines on pushes to Forgejo and push commit status back. Disinto queries Woodpecker directly for CI status (with a forge API fallback), so pipeline results are visible even if Woodpecker's status push to Forgejo is delayed.
|
||||||
|
|
||||||
## 5. Prepare the Target Repo
|
## 5. Prepare the Target Repo
|
||||||
|
|
||||||
### Required: CI pipeline
|
### Required: CI pipeline
|
||||||
|
|
|
||||||
143
bin/disinto
143
bin/disinto
|
|
@ -551,6 +551,129 @@ install_cron() {
|
||||||
echo "Cron entries installed"
|
echo "Cron entries installed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Set up Woodpecker CI to use Forgejo as its forge backend.
|
||||||
|
# Creates an OAuth2 app on Forgejo for Woodpecker, activates the repo.
|
||||||
|
setup_woodpecker() {
|
||||||
|
local forge_url="$1" repo_slug="$2"
|
||||||
|
local wp_server="${WOODPECKER_SERVER:-}"
|
||||||
|
|
||||||
|
if [ -z "$wp_server" ]; then
|
||||||
|
echo "Woodpecker: not configured (WOODPECKER_SERVER not set), skipping"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if Woodpecker is reachable
|
||||||
|
if ! curl -sf --max-time 5 "${wp_server}/api/version" >/dev/null 2>&1; then
|
||||||
|
echo "Woodpecker: not reachable at ${wp_server}, skipping"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "── Woodpecker CI setup ────────────────────────────────"
|
||||||
|
echo "Server: ${wp_server}"
|
||||||
|
|
||||||
|
# Create OAuth2 application on Forgejo for Woodpecker
|
||||||
|
local oauth2_name="woodpecker-ci"
|
||||||
|
local redirect_uri="${wp_server}/authorize"
|
||||||
|
local existing_app client_id client_secret
|
||||||
|
|
||||||
|
# Check if OAuth2 app already exists
|
||||||
|
existing_app=$(curl -sf \
|
||||||
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
"${forge_url}/api/v1/user/applications/oauth2" 2>/dev/null \
|
||||||
|
| jq -r --arg name "$oauth2_name" '.[] | select(.name == $name) | .client_id // empty' 2>/dev/null) || true
|
||||||
|
|
||||||
|
if [ -n "$existing_app" ]; then
|
||||||
|
echo "OAuth2: ${oauth2_name} (already exists, client_id=${existing_app})"
|
||||||
|
client_id="$existing_app"
|
||||||
|
else
|
||||||
|
local oauth2_resp
|
||||||
|
oauth2_resp=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${forge_url}/api/v1/user/applications/oauth2" \
|
||||||
|
-d "{\"name\":\"${oauth2_name}\",\"redirect_uris\":[\"${redirect_uri}\"],\"confidential_client\":true}" \
|
||||||
|
2>/dev/null) || oauth2_resp=""
|
||||||
|
|
||||||
|
if [ -z "$oauth2_resp" ]; then
|
||||||
|
echo "Warning: failed to create OAuth2 app on Forgejo" >&2
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
client_id=$(printf '%s' "$oauth2_resp" | jq -r '.client_id // empty')
|
||||||
|
client_secret=$(printf '%s' "$oauth2_resp" | jq -r '.client_secret // empty')
|
||||||
|
|
||||||
|
if [ -z "$client_id" ]; then
|
||||||
|
echo "Warning: OAuth2 app creation returned no client_id" >&2
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OAuth2: ${oauth2_name} created (client_id=${client_id})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Store Woodpecker forge config in .env
|
||||||
|
local env_file="${FACTORY_ROOT}/.env"
|
||||||
|
local wp_vars=(
|
||||||
|
"WOODPECKER_FORGEJO=true"
|
||||||
|
"WOODPECKER_FORGEJO_URL=${forge_url}"
|
||||||
|
)
|
||||||
|
if [ -n "${client_id:-}" ]; then
|
||||||
|
wp_vars+=("WOODPECKER_FORGEJO_CLIENT=${client_id}")
|
||||||
|
fi
|
||||||
|
if [ -n "${client_secret:-}" ]; then
|
||||||
|
wp_vars+=("WOODPECKER_FORGEJO_SECRET=${client_secret}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
for var_line in "${wp_vars[@]}"; do
|
||||||
|
local var_name="${var_line%%=*}"
|
||||||
|
if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then
|
||||||
|
sed -i "s|^${var_name}=.*|${var_line}|" "$env_file"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$var_line" >> "$env_file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Config: Woodpecker forge vars written to .env"
|
||||||
|
|
||||||
|
# Activate repo in Woodpecker (if not already)
|
||||||
|
local wp_token="${WOODPECKER_TOKEN:-}"
|
||||||
|
if [ -z "$wp_token" ]; then
|
||||||
|
echo "Warning: WOODPECKER_TOKEN not set — cannot activate repo" >&2
|
||||||
|
echo " Activate manually: woodpecker-cli repo add ${repo_slug}" >&2
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local wp_repo_id
|
||||||
|
wp_repo_id=$(curl -sf \
|
||||||
|
-H "Authorization: Bearer ${wp_token}" \
|
||||||
|
"${wp_server}/api/repos/lookup/${repo_slug}" 2>/dev/null \
|
||||||
|
| jq -r '.id // empty' 2>/dev/null) || true
|
||||||
|
|
||||||
|
if [ -n "$wp_repo_id" ] && [ "$wp_repo_id" != "0" ]; then
|
||||||
|
echo "Repo: ${repo_slug} already active in Woodpecker (id=${wp_repo_id})"
|
||||||
|
else
|
||||||
|
local activate_resp
|
||||||
|
activate_resp=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: Bearer ${wp_token}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${wp_server}/api/repos" \
|
||||||
|
-d "{\"forge_remote_id\":\"${repo_slug}\"}" 2>/dev/null) || activate_resp=""
|
||||||
|
|
||||||
|
wp_repo_id=$(printf '%s' "$activate_resp" | jq -r '.id // empty' 2>/dev/null) || true
|
||||||
|
|
||||||
|
if [ -n "$wp_repo_id" ] && [ "$wp_repo_id" != "0" ]; then
|
||||||
|
echo "Repo: ${repo_slug} activated in Woodpecker (id=${wp_repo_id})"
|
||||||
|
else
|
||||||
|
echo "Warning: could not activate repo in Woodpecker" >&2
|
||||||
|
echo " Activate manually: woodpecker-cli repo add ${repo_slug}" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Store repo ID for later TOML generation
|
||||||
|
if [ -n "$wp_repo_id" ] && [ "$wp_repo_id" != "0" ]; then
|
||||||
|
_WP_REPO_ID="$wp_repo_id"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ── init command ─────────────────────────────────────────────────────────────
|
# ── init command ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
disinto_init() {
|
disinto_init() {
|
||||||
|
|
@ -684,6 +807,26 @@ p.write_text(text)
|
||||||
echo "Created: ${toml_path}"
|
echo "Created: ${toml_path}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Set up Woodpecker CI to use Forgejo as forge backend
|
||||||
|
_WP_REPO_ID=""
|
||||||
|
setup_woodpecker "$forge_url" "$forge_repo"
|
||||||
|
|
||||||
|
# Use detected Woodpecker repo ID if ci_id was not explicitly set
|
||||||
|
if [ "$ci_id" = "0" ] && [ -n "${_WP_REPO_ID:-}" ]; then
|
||||||
|
ci_id="$_WP_REPO_ID"
|
||||||
|
echo "CI ID: ${ci_id} (from Woodpecker)"
|
||||||
|
# Update TOML if it already exists
|
||||||
|
if [ "$toml_exists" = true ] && [ -f "$toml_path" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import sys, re, pathlib
|
||||||
|
p = pathlib.Path(sys.argv[1])
|
||||||
|
text = p.read_text()
|
||||||
|
text = re.sub(r'^woodpecker_repo_id\s*=\s*.*$', 'woodpecker_repo_id = ' + sys.argv[2], text, flags=re.MULTILINE)
|
||||||
|
p.write_text(text)
|
||||||
|
" "$toml_path" "$ci_id"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Create labels on remote
|
# Create labels on remote
|
||||||
create_labels "$forge_repo" "$forge_url"
|
create_labels "$forge_repo" "$forge_url"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -261,8 +261,7 @@ for i in $(seq 0 $(($(echo "$PL_PRS" | jq 'length') - 1))); do
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PL_CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
PL_CI_STATE=$(ci_commit_status "$PL_PR_SHA") || true
|
||||||
"${API}/commits/${PL_PR_SHA}/status" | jq -r '.state // "unknown"') || true
|
|
||||||
|
|
||||||
# Non-code PRs may have no CI — treat as passed
|
# Non-code PRs may have no CI — treat as passed
|
||||||
if ! ci_passed "$PL_CI_STATE" && ! ci_required_for_pr "$PL_PR_NUM"; then
|
if ! ci_passed "$PL_CI_STATE" && ! ci_required_for_pr "$PL_PR_NUM"; then
|
||||||
|
|
@ -397,8 +396,7 @@ if [ "$ORPHAN_COUNT" -gt 0 ]; then
|
||||||
if [ -n "$HAS_PR" ]; then
|
if [ -n "$HAS_PR" ]; then
|
||||||
PR_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
PR_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${API}/pulls/${HAS_PR}" | jq -r '.head.sha') || true
|
"${API}/pulls/${HAS_PR}" | jq -r '.head.sha') || true
|
||||||
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
CI_STATE=$(ci_commit_status "$PR_SHA") || true
|
||||||
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"') || true
|
|
||||||
|
|
||||||
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
|
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
|
||||||
if ! ci_passed "$CI_STATE" && ! ci_required_for_pr "$HAS_PR"; then
|
if ! ci_passed "$CI_STATE" && ! ci_required_for_pr "$HAS_PR"; then
|
||||||
|
|
@ -510,8 +508,7 @@ for i in $(seq 0 $(($(echo "$OPEN_PRS" | jq 'length') - 1))); do
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
CI_STATE=$(ci_commit_status "$PR_SHA") || true
|
||||||
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"') || true
|
|
||||||
|
|
||||||
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
|
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
|
||||||
if ! ci_passed "$CI_STATE" && ! ci_required_for_pr "$PR_NUM"; then
|
if ! ci_passed "$CI_STATE" && ! ci_required_for_pr "$PR_NUM"; then
|
||||||
|
|
@ -652,8 +649,7 @@ for i in $(seq 0 $((BACKLOG_COUNT - 1))); do
|
||||||
if [ -n "$EXISTING_PR" ]; then
|
if [ -n "$EXISTING_PR" ]; then
|
||||||
PR_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
PR_SHA=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${API}/pulls/${EXISTING_PR}" | jq -r '.head.sha') || true
|
"${API}/pulls/${EXISTING_PR}" | jq -r '.head.sha') || true
|
||||||
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
CI_STATE=$(ci_commit_status "$PR_SHA") || true
|
||||||
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"') || true
|
|
||||||
|
|
||||||
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
|
# Non-code PRs (docs, formulas, evidence) may have no CI — treat as passed
|
||||||
if ! ci_passed "$CI_STATE" && ! ci_required_for_pr "$EXISTING_PR"; then
|
if ! ci_passed "$CI_STATE" && ! ci_required_for_pr "$EXISTING_PR"; then
|
||||||
|
|
|
||||||
|
|
@ -346,8 +346,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
|
||||||
# Re-fetch HEAD — Claude may have pushed new commits since loop started
|
# Re-fetch HEAD — Claude may have pushed new commits since loop started
|
||||||
CI_CURRENT_SHA=$(git -C "${WORKTREE}" rev-parse HEAD 2>/dev/null || echo "$CI_CURRENT_SHA")
|
CI_CURRENT_SHA=$(git -C "${WORKTREE}" rev-parse HEAD 2>/dev/null || echo "$CI_CURRENT_SHA")
|
||||||
|
|
||||||
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
CI_STATE=$(ci_commit_status "$CI_CURRENT_SHA")
|
||||||
"${API}/commits/${CI_CURRENT_SHA}/status" | jq -r '.state // "unknown"')
|
|
||||||
if [ "$CI_STATE" = "success" ] || [ "$CI_STATE" = "failure" ] || [ "$CI_STATE" = "error" ]; then
|
if [ "$CI_STATE" = "success" ] || [ "$CI_STATE" = "failure" ] || [ "$CI_STATE" = "error" ]; then
|
||||||
CI_DONE=true
|
CI_DONE=true
|
||||||
[ "$CI_STATE" = "success" ] && CI_FIX_COUNT=0
|
[ "$CI_STATE" = "success" ] && CI_FIX_COUNT=0
|
||||||
|
|
@ -370,9 +369,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
|
||||||
echo \"PHASE:awaiting_review\" > \"${PHASE_FILE}\""
|
echo \"PHASE:awaiting_review\" > \"${PHASE_FILE}\""
|
||||||
else
|
else
|
||||||
# Fetch CI error details
|
# Fetch CI error details
|
||||||
PIPELINE_NUM=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
PIPELINE_NUM=$(ci_pipeline_number "$CI_CURRENT_SHA")
|
||||||
"${API}/commits/${CI_CURRENT_SHA}/status" | \
|
|
||||||
jq -r '.statuses[0].target_url // ""' | grep -oP 'pipeline/\K[0-9]+' | head -1 || true)
|
|
||||||
|
|
||||||
FAILED_STEP=""
|
FAILED_STEP=""
|
||||||
FAILED_EXIT=""
|
FAILED_EXIT=""
|
||||||
|
|
|
||||||
|
|
@ -429,8 +429,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait:
|
||||||
head_sha=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
head_sha=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
"${FORGE_API}/pulls/${_GARDENER_PR}" | jq -r '.head.sha // empty') || true
|
||||||
|
|
||||||
ci_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
ci_state=$(ci_commit_status "$head_sha") || ci_state="unknown"
|
||||||
"${FORGE_API}/commits/${head_sha}/status" | jq -r '.state // "unknown"') || ci_state="unknown"
|
|
||||||
|
|
||||||
case "$ci_state" in
|
case "$ci_state" in
|
||||||
success|failure|error) ci_done=true; break ;;
|
success|failure|error) ci_done=true; break ;;
|
||||||
|
|
@ -463,9 +462,7 @@ Write PHASE:awaiting_review to the phase file, then stop and wait:
|
||||||
|
|
||||||
# Get error details
|
# Get error details
|
||||||
local pipeline_num ci_error_log
|
local pipeline_num ci_error_log
|
||||||
pipeline_num=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
pipeline_num=$(ci_pipeline_number "$head_sha")
|
||||||
"${FORGE_API}/commits/${head_sha}/status" | \
|
|
||||||
jq -r '.statuses[0].target_url // ""' | grep -oP 'pipeline/\K[0-9]+' | head -1 || true)
|
|
||||||
|
|
||||||
ci_error_log=""
|
ci_error_log=""
|
||||||
if [ -n "$pipeline_num" ]; then
|
if [ -n "$pipeline_num" ]; then
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ set -euo pipefail
|
||||||
#
|
#
|
||||||
# Source from any script: source "$(dirname "$0")/../lib/ci-helpers.sh"
|
# Source from any script: source "$(dirname "$0")/../lib/ci-helpers.sh"
|
||||||
# ci_passed() requires: WOODPECKER_REPO_ID (from env.sh / project config)
|
# ci_passed() requires: WOODPECKER_REPO_ID (from env.sh / project config)
|
||||||
|
# ci_commit_status() / ci_pipeline_number() require: woodpecker_api(), forge_api() (from env.sh)
|
||||||
# classify_pipeline_failure() requires: woodpecker_api() (defined in env.sh)
|
# classify_pipeline_failure() requires: woodpecker_api() (defined in env.sh)
|
||||||
|
|
||||||
# ensure_blocked_label_id — look up (or create) the "blocked" label, print its ID.
|
# ensure_blocked_label_id — look up (or create) the "blocked" label, print its ID.
|
||||||
|
|
@ -102,6 +103,55 @@ ci_failed() {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
# is_infra_step <step_name> <exit_code> [log_data]
|
# is_infra_step <step_name> <exit_code> [log_data]
|
||||||
# Checks whether a single CI step failure matches infra heuristics.
|
# Checks whether a single CI step failure matches infra heuristics.
|
||||||
# Returns 0 (infra) with reason on stdout, or 1 (not infra).
|
# Returns 0 (infra) with reason on stdout, or 1 (not infra).
|
||||||
|
|
|
||||||
|
|
@ -196,8 +196,7 @@ if [ -n "${REVIEW_SESSIONS:-}" ]; then
|
||||||
pr_branch=$(printf '%s' "$pr_json" | jq -r '.head.ref // ""')
|
pr_branch=$(printf '%s' "$pr_json" | jq -r '.head.ref // ""')
|
||||||
if [ -z "$current_sha" ] || [ "$current_sha" = "$reviewed_sha" ]; then continue; fi
|
if [ -z "$current_sha" ] || [ "$current_sha" = "$reviewed_sha" ]; then continue; fi
|
||||||
|
|
||||||
ci_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
ci_state=$(ci_commit_status "$current_sha")
|
||||||
"${API_BASE}/commits/${current_sha}/status" | jq -r '.state // "unknown"')
|
|
||||||
|
|
||||||
if ! ci_passed "$ci_state"; then
|
if ! ci_passed "$ci_state"; then
|
||||||
if ci_required_for_pr "$pr_num"; then
|
if ci_required_for_pr "$pr_num"; then
|
||||||
|
|
@ -227,8 +226,7 @@ while IFS= read -r line; do
|
||||||
PR_SHA=$(echo "$line" | awk '{print $2}')
|
PR_SHA=$(echo "$line" | awk '{print $2}')
|
||||||
PR_BRANCH=$(echo "$line" | awk '{print $3}')
|
PR_BRANCH=$(echo "$line" | awk '{print $3}')
|
||||||
|
|
||||||
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
CI_STATE=$(ci_commit_status "$PR_SHA")
|
||||||
"${API_BASE}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"')
|
|
||||||
|
|
||||||
# Skip if CI is running/failed. Allow "success", no CI configured, or non-code PRs
|
# Skip if CI is running/failed. Allow "success", no CI configured, or non-code PRs
|
||||||
if ! ci_passed "$CI_STATE"; then
|
if ! ci_passed "$CI_STATE"; then
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,7 @@ if [ "$PR_STATE" != "open" ]; then
|
||||||
cd "${PROJECT_REPO_ROOT}"; git worktree remove "$WORKTREE" --force 2>/dev/null || true
|
cd "${PROJECT_REPO_ROOT}"; git worktree remove "$WORKTREE" --force 2>/dev/null || true
|
||||||
rm -rf "$WORKTREE" "$PHASE_FILE" "$OUTPUT_FILE" 2>/dev/null || true; exit 0
|
rm -rf "$WORKTREE" "$PHASE_FILE" "$OUTPUT_FILE" 2>/dev/null || true; exit 0
|
||||||
fi
|
fi
|
||||||
CI_STATE=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
|
CI_STATE=$(ci_commit_status "$PR_SHA")
|
||||||
"${API}/commits/${PR_SHA}/status" | jq -r '.state // "unknown"')
|
|
||||||
CI_NOTE=""; if ! ci_passed "$CI_STATE"; then
|
CI_NOTE=""; if ! ci_passed "$CI_STATE"; then
|
||||||
ci_required_for_pr "$PR_NUMBER" && { log "SKIP: CI=${CI_STATE}"; exit 0; }
|
ci_required_for_pr "$PR_NUMBER" && { log "SKIP: CI=${CI_STATE}"; exit 0; }
|
||||||
CI_NOTE=" (not required — non-code PR)"; fi
|
CI_NOTE=" (not required — non-code PR)"; fi
|
||||||
|
|
|
||||||
|
|
@ -415,7 +415,7 @@ check_project() {
|
||||||
PR_SHA=$(echo "$PR_JSON" | jq -r '.head.sha // ""')
|
PR_SHA=$(echo "$PR_JSON" | jq -r '.head.sha // ""')
|
||||||
[ -z "$PR_SHA" ] && continue
|
[ -z "$PR_SHA" ] && continue
|
||||||
|
|
||||||
CI_STATE=$(forge_api GET "/commits/${PR_SHA}/status" 2>/dev/null | jq -r '.state // "unknown"' 2>/dev/null || true)
|
CI_STATE=$(ci_commit_status "$PR_SHA" 2>/dev/null || true)
|
||||||
|
|
||||||
MERGEABLE=$(echo "$PR_JSON" | jq -r '.mergeable // true')
|
MERGEABLE=$(echo "$PR_JSON" | jq -r '.mergeable // true')
|
||||||
if [ "$MERGEABLE" = "false" ] && ci_passed "$CI_STATE"; then
|
if [ "$MERGEABLE" = "false" ] && ci_passed "$CI_STATE"; then
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue