Skip to content

Commit edafb6f

Browse files
committed
Harden IO error handling and explicit trait contracts
1 parent b0a018f commit edafb6f

12 files changed

Lines changed: 672 additions & 76 deletions

scripts/docs/.sct

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
e802872cc61ff4c166320f579fd405269ce6b32103fc7a81775622e9fea34bca

scripts/docs/scrutinizer_plan.sh

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5+
cd "$ROOT_DIR"
6+
7+
BASE_URL="https://scrutinizer-ci.com/api"
8+
PROVIDER="g"
9+
OWNER=""
10+
REPO=""
11+
INDEX_ID=""
12+
ISSUES_FILE=""
13+
PER_PAGE="100"
14+
OUT_DIR=".tmp_quality/scrutinizer"
15+
TOKEN="${SCRUTINIZER_TOKEN:-}"
16+
17+
usage() {
18+
cat <<'EOF'
19+
Usage:
20+
scripts/docs/scrutinizer_plan.sh --owner <owner> --repo <repo> [options]
21+
scripts/docs/scrutinizer_plan.sh --issues-file <path> [options]
22+
23+
Required:
24+
Mode API:
25+
--owner <owner> Repository owner/login (GitHub provider: user/org)
26+
--repo <repo> Repository name
27+
Mode offline:
28+
--issues-file <path> Existing issues JSON array (skip Scrutinizer API calls)
29+
30+
Options:
31+
--token <token> Scrutinizer access token (or use SCRUTINIZER_TOKEN env)
32+
--provider <g|b|gl|gp> Repository provider type (default: g)
33+
--index <id> Use a specific index id/source reference (skip inspection lookup)
34+
--issues-file <path> Re-generate plan from local JSON issues file
35+
--per-page <n> Pagination size for issues (default: 100, max 100)
36+
--base-url <url> API base URL (default: https://scrutinizer-ci.com/api)
37+
--out-dir <path> Output directory (default: .tmp_quality/scrutinizer)
38+
-h, --help Show this help
39+
40+
Outputs:
41+
<out-dir>/scrutinizer-issues.json
42+
<out-dir>/scrutinizer-plan.md
43+
<out-dir>/scrutinizer-meta.json
44+
45+
Examples:
46+
SCRUTINIZER_TOKEN=xxx scripts/docs/scrutinizer_plan.sh --owner psfs --repo core
47+
scripts/docs/scrutinizer_plan.sh --owner psfs --repo core --token xxx --index 12345
48+
scripts/docs/scrutinizer_plan.sh --issues-file .tmp_quality/scrutinizer/scrutinizer-issues.json
49+
EOF
50+
}
51+
52+
require_cmd() {
53+
if ! command -v "$1" >/dev/null 2>&1; then
54+
echo "[SCRUTINIZER][ERROR] Missing required command: $1" >&2
55+
exit 1
56+
fi
57+
}
58+
59+
while [[ $# -gt 0 ]]; do
60+
case "$1" in
61+
--owner)
62+
OWNER="${2:-}"; shift 2 ;;
63+
--repo)
64+
REPO="${2:-}"; shift 2 ;;
65+
--token)
66+
TOKEN="${2:-}"; shift 2 ;;
67+
--provider)
68+
PROVIDER="${2:-}"; shift 2 ;;
69+
--index)
70+
INDEX_ID="${2:-}"; shift 2 ;;
71+
--issues-file)
72+
ISSUES_FILE="${2:-}"; shift 2 ;;
73+
--per-page)
74+
PER_PAGE="${2:-}"; shift 2 ;;
75+
--base-url)
76+
BASE_URL="${2:-}"; shift 2 ;;
77+
--out-dir)
78+
OUT_DIR="${2:-}"; shift 2 ;;
79+
-h|--help)
80+
usage; exit 0 ;;
81+
*)
82+
echo "[SCRUTINIZER][ERROR] Unknown argument: $1" >&2
83+
usage
84+
exit 1 ;;
85+
esac
86+
done
87+
88+
require_cmd jq
89+
if [[ -z "$ISSUES_FILE" ]]; then
90+
require_cmd curl
91+
if [[ -z "$OWNER" || -z "$REPO" ]]; then
92+
echo "[SCRUTINIZER][ERROR] --owner and --repo are required in API mode." >&2
93+
usage
94+
exit 1
95+
fi
96+
if [[ -z "$TOKEN" ]]; then
97+
echo "[SCRUTINIZER][ERROR] Missing token. Use --token or SCRUTINIZER_TOKEN env." >&2
98+
exit 1
99+
fi
100+
fi
101+
102+
if [[ "$PER_PAGE" =~ ^[0-9]+$ ]]; then
103+
if (( PER_PAGE < 1 || PER_PAGE > 100 )); then
104+
echo "[SCRUTINIZER][ERROR] --per-page must be between 1 and 100." >&2
105+
exit 1
106+
fi
107+
else
108+
echo "[SCRUTINIZER][ERROR] --per-page must be numeric." >&2
109+
exit 1
110+
fi
111+
112+
case "$PROVIDER" in
113+
g|b|gl|gp) ;;
114+
*)
115+
echo "[SCRUTINIZER][ERROR] --provider must be one of: g, b, gl, gp." >&2
116+
exit 1 ;;
117+
esac
118+
119+
mkdir -p "$OUT_DIR"
120+
TMP_DIR="$(mktemp -d)"
121+
trap 'rm -rf "$TMP_DIR"' EXIT
122+
123+
api_get() {
124+
local path="$1"
125+
local url="${BASE_URL%/}/${path}"
126+
local body_file
127+
local status
128+
body_file="$(mktemp "${TMP_DIR}/api-body.XXXXXX")" || {
129+
echo "[SCRUTINIZER][ERROR] Unable to create temporary file in ${TMP_DIR}" >&2
130+
exit 1
131+
}
132+
if [[ "$url" == *\?* ]]; then
133+
url="${url}&access_token=${TOKEN}"
134+
else
135+
url="${url}?access_token=${TOKEN}"
136+
fi
137+
138+
status="$(curl -sS -o "$body_file" -w '%{http_code}' -H "Accept: application/json" "$url" || true)"
139+
if [[ -z "$status" || "$status" -lt 200 || "$status" -ge 300 ]]; then
140+
echo "[SCRUTINIZER][ERROR] API request failed: ${url}" >&2
141+
echo "[SCRUTINIZER][ERROR] HTTP status: ${status:-unknown}" >&2
142+
if [[ -s "$body_file" ]]; then
143+
echo "[SCRUTINIZER][ERROR] Response body:" >&2
144+
cat "$body_file" >&2
145+
echo >&2
146+
fi
147+
echo "[SCRUTINIZER][HINT] Revisa token, provider (--provider), owner/repo o permisos del repo en Scrutinizer." >&2
148+
exit 1
149+
fi
150+
cat "$body_file"
151+
}
152+
153+
INSPECTION_ID=""
154+
pages=0
155+
total_issues=0
156+
157+
if [[ -n "$ISSUES_FILE" ]]; then
158+
if [[ ! -f "$ISSUES_FILE" ]]; then
159+
echo "[SCRUTINIZER][ERROR] --issues-file not found: $ISSUES_FILE" >&2
160+
exit 1
161+
fi
162+
jq -e 'type == "array"' "$ISSUES_FILE" >/dev/null
163+
issues_abs="$(cd "$(dirname "$ISSUES_FILE")" && pwd)/$(basename "$ISSUES_FILE")"
164+
out_issues_abs="$(cd "$OUT_DIR" && pwd)/scrutinizer-issues.json"
165+
if [[ "$issues_abs" != "$out_issues_abs" ]]; then
166+
cp "$ISSUES_FILE" "${OUT_DIR}/scrutinizer-issues.json"
167+
fi
168+
total_issues="$(jq 'length' "${OUT_DIR}/scrutinizer-issues.json")"
169+
INDEX_ID="${INDEX_ID:-offline}"
170+
OWNER="${OWNER:-unknown}"
171+
REPO="${REPO:-unknown}"
172+
echo "[SCRUTINIZER] Offline mode: using issues from ${ISSUES_FILE}"
173+
else
174+
REPO_PATH="repositories/${PROVIDER}/${OWNER}/${REPO}"
175+
if [[ -z "$INDEX_ID" ]]; then
176+
echo "[SCRUTINIZER] Resolviendo última inspección para ${PROVIDER}/${OWNER}/${REPO}..."
177+
INSPECTIONS_JSON="$(api_get "${REPO_PATH}/inspections?per_page=1")"
178+
printf '%s' "$INSPECTIONS_JSON" > "${TMP_DIR}/inspections.json"
179+
180+
INSPECTION_ID="$(jq -r '._embedded.inspections[0].uuid // ._embedded.inspections[0].id // empty' "${TMP_DIR}/inspections.json")"
181+
if [[ -z "$INSPECTION_ID" ]]; then
182+
echo "[SCRUTINIZER][ERROR] No se encontró inspección reciente. Verifica repo/token." >&2
183+
exit 1
184+
fi
185+
186+
INSPECTION_JSON="$(api_get "${REPO_PATH}/inspections/${INSPECTION_ID}")"
187+
printf '%s' "$INSPECTION_JSON" > "${TMP_DIR}/inspection.json"
188+
INDEX_ID="$(jq -r '.head_index.id // .head_index // ._embedded.head_index.id // ._embedded.indices[0].id // .index.id // empty' "${TMP_DIR}/inspection.json")"
189+
190+
if [[ -z "$INDEX_ID" ]]; then
191+
echo "[SCRUTINIZER][ERROR] No se pudo resolver head_index en inspección ${INSPECTION_ID}. Usa --index." >&2
192+
exit 1
193+
fi
194+
fi
195+
196+
echo "[SCRUTINIZER] Descargando issues del índice: ${INDEX_ID}"
197+
page=1
198+
while :; do
199+
PAGE_JSON="${TMP_DIR}/issues-page-${page}.json"
200+
api_get "${REPO_PATH}/indices/${INDEX_ID}/issues?per_page=${PER_PAGE}&page=${page}" > "$PAGE_JSON"
201+
202+
count="$(jq '._embedded.issues | length' "$PAGE_JSON")"
203+
if [[ "$count" == "0" ]]; then
204+
break
205+
fi
206+
207+
pages=$((pages + 1))
208+
total_issues=$((total_issues + count))
209+
page=$((page + 1))
210+
done
211+
212+
if (( pages == 0 )); then
213+
echo "[]" > "${OUT_DIR}/scrutinizer-issues.json"
214+
else
215+
jq -s '[ .[] | ._embedded.issues[] ]' "${TMP_DIR}"/issues-page-*.json > "${OUT_DIR}/scrutinizer-issues.json"
216+
fi
217+
fi
218+
219+
cat > "${OUT_DIR}/scrutinizer-meta.json" <<EOF
220+
{
221+
"generated_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
222+
"base_url": "${BASE_URL}",
223+
"provider": "${PROVIDER}",
224+
"owner": "${OWNER}",
225+
"repo": "${REPO}",
226+
"inspection_id": "${INSPECTION_ID}",
227+
"index_id": "${INDEX_ID}",
228+
"pages": ${pages},
229+
"issues": ${total_issues}
230+
}
231+
EOF
232+
233+
echo "[SCRUTINIZER] Generando plan priorizado..."
234+
235+
jq -r '
236+
def sev_num: (if type == "number" then . else (tonumber? // -1) end);
237+
def priority:
238+
if (sev_num) == 10 then "P0"
239+
elif (sev_num) == 5 then "P1"
240+
elif (sev_num) == 0 then "P2"
241+
else "PX" end;
242+
def weight:
243+
if (sev_num) == 10 then 0
244+
elif (sev_num) == 5 then 1
245+
elif (sev_num) == 0 then 2
246+
else 3 end;
247+
248+
. as $issues
249+
| [
250+
"# Scrutinizer Action Plan",
251+
"",
252+
"Generated at: " + (now | strftime("%Y-%m-%d %H:%M:%S UTC")),
253+
"",
254+
"## Summary",
255+
"",
256+
"- Total issues: " + (($issues | length) | tostring),
257+
"- P0 (severity 10): " + (($issues | map(select((.severity|sev_num) == 10)) | length) | tostring),
258+
"- P1 (severity 5): " + (($issues | map(select((.severity|sev_num) == 5)) | length) | tostring),
259+
"- P2 (severity 0): " + (($issues | map(select((.severity|sev_num) == 0)) | length) | tostring),
260+
"- Other severities: " + (($issues | map(select(((.severity|sev_num) != 10) and ((.severity|sev_num) != 5) and ((.severity|sev_num) != 0))) | length) | tostring),
261+
"",
262+
"## Top Hotspots (by issue count)",
263+
""
264+
]
265+
+ (
266+
$issues
267+
| group_by((.path // "unknown") | tostring)
268+
| map({path: ((.[0].path // "unknown") | tostring), count: length})
269+
| sort_by(-.count)
270+
| .[:15]
271+
| if length == 0 then ["- No hotspots (empty issue set)."]
272+
else map("- `" + .path + "`: " + (.count|tostring) + " issues")
273+
end
274+
)
275+
+ [
276+
"",
277+
"## Prioritized Backlog",
278+
""
279+
]
280+
+ (
281+
$issues
282+
| sort_by((.severity // "unknown" | weight), ((.path // "unknown")|tostring), ((.line // 0)|tonumber? // 0))
283+
| .[:200]
284+
| if length == 0 then ["- No issues found."]
285+
else map(
286+
"- [" + ((.severity|sev_num|priority)) + "|S" + (((.severity // -1)|sev_num)|tostring) + "] "
287+
+ "`" + ((.path // "unknown") | tostring) + ":" + (((.line // 0)|tonumber? // 0)|tostring) + "` "
288+
+ ((.message // .message_id // "No message") | tostring)
289+
+ (
290+
if (.labels // [] | length) > 0 then
291+
" _(labels: " + ((.labels | map(tostring) | join(", "))) + ")_"
292+
else "" end
293+
)
294+
)
295+
end
296+
)
297+
+ [
298+
"",
299+
"## Suggested Execution Order",
300+
"",
301+
"1. Fix all `P0 (severity 10)` issues first.",
302+
"2. Tackle files with highest hotspot counts to reduce repeated defects.",
303+
"3. Address `P1 (severity 5)` issues in changed modules.",
304+
"4. Batch `P2 (severity 0)` items into cleanup/refactor passes.",
305+
"",
306+
"## Notes",
307+
"",
308+
"- This plan is auto-generated from Scrutinizer issues endpoint.",
309+
"- Re-run after fixes to compare trend and close loop."
310+
]
311+
| .[]
312+
' "${OUT_DIR}/scrutinizer-issues.json" > "${OUT_DIR}/scrutinizer-plan.md"
313+
314+
echo "[SCRUTINIZER] Done."
315+
echo " - Issues JSON: ${OUT_DIR}/scrutinizer-issues.json"
316+
echo " - Plan MD: ${OUT_DIR}/scrutinizer-plan.md"
317+
echo " - Meta JSON: ${OUT_DIR}/scrutinizer-meta.json"

0 commit comments

Comments
 (0)