Compare commits
1 Commits
fix/t_a292
...
fd874649e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd874649e8 |
@@ -653,9 +653,3 @@ Independent security review surfaced three high-impact and four medium findings;
|
||||
2. After merge, kickoff M3-01 (DTOs) on `feat/federation-m3-types` with sonnet subagent in worktree
|
||||
3. Once M3-01 lands, fan out: M3-02 (harness) || M3-03 (AuthGuard) → M3-04 (ScopeService) || M3-08 (FederationClient)
|
||||
4. Re-converge at M3-10 (Integration) → M3-11 (E2E)
|
||||
|
||||
### Session 24 — 2026-05-22 — Gitea PR metadata wrapper hardening
|
||||
|
||||
- Fixed `packages/mosaic/framework/tools/git/pr-metadata.sh` Gitea path to fail closed on non-2xx API responses instead of normalizing API error JSON into null/empty PR metadata.
|
||||
- Added token fallback behavior: try explicit `GITEA_TOKEN`, Mosaic credential-loader token, then matching `~/.git-credentials` HTTPS token; this handles stale host-scoped credential-loader entries while preserving existing credential sources.
|
||||
- Confirmed real U-Connect Gitea PRs #1905 and #1908 now return `number`, `headRefName`, `baseRefName`, `state`, `author`, `url`, and `mergeable` correctly, restoring `pr-merge.sh` base branch detection.
|
||||
|
||||
@@ -52,6 +52,20 @@ _mosaic_sync_woodpecker_env() {
|
||||
printf '%s\n' "$expected" > "$env_file"
|
||||
}
|
||||
|
||||
# Load legacy flat Woodpecker credentials (.woodpecker.url / .woodpecker.token).
|
||||
# Some environments export WOODPECKER_INSTANCE=mosaic, but the current
|
||||
# credentials.json may still use the legacy flat schema. Treat "mosaic" as the
|
||||
# default flat instance when a nested .woodpecker.mosaic object is absent.
|
||||
_mosaic_load_woodpecker_legacy() {
|
||||
export WOODPECKER_URL="$(_mosaic_read_cred '.woodpecker.url')"
|
||||
export WOODPECKER_TOKEN="$(_mosaic_read_cred '.woodpecker.token')"
|
||||
export WOODPECKER_INSTANCE="${WOODPECKER_INSTANCE:-mosaic}"
|
||||
WOODPECKER_URL="${WOODPECKER_URL%/}"
|
||||
[[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.url not found" >&2; return 1; }
|
||||
[[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.token not found" >&2; return 1; }
|
||||
_mosaic_sync_woodpecker_env "$WOODPECKER_INSTANCE" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
|
||||
}
|
||||
|
||||
load_credentials() {
|
||||
local service="$1"
|
||||
|
||||
@@ -155,7 +169,14 @@ EOF
|
||||
;;
|
||||
woodpecker-*)
|
||||
local wp_instance="${service#woodpecker-}"
|
||||
# credentials.json is authoritative — always read from it, ignore env
|
||||
# credentials.json is authoritative — always read from it, ignore env.
|
||||
# Backward compatibility: the default Mosaic Woodpecker instance may be
|
||||
# stored in the legacy flat schema (.woodpecker.url/.token) instead of
|
||||
# .woodpecker.mosaic.url/.token.
|
||||
if [[ "$wp_instance" == "mosaic" ]] && [[ -z "$(_mosaic_read_cred '.woodpecker.mosaic.url')" ]] && [[ -n "$(_mosaic_read_cred '.woodpecker.url')" ]]; then
|
||||
WOODPECKER_INSTANCE="mosaic" _mosaic_load_woodpecker_legacy
|
||||
return $?
|
||||
fi
|
||||
export WOODPECKER_URL="$(_mosaic_read_cred ".woodpecker.${wp_instance}.url")"
|
||||
export WOODPECKER_TOKEN="$(_mosaic_read_cred ".woodpecker.${wp_instance}.token")"
|
||||
export WOODPECKER_INSTANCE="$wp_instance"
|
||||
@@ -166,7 +187,10 @@ EOF
|
||||
_mosaic_sync_woodpecker_env "$wp_instance" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
|
||||
;;
|
||||
woodpecker)
|
||||
# Resolve default instance, then load it
|
||||
# Resolve default instance, then load it. If WOODPECKER_INSTANCE is set to
|
||||
# "mosaic" by a shell/profile but credentials.json still uses the legacy
|
||||
# flat .woodpecker.url/.token schema, load the flat credentials instead of
|
||||
# failing with "woodpecker.mosaic.url not found".
|
||||
local wp_default
|
||||
wp_default="${WOODPECKER_INSTANCE:-$(_mosaic_read_cred '.woodpecker.default')}"
|
||||
if [[ -z "$wp_default" ]]; then
|
||||
@@ -174,18 +198,18 @@ EOF
|
||||
local legacy_url
|
||||
legacy_url="$(_mosaic_read_cred '.woodpecker.url')"
|
||||
if [[ -n "$legacy_url" ]]; then
|
||||
export WOODPECKER_URL="${WOODPECKER_URL:-$legacy_url}"
|
||||
export WOODPECKER_TOKEN="${WOODPECKER_TOKEN:-$(_mosaic_read_cred '.woodpecker.token')}"
|
||||
WOODPECKER_URL="${WOODPECKER_URL%/}"
|
||||
[[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.url not found" >&2; return 1; }
|
||||
[[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.token not found" >&2; return 1; }
|
||||
_mosaic_load_woodpecker_legacy
|
||||
else
|
||||
echo "Error: woodpecker.default not set and no WOODPECKER_INSTANCE env var" >&2
|
||||
echo "Available instances: $(jq -r '.woodpecker | keys | join(", ")' "$MOSAIC_CREDENTIALS_FILE" 2>/dev/null)" >&2
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
load_credentials "woodpecker-${wp_default}"
|
||||
if [[ "$wp_default" == "mosaic" ]] && [[ -z "$(_mosaic_read_cred '.woodpecker.mosaic.url')" ]] && [[ -n "$(_mosaic_read_cred '.woodpecker.url')" ]]; then
|
||||
WOODPECKER_INSTANCE="mosaic" _mosaic_load_woodpecker_legacy
|
||||
else
|
||||
load_credentials "woodpecker-${wp_default}"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
cloudflare-*)
|
||||
|
||||
@@ -69,81 +69,32 @@ elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
|
||||
API_URL="https://${HOST}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}"
|
||||
|
||||
declare -a TOKEN_CANDIDATES=()
|
||||
add_token_candidate() {
|
||||
local candidate="$1"
|
||||
[[ -z "$candidate" ]] && return 0
|
||||
local existing
|
||||
for existing in "${TOKEN_CANDIDATES[@]:-}"; do
|
||||
[[ "$existing" == "$candidate" ]] && return 0
|
||||
done
|
||||
TOKEN_CANDIDATES+=("$candidate")
|
||||
}
|
||||
GITEA_API_TOKEN=$(get_gitea_token "$HOST" || true)
|
||||
|
||||
add_token_candidate "${GITEA_TOKEN:-}"
|
||||
add_token_candidate "$(get_gitea_token "$HOST" || true)"
|
||||
|
||||
# Git HTTPS credentials often contain a valid Gitea API token even when a
|
||||
# Mosaic credential-source entry is stale. Try them as a fallback before
|
||||
# falling back to an unauthenticated request.
|
||||
CREDS_FILE="$HOME/.git-credentials"
|
||||
if [[ -f "$CREDS_FILE" ]]; then
|
||||
while IFS= read -r credential_token; do
|
||||
add_token_candidate "$credential_token"
|
||||
done < <(grep -F "$HOST" "$CREDS_FILE" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p')
|
||||
fi
|
||||
|
||||
RAW=""
|
||||
HTTP_STATUS=""
|
||||
if [[ ${#TOKEN_CANDIDATES[@]} -gt 0 ]]; then
|
||||
for GITEA_API_TOKEN in "${TOKEN_CANDIDATES[@]}"; do
|
||||
RESPONSE_FILE=$(mktemp)
|
||||
HTTP_STATUS=$(curl -sS -o "$RESPONSE_FILE" -w '%{http_code}' -H "Authorization: token $GITEA_API_TOKEN" "$API_URL" || true)
|
||||
RAW=$(cat "$RESPONSE_FILE")
|
||||
rm -f "$RESPONSE_FILE"
|
||||
[[ "$HTTP_STATUS" =~ ^2 ]] && break
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ! "$HTTP_STATUS" =~ ^2 ]]; then
|
||||
RESPONSE_FILE=$(mktemp)
|
||||
HTTP_STATUS=$(curl -sS -o "$RESPONSE_FILE" -w '%{http_code}' "$API_URL" || true)
|
||||
RAW=$(cat "$RESPONSE_FILE")
|
||||
rm -f "$RESPONSE_FILE"
|
||||
fi
|
||||
|
||||
if [[ ! "$HTTP_STATUS" =~ ^2 ]]; then
|
||||
ERROR_MESSAGE=$(printf '%s' "$RAW" | python3 -c 'import json, sys
|
||||
try:
|
||||
data=json.load(sys.stdin)
|
||||
except Exception:
|
||||
data={}
|
||||
print(data.get("message") or data.get("error") or "unknown error")')
|
||||
echo "Error: failed to fetch Gitea PR #$PR_NUMBER from $HOST/$OWNER/$REPO (HTTP $HTTP_STATUS): $ERROR_MESSAGE" >&2
|
||||
exit 1
|
||||
if [[ -n "$GITEA_API_TOKEN" ]]; then
|
||||
RAW=$(curl -sS -H "Authorization: token $GITEA_API_TOKEN" "$API_URL")
|
||||
else
|
||||
RAW=$(curl -sS "$API_URL")
|
||||
fi
|
||||
|
||||
# Normalize Gitea response to match our expected schema
|
||||
METADATA=$(echo "$RAW" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
head = data.get('head') or {}
|
||||
base = data.get('base') or {}
|
||||
user = data.get('user') or {}
|
||||
normalized = {
|
||||
'number': data.get('number') or data.get('index'),
|
||||
'number': data.get('number'),
|
||||
'title': data.get('title'),
|
||||
'body': data.get('body', ''),
|
||||
'state': data.get('state'),
|
||||
'author': user.get('login', ''),
|
||||
'headRefName': head.get('ref') or head.get('label', '').split(':')[-1],
|
||||
'baseRefName': base.get('ref') or base.get('label', '').split(':')[-1],
|
||||
'author': data.get('user', {}).get('login', ''),
|
||||
'headRefName': data.get('head', {}).get('ref', ''),
|
||||
'baseRefName': data.get('base', {}).get('ref', ''),
|
||||
'labels': [l.get('name', '') for l in data.get('labels', [])],
|
||||
'assignees': [a.get('login', '') for a in data.get('assignees', [])],
|
||||
'milestone': data.get('milestone', {}).get('title', '') if data.get('milestone') else '',
|
||||
'createdAt': data.get('created_at', ''),
|
||||
'updatedAt': data.get('updated_at', ''),
|
||||
'url': data.get('html_url') or data.get('url', ''),
|
||||
'url': data.get('html_url', ''),
|
||||
'isDraft': data.get('draft', False),
|
||||
'mergeable': data.get('mergeable'),
|
||||
'diffUrl': data.get('diff_url', ''),
|
||||
|
||||
@@ -50,7 +50,7 @@ REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
|
||||
|
||||
response=$(curl -sk -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||
"${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines?per_page=${LIMIT}")
|
||||
"${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines?perPage=${LIMIT}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
Reference in New Issue
Block a user