@cxi-lmai/ci-agent-platform 3.0.1 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/package.json +2 -2
- package/payload/INSTALL.md +8 -2
- package/payload/agents/agent-architect.md +1 -1
- package/payload/agents/code-reviewer.md +1 -1
- package/payload/agents/codebase-auditor.md +1 -1
- package/payload/agents/coder.md +1 -1
- package/payload/agents/decomposer.md +1 -1
- package/payload/agents/docs-sync.md +1 -1
- package/payload/agents/e2e-test-writer.md +1 -1
- package/payload/agents/performance-reviewer.md +1 -1
- package/payload/agents/release-mr.md +1 -1
- package/payload/agents/security-reviewer.md +1 -1
- package/payload/agents/test-fix.md +1 -1
- package/payload/agents/test-writer.md +1 -1
- package/payload/agents-omp/agent-architect.md +1 -1
- package/payload/agents-omp/code-reviewer.md +1 -1
- package/payload/agents-omp/codebase-auditor.md +1 -1
- package/payload/agents-omp/coder.md +1 -1
- package/payload/agents-omp/decomposer.md +1 -1
- package/payload/agents-omp/docs-sync.md +1 -1
- package/payload/agents-omp/e2e-test-writer.md +1 -1
- package/payload/agents-omp/migration-reviewer.md +1 -1
- package/payload/agents-omp/orchestrator.md +1 -1
- package/payload/agents-omp/performance-reviewer.md +1 -1
- package/payload/agents-omp/postmortem.md +1 -1
- package/payload/agents-omp/release-mr.md +1 -1
- package/payload/agents-omp/security-reviewer.md +1 -1
- package/payload/agents-omp/test-fix.md +1 -1
- package/payload/agents-omp/test-writer.md +1 -1
- package/payload/ci-templates/claude-pipeline.gitlab-ci.yml +210 -0
- package/payload/ci-templates/github/claude-issue-pipeline.yml +1 -1
- package/payload/ci-templates/github/claude-pipeline.yml +2 -2
- package/payload/ci-templates/github/claude-test-fix.yml +1 -1
- package/payload/ci-templates/scripts/agent-architect.sh +233 -0
- package/payload/ci-templates/scripts/code.sh +26 -27
- package/payload/ci-templates/scripts/codebase-audit.sh +252 -0
- package/payload/ci-templates/scripts/coverage-ratchet.sh +77 -0
- package/payload/ci-templates/scripts/cve-fix.sh +246 -0
- package/payload/ci-templates/scripts/docs-sync.sh +225 -0
- package/payload/ci-templates/scripts/e2e-test-gen.sh +201 -0
- package/payload/ci-templates/scripts/lib/failure-notice.sh +104 -0
- package/payload/ci-templates/scripts/lib/issue-loop.sh +155 -40
- package/payload/ci-templates/scripts/lib/pipeline-common.sh +100 -26
- package/payload/ci-templates/scripts/lib/platform.sh +270 -13
- package/payload/ci-templates/scripts/lib/usage-capture-omp.sh +7 -1
- package/payload/ci-templates/scripts/lib/usage-capture.sh +7 -1
- package/payload/ci-templates/scripts/metrics-snapshot.sh +377 -0
- package/payload/ci-templates/scripts/orchestrate.sh +55 -37
- package/payload/ci-templates/scripts/postmortem.sh +10 -0
- package/payload/ci-templates/scripts/review-fix.sh +22 -11
- package/payload/ci-templates/scripts/review.sh +11 -7
- package/payload/ci-templates/scripts/test-fix.sh +7 -0
- package/payload/skills/agent-architect/SKILL.md +45 -0
- package/payload/skills/codebase-audit/SKILL.md +84 -0
- package/payload/skills/cve-fix/SKILL.md +98 -0
- package/payload/skills/docs-sync/SKILL.md +74 -0
- package/payload/skills/e2e-test-gen/SKILL.md +65 -0
- package/payload/skills/fix-review-findings/SKILL.md +2 -2
- package/payload/skills/fix-tests/SKILL.md +2 -2
- package/payload/templates/memory-index.template.md +34 -0
- package/payload/templates/pipeline-config.template.md +11 -1
- package/payload/templates/review_suppressions.template.md +55 -0
- package/payload/templates/spec-issue.template.md +39 -7
|
@@ -13,6 +13,8 @@ source "$SCRIPT_LIB_DIR/usage-capture.sh"
|
|
|
13
13
|
source "$SCRIPT_LIB_DIR/usage-capture-omp.sh"
|
|
14
14
|
# shellcheck source=./platform.sh
|
|
15
15
|
source "$SCRIPT_LIB_DIR/platform.sh"
|
|
16
|
+
# shellcheck source=./failure-notice.sh
|
|
17
|
+
source "$SCRIPT_LIB_DIR/failure-notice.sh"
|
|
16
18
|
|
|
17
19
|
# --- Defaults ---------------------------------------------------------------
|
|
18
20
|
# Every PIPE_ variable has a default here so a project that overrides nothing
|
|
@@ -25,6 +27,7 @@ pipe_defaults() {
|
|
|
25
27
|
: "${PIPE_CONFIG_PATH:=.claude/pipeline-config.md}"
|
|
26
28
|
: "${PIPE_CONTEXT_DIR:=build/pipeline}"
|
|
27
29
|
: "${PIPE_METRICS_DIR:=$PIPE_CONTEXT_DIR/metrics}"
|
|
30
|
+
: "${PIPE_AGENT_STDERR:=$PIPE_CONTEXT_DIR/agent-stderr.log}"
|
|
28
31
|
: "${PIPE_TARGET_BRANCH:=main}"
|
|
29
32
|
: "${PIPE_FIX_LOOP_CAP:=2}"
|
|
30
33
|
: "${PIPE_CODER_CAP:=3}"
|
|
@@ -33,29 +36,78 @@ pipe_defaults() {
|
|
|
33
36
|
# Model defaults track the active harness. Each PIPE_MODEL_* variable remains
|
|
34
37
|
# explicitly overridable.
|
|
35
38
|
if [ "$PIPE_HARNESS" = "omp" ]; then
|
|
36
|
-
: "${PIPE_MODEL_TRIAGE:=openrouter/anthropic/claude-haiku-4
|
|
37
|
-
: "${PIPE_MODEL_CODE:=openrouter/anthropic/claude-sonnet-5
|
|
38
|
-
: "${PIPE_MODEL_REVIEW:=openrouter/anthropic/claude-sonnet-5
|
|
39
|
+
: "${PIPE_MODEL_TRIAGE:=openrouter/anthropic/claude-haiku-4.5}"
|
|
40
|
+
: "${PIPE_MODEL_CODE:=openrouter/anthropic/claude-sonnet-5}"
|
|
41
|
+
: "${PIPE_MODEL_REVIEW:=openrouter/anthropic/claude-sonnet-5}"
|
|
39
42
|
else
|
|
40
43
|
: "${PIPE_MODEL_TRIAGE:=haiku}"
|
|
41
|
-
: "${PIPE_MODEL_CODE:=claude-sonnet-5
|
|
42
|
-
: "${PIPE_MODEL_REVIEW:=claude-sonnet-5
|
|
44
|
+
: "${PIPE_MODEL_CODE:=claude-sonnet-5}"
|
|
45
|
+
: "${PIPE_MODEL_REVIEW:=claude-sonnet-5}"
|
|
43
46
|
fi
|
|
44
47
|
: "${PIPE_COMMIT_TESTFIX:=Fix test errors}"
|
|
45
48
|
: "${PIPE_COMMIT_REVIEWFIX:=Fix review findings}"
|
|
46
49
|
: "${PIPE_COMMIT_COVERAGE:=Add coverage tests}"
|
|
50
|
+
: "${PIPE_COMMIT_DOCSSYNC:=Update docs per docs-sync findings}"
|
|
51
|
+
: "${PIPE_COMMIT_AUDIT:=docs: apply convention audit findings}"
|
|
52
|
+
: "${PIPE_COMMIT_CVEFIX:=Update dependencies to resolve CVEs}"
|
|
47
53
|
: "${PIPE_LABEL_READY:=pipe-ready}"
|
|
48
54
|
: "${PIPE_LABEL_WIP:=pipe-wip}"
|
|
49
55
|
: "${PIPE_LABEL_STUCK:=pipe-stuck}"
|
|
50
56
|
: "${PIPE_LABEL_BLOCKER:=pipe-blocker}"
|
|
51
57
|
: "${PIPE_LABEL_BLOCKED:=pipe-blocked}"
|
|
52
58
|
: "${PIPE_LABEL_DECOMPOSED:=pipe-decomposed}"
|
|
59
|
+
: "${PIPE_LABEL_AUDIT:=codebase-audit}"
|
|
53
60
|
: "${PIPE_RESULT_REVIEW:=$PIPE_CONTEXT_DIR/code-review-result.txt}"
|
|
54
61
|
: "${PIPE_RESULT_REVIEWFIX:=$PIPE_CONTEXT_DIR/review-fix-result.json}"
|
|
62
|
+
: "${PIPE_RESULT_DOCSSYNC:=$PIPE_CONTEXT_DIR/docs-sync.json}"
|
|
55
63
|
: "${PIPE_VERIFY_CMD:=}"
|
|
56
64
|
: "${PIPE_COMPILE_CMD:=${PIPE_VERIFY_CMD}}"
|
|
57
65
|
: "${PIPE_TEST_REPORT_GLOB:=}"
|
|
66
|
+
: "${PIPE_COVERAGE_RATCHET:=0}"
|
|
67
|
+
: "${PIPE_COVERAGE_REPORT:=}"
|
|
68
|
+
: "${PIPE_COVERAGE_REPORT_KIND:=jacoco}"
|
|
58
69
|
: "${PIPE_COVERAGE_SIGNAL:=}"
|
|
70
|
+
# CVE remediation. Off by default: project-local scanners provide the
|
|
71
|
+
# HIGH/CRITICAL identifier list when PIPE_CVE_FIX=1 enables this job.
|
|
72
|
+
: "${PIPE_CVE_FIX:=0}"
|
|
73
|
+
: "${PIPE_DEPENDENCY_MANIFEST:=build.gradle}"
|
|
74
|
+
: "${PIPE_CVE_SUPPRESSIONS:=owasp-suppressions.xml}"
|
|
75
|
+
# Issue-driven E2E generation. Off by default; PIPE_VERIFY_CMD is the
|
|
76
|
+
# configured verification command and must be non-empty before tests run.
|
|
77
|
+
: "${PIPE_E2E_TEST_GEN:=0}"
|
|
78
|
+
: "${PIPE_LABEL_TESTREADY:=test-ready}"
|
|
79
|
+
: "${PIPE_LABEL_E2E_SCOPE:=}"
|
|
80
|
+
: "${PIPE_E2E_TEST_DIR:=e2e/tests}"
|
|
81
|
+
: "${PIPE_E2E_BASE_URL:=}"
|
|
82
|
+
: "${PIPE_E2E_VERIFY_CMD:=}"
|
|
83
|
+
# Documentation sync. Off by default: the job only runs when the pipeline
|
|
84
|
+
# sets PIPE_DOCS_SYNC=1. PIPE_DOCS_ROOTS is the comma-separated set of paths
|
|
85
|
+
# the docs-sync agent may edit and the runner may commit.
|
|
86
|
+
: "${PIPE_DOCS_SYNC:=0}"
|
|
87
|
+
: "${PIPE_DOCS_ROOTS:=docs,CLAUDE.md,.claude/memory}"
|
|
88
|
+
# Codebase audit. Off by default: the job only runs on a schedule that sets
|
|
89
|
+
# PIPE_CODEBASE_AUDIT=1. Reuses PIPE_DOCS_ROOTS for the optional auto-fix
|
|
90
|
+
# phase's coder scope.
|
|
91
|
+
: "${PIPE_CODEBASE_AUDIT:=0}"
|
|
92
|
+
# Agent architect. Off by default: a scheduled pipeline explicitly enables
|
|
93
|
+
# this evidence-backed proposal job after a project has reviewed its output.
|
|
94
|
+
: "${PIPE_AGENT_ARCHITECT:=0}"
|
|
95
|
+
: "${PIPE_ARCHITECT_WINDOW_DAYS:=7}"
|
|
96
|
+
: "${PIPE_ARCHITECT_DRY_RUN:=true}"
|
|
97
|
+
: "${PIPE_LABEL_IMPROVEMENT:=pipe-improvement}"
|
|
98
|
+
# Metrics aggregation. Off by default: the job only runs on a schedule that
|
|
99
|
+
# sets PIPE_METRICS_SNAPSHOT=1, and it publishes an artifact unless the
|
|
100
|
+
# project explicitly opts in to committing generated data to its repository.
|
|
101
|
+
: "${PIPE_METRICS_SNAPSHOT:=0}"
|
|
102
|
+
: "${PIPE_METRICS_OUTPUT_DIR:=data/metrics/snapshots}"
|
|
103
|
+
: "${PIPE_METRICS_WINDOW_DAYS:=1}"
|
|
104
|
+
: "${PIPE_METRICS_COMMIT_REPO:=false}"
|
|
105
|
+
: "${PIPE_METRICS_REVIEW_AGENTS:=code-reviewer,review-verdict,review-fix}"
|
|
106
|
+
# Commit subjects that mark a defect in already-merged work. Keep in sync with
|
|
107
|
+
# the Metrics Conventions section of the pipeline config.
|
|
108
|
+
: "${PIPE_METRICS_DEFECT_REVERT:=Reverts !}"
|
|
109
|
+
: "${PIPE_METRICS_DEFECT_REGRESSION:=Fixes regression from !}"
|
|
110
|
+
: "${PIPE_METRICS_DEFECT_REINTRODUCE:=Reintroduces work from !}"
|
|
59
111
|
# Spec template path. GitHub projects usually keep it under .github, GitLab
|
|
60
112
|
# under .gitlab, so the default follows the detected platform.
|
|
61
113
|
if [ "$PIPE_PLATFORM" = "github" ]; then
|
|
@@ -69,12 +121,20 @@ pipe_defaults() {
|
|
|
69
121
|
# would pick up result files (E2E finding N-6). A `*` gitignore inside the
|
|
70
122
|
# dir hides everything in it, including itself, from any git add.
|
|
71
123
|
[ -f "$PIPE_CONTEXT_DIR/.gitignore" ] || printf '*\n' > "$PIPE_CONTEXT_DIR/.gitignore"
|
|
72
|
-
export PIPE_PLATFORM PIPE_CONFIG_PATH PIPE_CONTEXT_DIR PIPE_METRICS_DIR \
|
|
124
|
+
export PIPE_PLATFORM PIPE_CONFIG_PATH PIPE_CONTEXT_DIR PIPE_METRICS_DIR PIPE_AGENT_STDERR \
|
|
73
125
|
PIPE_TARGET_BRANCH PIPE_FIX_LOOP_CAP PIPE_CODER_CAP PIPE_ISSUE_SCAN PIPE_COMMIT_TESTFIX \
|
|
74
|
-
PIPE_COMMIT_REVIEWFIX PIPE_COMMIT_COVERAGE PIPE_LABEL_READY PIPE_LABEL_WIP \
|
|
75
|
-
PIPE_LABEL_STUCK PIPE_LABEL_BLOCKER PIPE_LABEL_BLOCKED PIPE_LABEL_DECOMPOSED \
|
|
76
|
-
PIPE_RESULT_REVIEW PIPE_RESULT_REVIEWFIX
|
|
77
|
-
|
|
126
|
+
PIPE_COMMIT_REVIEWFIX PIPE_COMMIT_COVERAGE PIPE_COMMIT_CVEFIX PIPE_LABEL_READY PIPE_LABEL_WIP \
|
|
127
|
+
PIPE_LABEL_STUCK PIPE_LABEL_BLOCKER PIPE_LABEL_BLOCKED PIPE_LABEL_DECOMPOSED PIPE_LABEL_AUDIT \
|
|
128
|
+
PIPE_RESULT_REVIEW PIPE_RESULT_REVIEWFIX PIPE_RESULT_DOCSSYNC PIPE_COMMIT_DOCSSYNC PIPE_COMMIT_AUDIT \
|
|
129
|
+
PIPE_CVE_FIX PIPE_DEPENDENCY_MANIFEST PIPE_CVE_SUPPRESSIONS PIPE_E2E_TEST_GEN PIPE_LABEL_TESTREADY \
|
|
130
|
+
PIPE_LABEL_E2E_SCOPE PIPE_E2E_TEST_DIR PIPE_E2E_BASE_URL PIPE_E2E_VERIFY_CMD PIPE_DOCS_SYNC PIPE_DOCS_ROOTS PIPE_VERIFY_CMD PIPE_COMPILE_CMD \
|
|
131
|
+
PIPE_TEST_REPORT_GLOB PIPE_COVERAGE_RATCHET PIPE_COVERAGE_REPORT PIPE_COVERAGE_REPORT_KIND \
|
|
132
|
+
PIPE_COVERAGE_SIGNAL PIPE_SPEC_TEMPLATE_PATH PIPE_CODEBASE_AUDIT PIPE_AGENT_ARCHITECT \
|
|
133
|
+
PIPE_ARCHITECT_WINDOW_DAYS PIPE_ARCHITECT_DRY_RUN PIPE_LABEL_IMPROVEMENT \
|
|
134
|
+
PIPE_METRICS_SNAPSHOT PIPE_METRICS_OUTPUT_DIR PIPE_METRICS_WINDOW_DAYS \
|
|
135
|
+
PIPE_METRICS_COMMIT_REPO PIPE_METRICS_REVIEW_AGENTS PIPE_METRICS_DEFECT_REVERT \
|
|
136
|
+
PIPE_METRICS_DEFECT_REGRESSION PIPE_METRICS_DEFECT_REINTRODUCE \
|
|
137
|
+
PIPE_METRICS_SNAPSHOT_DATE PIPE_METRICS_RAW_DIR PIPE_METRICS_MRS_FILE \
|
|
78
138
|
PIPE_HARNESS PIPE_MODEL_TRIAGE PIPE_MODEL_CODE PIPE_MODEL_REVIEW
|
|
79
139
|
}
|
|
80
140
|
|
|
@@ -99,6 +159,17 @@ pipe_git_identity() {
|
|
|
99
159
|
# Checkout the real source branch so commits land on the branch tip, not on the
|
|
100
160
|
# detached MR/PR ref that can fall behind and cause non-fast-forward push errors.
|
|
101
161
|
pipe_checkout_source() {
|
|
162
|
+
# The MR/PR itself is the authoritative source of the branch name. A
|
|
163
|
+
# workflow_run or branch pipeline carries no merge-request source ref, and
|
|
164
|
+
# without one this checkout used to no-op silently, leaving the fix commit on
|
|
165
|
+
# a detached head that no push could land.
|
|
166
|
+
if [ -z "${PIPE_SOURCE_BRANCH:-}" ] && [ -n "${PIPE_MR_IID:-}" ]; then
|
|
167
|
+
# `|| PIPE_SOURCE_BRANCH=""` is the whole error path: an unreachable or
|
|
168
|
+
# unconfigured API must leave the caller where it was, not abort it.
|
|
169
|
+
PIPE_SOURCE_BRANCH=$(platform_mr_meta 2>/dev/null | jq -r '.source_branch // ""' 2>/dev/null) \
|
|
170
|
+
|| PIPE_SOURCE_BRANCH=""
|
|
171
|
+
export PIPE_SOURCE_BRANCH
|
|
172
|
+
fi
|
|
102
173
|
[ -z "${PIPE_SOURCE_BRANCH:-}" ] && return 0
|
|
103
174
|
git fetch origin "$PIPE_TARGET_BRANCH" "$PIPE_SOURCE_BRANCH" 2>/dev/null || true
|
|
104
175
|
git checkout -B "$PIPE_SOURCE_BRANCH" "origin/$PIPE_SOURCE_BRANCH" 2>/dev/null || true
|
|
@@ -117,15 +188,17 @@ pipe_rebase_and_push() {
|
|
|
117
188
|
}
|
|
118
189
|
|
|
119
190
|
# --- Cap counting -----------------------------------------------------------
|
|
120
|
-
# Count consecutive commits from the tip back toward the target branch
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
|
|
191
|
+
# Count consecutive matching commits from the tip back toward the target branch.
|
|
192
|
+
# Docs-sync commits are transparent: they do not count, but do not interrupt a
|
|
193
|
+
# fix streak. The skills use the same exact-subject rules for their cap checks;
|
|
194
|
+
# the runner needs this count only to detect whether a fix commit landed.
|
|
124
195
|
pipe_count_fix_commits() {
|
|
125
196
|
local pattern="$1" count=0 subject
|
|
126
197
|
while IFS= read -r subject; do
|
|
127
|
-
if
|
|
198
|
+
if [ "$subject" = "$pattern" ]; then
|
|
128
199
|
count=$((count + 1))
|
|
200
|
+
elif [ "$subject" = "$PIPE_COMMIT_DOCSSYNC" ]; then
|
|
201
|
+
continue
|
|
129
202
|
else
|
|
130
203
|
break
|
|
131
204
|
fi
|
|
@@ -368,23 +441,24 @@ pipe_get_env() {
|
|
|
368
441
|
pipe_apply_escalation() {
|
|
369
442
|
local pm_env="$PIPE_CONTEXT_DIR/postmortem.env"
|
|
370
443
|
local pm_body="$PIPE_CONTEXT_DIR/postmortem.md"
|
|
371
|
-
local add remove category
|
|
444
|
+
local add remove category comment_file
|
|
372
445
|
add=$(pipe_get_env "$pm_env" ADD_LABELS); add=${add:-$PIPE_LABEL_STUCK}
|
|
373
446
|
remove=$(pipe_get_env "$pm_env" REMOVE_LABELS); remove=${remove:-$PIPE_LABEL_WIP}
|
|
374
447
|
category=$(pipe_get_env "$pm_env" FAILURE_CATEGORY); category=${category:-${1:-other}}
|
|
375
448
|
|
|
376
449
|
platform_set_labels "$add" "$remove"
|
|
377
450
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
451
|
+
comment_file="$PIPE_CONTEXT_DIR/auto-fix-stuck.md"
|
|
452
|
+
{
|
|
453
|
+
printf '🤖 **Auto-fix stuck after %s attempts. Diagnostic analysis:**\n\n' "$PIPE_FIX_LOOP_CAP"
|
|
454
|
+
if [ -s "$pm_body" ]; then
|
|
455
|
+
cat "$pm_body"
|
|
456
|
+
else
|
|
457
|
+
printf 'Automated fixes were exhausted after %s attempts. A human needs to take over.' "$PIPE_FIX_LOOP_CAP"
|
|
458
|
+
fi
|
|
459
|
+
printf '\n\nHuman intervention needed.\n'
|
|
460
|
+
} > "$comment_file"
|
|
461
|
+
platform_post_comment_file "$comment_file"
|
|
388
462
|
|
|
389
463
|
pipe_metric_event postmortem dev_stuck \
|
|
390
464
|
"$(jq -n --arg c "$category" --arg ctx "${2:-unknown}" '{failure_category: $c, context: $ctx}')"
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
# Common (both platforms):
|
|
9
9
|
# PIPE_MR_IID MR iid (GitLab) or PR number (GitHub)
|
|
10
10
|
# PIPE_TARGET_BRANCH branch the MR/PR targets
|
|
11
|
-
# PIPE_SOURCE_BRANCH the MR/PR source branch
|
|
11
|
+
# PIPE_SOURCE_BRANCH the MR/PR source branch (optional: platform_mr_meta
|
|
12
|
+
# reports it as `source_branch` when the runtime does not)
|
|
12
13
|
# PIPE_HEAD_SHA current head commit under review
|
|
13
14
|
# PIPE_PROJECT_PATH namespace/project (for git push URL)
|
|
14
15
|
# PIPE_SERVER_HOST git host (for git push URL)
|
|
@@ -58,7 +59,10 @@ _gl_get() {
|
|
|
58
59
|
"$PIPE_API_URL/projects/$PIPE_PROJECT_ID/merge_requests/$PIPE_MR_IID$1"
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
# `source_branch` is the branch the MR/PR was opened from. It comes from the API
|
|
63
|
+
# rather than the CI runtime because branch and workflow_run pipelines carry no
|
|
64
|
+
# merge-request source ref, and a runner that pushes needs it.
|
|
65
|
+
_gl_mr_meta() { _gl_get "" | jq -r '{title: (.title // ""), description: (.description // ""), labels: ((.labels // []) | join(",")), source_branch: (.source_branch // "")}'; }
|
|
62
66
|
_gl_mr_labels() { _gl_get "" | jq -r '((.labels // []) | join(","))'; }
|
|
63
67
|
# Callers take the LAST matching note, so the array must be chronological AND
|
|
64
68
|
# must contain the newest notes. One asc page would return the oldest 100 on a
|
|
@@ -66,11 +70,12 @@ _gl_mr_labels() { _gl_get "" | jq -r '((.labels // []) | join(","))'; }
|
|
|
66
70
|
_gl_notes() { _gl_get "/notes?per_page=100&sort=desc" | jq -r '[.[] | {author: .author.username, body: .body}] | reverse'; }
|
|
67
71
|
_gl_issue() { curl -s -H "PRIVATE-TOKEN: $PIPE_TOKEN" "$PIPE_API_URL/projects/$PIPE_PROJECT_ID/issues/$1" | jq -r '{iid: .iid, title: (.title // ""), description: (.description // "")}'; }
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
_gl_post_comment_file() {
|
|
74
|
+
# $1 = body file under PIPE_CONTEXT_DIR.
|
|
70
75
|
curl --fail-with-body -s -X POST \
|
|
71
76
|
-H "PRIVATE-TOKEN: $PIPE_TOKEN" \
|
|
72
77
|
"$PIPE_API_URL/projects/$PIPE_PROJECT_ID/merge_requests/$PIPE_MR_IID/notes" \
|
|
73
|
-
--data-urlencode "body
|
|
78
|
+
--data-urlencode "body@$1" > /dev/null
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
_gl_set_labels() {
|
|
@@ -102,15 +107,22 @@ _gl_compute_diff_base() {
|
|
|
102
107
|
|
|
103
108
|
_gh_get() { gh api "repos/$PIPE_REPO/$1"; }
|
|
104
109
|
|
|
105
|
-
_gh_mr_meta() { _gh_get "pulls/$PIPE_MR_IID" | jq -r '{title: (.title // ""), description: (.body // ""), labels: ((.labels // []) | map(.name) | join(","))}'; }
|
|
110
|
+
_gh_mr_meta() { _gh_get "pulls/$PIPE_MR_IID" | jq -r '{title: (.title // ""), description: (.body // ""), labels: ((.labels // []) | map(.name) | join(",")), source_branch: (.head.ref // "")}'; }
|
|
106
111
|
_gh_mr_labels() { _gh_get "pulls/$PIPE_MR_IID" | jq -r '((.labels // []) | map(.name) | join(","))'; }
|
|
107
112
|
# The per-issue comments endpoint has no sort param, so paginate all pages to
|
|
108
113
|
# guarantee the newest comments are present (callers take the last match).
|
|
109
114
|
_gh_notes() { gh api --paginate "repos/$PIPE_REPO/issues/$PIPE_MR_IID/comments?per_page=100" --jq '.[] | {author: .user.login, body: .body}' | jq -s '.'; }
|
|
110
115
|
_gh_issue() { _gh_get "issues/$1" | jq -r '{iid: .number, title: (.title // ""), description: (.body // "")}'; }
|
|
111
116
|
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
_gh_post_comment_file() {
|
|
118
|
+
# $1 = body file under PIPE_CONTEXT_DIR.
|
|
119
|
+
local payload status=0
|
|
120
|
+
payload=$(mktemp "$PIPE_CONTEXT_DIR/github-comment.XXXXXX.json")
|
|
121
|
+
jq -n --rawfile body "$1" '{body:$body}' > "$payload"
|
|
122
|
+
gh api -X POST "repos/$PIPE_REPO/issues/$PIPE_MR_IID/comments" \
|
|
123
|
+
-H "Content-Type: application/json" --input "$payload" > /dev/null || status=$?
|
|
124
|
+
rm -f "$payload"
|
|
125
|
+
return "$status"
|
|
114
126
|
}
|
|
115
127
|
|
|
116
128
|
_gh_set_labels() {
|
|
@@ -135,6 +147,179 @@ _gh_compute_diff_base() {
|
|
|
135
147
|
fi
|
|
136
148
|
}
|
|
137
149
|
|
|
150
|
+
# --- Pipeline and artifact helpers ------------------------------------------
|
|
151
|
+
|
|
152
|
+
_gl_completed_pipelines() {
|
|
153
|
+
local updated_after
|
|
154
|
+
updated_after=$(date -u -d "$1 days ago" '+%Y-%m-%dT%H:%M:%SZ') || return 1
|
|
155
|
+
curl -s --fail -H "PRIVATE-TOKEN: $PIPE_TOKEN" \
|
|
156
|
+
"$PIPE_API_URL/projects/$PIPE_PROJECT_ID/pipelines?scope=finished&updated_after=$updated_after&per_page=100" |
|
|
157
|
+
jq -c '[.[] | {id, status: (.status // ""), ref: (.ref // ""), updated_at: (.updated_at // "")}]'
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_gl_pipeline_artifacts() {
|
|
161
|
+
curl -s --fail -H "PRIVATE-TOKEN: $PIPE_TOKEN" \
|
|
162
|
+
"$PIPE_API_URL/projects/$PIPE_PROJECT_ID/pipelines/$1/jobs?per_page=100" |
|
|
163
|
+
jq -c '[.[] | {job_id: .id, job_name: (.name // ""), artifact_available: (((.artifacts_file // {}) | .filename) != null)}]'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_gl_download_artifact() {
|
|
167
|
+
curl -sSL --fail -H "PRIVATE-TOKEN: $PIPE_TOKEN" \
|
|
168
|
+
"$PIPE_API_URL/projects/$PIPE_PROJECT_ID/jobs/$1/artifacts" -o "$2"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
_gl_latest_success_coverage() {
|
|
172
|
+
curl -s --fail -H "PRIVATE-TOKEN: $PIPE_TOKEN" \
|
|
173
|
+
"$PIPE_API_URL/projects/$PIPE_PROJECT_ID/pipelines?ref=$1&status=success&per_page=1" |
|
|
174
|
+
jq -r '.[0].coverage // ""'
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_gl_merge_requests_updated_since() {
|
|
178
|
+
local page response page_size results
|
|
179
|
+
page=1
|
|
180
|
+
results='[]'
|
|
181
|
+
while :; do
|
|
182
|
+
response=$(curl -s --fail -H "PRIVATE-TOKEN: $PIPE_TOKEN" \
|
|
183
|
+
"$PIPE_API_URL/projects/$PIPE_PROJECT_ID/merge_requests?updated_after=$1&per_page=100&page=$page&scope=all") || return 1
|
|
184
|
+
page_size=$(jq -r 'length' <<< "$response") || return 1
|
|
185
|
+
results=$(jq -c --argjson results "$results" \
|
|
186
|
+
'$results + [.[] | {iid, title: (.title // ""), state: (.state // ""), author: (.author.username // ""), labels: (.labels // []), created_at: (.created_at // ""), merged_at: .merged_at, target_branch: (.target_branch // ""), description: (.description // "")}]' <<< "$response") || return 1
|
|
187
|
+
[ "$page_size" -lt 100 ] && break
|
|
188
|
+
page=$((page + 1))
|
|
189
|
+
done
|
|
190
|
+
printf '%s\n' "$results"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Merged MRs targeting a branch, newest first, filtered to iid > since_iid.
|
|
194
|
+
# Used by the codebase-audit job to find work since the last audit.
|
|
195
|
+
_gl_merged_mrs() {
|
|
196
|
+
# $1=target_branch $2=since_iid $3=limit. One page, newest merged first;
|
|
197
|
+
# matches the source script's own non-paginated fetch. Captured into a
|
|
198
|
+
# variable (not piped straight into jq) so a curl failure propagates: a
|
|
199
|
+
# pipeline's exit status is jq's, not curl's, without `set -o pipefail`.
|
|
200
|
+
local response
|
|
201
|
+
response=$(curl -s --fail -H "PRIVATE-TOKEN: $PIPE_TOKEN" \
|
|
202
|
+
"$PIPE_API_URL/projects/$PIPE_PROJECT_ID/merge_requests?target_branch=$1&state=merged&per_page=$3&order_by=created_at&sort=desc") || return 1
|
|
203
|
+
jq -c --argjson since "$2" '[.[] | select(.iid > $since) | {iid, title: (.title // ""), created_at: (.created_at // "")}]' <<< "$response"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_gl_mr_changes() {
|
|
207
|
+
# $1=iid $2=limit. Changed file paths for one merged MR, capped at limit.
|
|
208
|
+
local response
|
|
209
|
+
response=$(curl -s --fail -H "PRIVATE-TOKEN: $PIPE_TOKEN" \
|
|
210
|
+
"$PIPE_API_URL/projects/$PIPE_PROJECT_ID/merge_requests/$1/changes?per_page=$2") || return 1
|
|
211
|
+
jq -r --argjson limit "$2" '[.changes[]?.new_path] | .[0:$limit][]' <<< "$response" 2>/dev/null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# $1=iid $2=limit $3=sort("asc"|"desc"). Normalized, non-system MR notes.
|
|
215
|
+
_gl_mr_notes() {
|
|
216
|
+
local response
|
|
217
|
+
[[ "$2" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
218
|
+
[[ "$3" = "asc" || "$3" = "desc" ]] || return 1
|
|
219
|
+
response=$(curl -s --fail -H "PRIVATE-TOKEN: $PIPE_TOKEN" \
|
|
220
|
+
"$PIPE_API_URL/projects/$PIPE_PROJECT_ID/merge_requests/$1/notes?per_page=$2&sort=$3") || return 1
|
|
221
|
+
jq --argjson limit "$2" '[.[] | select(.system != true)
|
|
222
|
+
| {author: (.author.username // ""), body: (.body // "")}] | .[0:$limit]' <<< "$response"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# These APIs return downloadable binary responses as well as JSON. Use curl
|
|
226
|
+
# rather than gh so artifact downloads remain a silent, status-only operation.
|
|
227
|
+
_gh_pipeline_api() {
|
|
228
|
+
local token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
|
|
229
|
+
curl -s --fail \
|
|
230
|
+
-H "Accept: application/vnd.github+json" \
|
|
231
|
+
-H "Authorization: Bearer $token" \
|
|
232
|
+
"${GITHUB_API_URL:-https://api.github.com}/repos/$PIPE_REPO/$1"
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
_gh_completed_pipelines() {
|
|
236
|
+
local updated_after page response page_size results
|
|
237
|
+
updated_after=$(date -u -d "$1 days ago" '+%Y-%m-%dT%H:%M:%SZ') || return 1
|
|
238
|
+
page=1
|
|
239
|
+
results='[]'
|
|
240
|
+
while :; do
|
|
241
|
+
response=$(_gh_pipeline_api "actions/runs?status=completed&per_page=100&page=$page") || return 1
|
|
242
|
+
page_size=$(jq -r '(.workflow_runs // []) | length' <<< "$response") || return 1
|
|
243
|
+
results=$(jq -c --argjson results "$results" --arg updated_after "$updated_after" \
|
|
244
|
+
'$results + [.workflow_runs[] | select((.updated_at // "") >= $updated_after) | {id, status: (.conclusion // .status // ""), ref: (.head_branch // ""), updated_at: (.updated_at // "")}]' <<< "$response") || return 1
|
|
245
|
+
[ "$page_size" -lt 100 ] && break
|
|
246
|
+
page=$((page + 1))
|
|
247
|
+
done
|
|
248
|
+
printf '%s\n' "$results"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_gh_pipeline_artifacts() {
|
|
252
|
+
_gh_pipeline_api "actions/runs/$1/artifacts?per_page=100" |
|
|
253
|
+
jq -c '[.artifacts[] | {job_id: .id, job_name: (.name // ""), artifact_available: ((.expired // true) | not)}]'
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_gh_download_artifact() {
|
|
257
|
+
local token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
|
|
258
|
+
curl -sSL --fail \
|
|
259
|
+
-H "Accept: application/vnd.github+json" \
|
|
260
|
+
-H "Authorization: Bearer $token" \
|
|
261
|
+
"${GITHUB_API_URL:-https://api.github.com}/repos/$PIPE_REPO/actions/artifacts/$1/zip" -o "$2"
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_gh_latest_success_coverage() {
|
|
265
|
+
_gh_pipeline_api "actions/runs?branch=$1&status=success&per_page=1" |
|
|
266
|
+
jq -r '.workflow_runs[0].coverage // ""'
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# The pull-request API has no updated-since filter, so the newest-first listing
|
|
270
|
+
# is walked until a page carries nothing inside the window.
|
|
271
|
+
_gh_merge_requests_updated_since() {
|
|
272
|
+
local page response page_size kept kept_size results
|
|
273
|
+
page=1
|
|
274
|
+
results='[]'
|
|
275
|
+
while :; do
|
|
276
|
+
response=$(_gh_pipeline_api "pulls?state=all&sort=updated&direction=desc&per_page=100&page=$page") || return 1
|
|
277
|
+
page_size=$(jq -r 'length' <<< "$response") || return 1
|
|
278
|
+
kept=$(jq -c --arg since "$1" \
|
|
279
|
+
'[.[] | select((.updated_at // "") >= $since) | {iid: .number, title: (.title // ""), state: (if .merged_at then "merged" else (.state // "") end), author: (.user.login // ""), labels: [(.labels // [])[].name], created_at: (.created_at // ""), merged_at: .merged_at, target_branch: (.base.ref // ""), description: (.body // "")}]' <<< "$response") || return 1
|
|
280
|
+
kept_size=$(jq -r 'length' <<< "$kept") || return 1
|
|
281
|
+
results=$(jq -nc --argjson results "$results" --argjson kept "$kept" '$results + $kept') || return 1
|
|
282
|
+
[ "$kept_size" -lt "$page_size" ] && break
|
|
283
|
+
[ "$page_size" -lt 100 ] && break
|
|
284
|
+
page=$((page + 1))
|
|
285
|
+
done
|
|
286
|
+
printf '%s\n' "$results"
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# Merged PRs targeting a branch, newest first, filtered to number > since_iid.
|
|
290
|
+
# GitHub's closed-PR listing has no server-side merged-only filter, so
|
|
291
|
+
# merged_at != null is applied client-side.
|
|
292
|
+
_gh_merged_mrs() {
|
|
293
|
+
# $1=target_branch $2=since_iid $3=limit.
|
|
294
|
+
local response
|
|
295
|
+
response=$(_gh_pipeline_api "pulls?base=$1&state=closed&per_page=$3&sort=created&direction=desc") || return 1
|
|
296
|
+
jq -c --argjson since "$2" '[.[] | select(.merged_at != null and .number > $since) | {iid: .number, title: (.title // ""), created_at: (.created_at // "")}]' <<< "$response"
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
_gh_mr_changes() {
|
|
300
|
+
# $1=iid $2=limit. Changed file paths for one merged PR, capped at limit.
|
|
301
|
+
local response
|
|
302
|
+
response=$(_gh_pipeline_api "pulls/$1/files?per_page=$2") || return 1
|
|
303
|
+
jq -r --argjson limit "$2" '[.[]?.filename] | .[0:$limit][]' <<< "$response" 2>/dev/null
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# $1=iid $2=limit $3=sort("asc"|"desc"). GitHub returns comments oldest
|
|
307
|
+
# first, so descend requests paginate before reversing and slicing to preserve
|
|
308
|
+
# real newest-first semantics rather than reversing an arbitrary first page.
|
|
309
|
+
_gh_mr_notes() {
|
|
310
|
+
local response filter
|
|
311
|
+
[[ "$2" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
312
|
+
[[ "$3" = "asc" || "$3" = "desc" ]] || return 1
|
|
313
|
+
if [ "$3" = "asc" ]; then
|
|
314
|
+
response=$(gh api "repos/$PIPE_REPO/issues/$1/comments?per_page=$2") || return 1
|
|
315
|
+
else
|
|
316
|
+
response=$(gh api --paginate "repos/$PIPE_REPO/issues/$1/comments?per_page=100") || return 1
|
|
317
|
+
fi
|
|
318
|
+
filter='[.[] | {author: (.user.login // ""), body: (.body // "")}]'
|
|
319
|
+
[ "$3" = "desc" ] && filter="$filter | reverse"
|
|
320
|
+
jq --argjson limit "$2" "$filter | .[0:\$limit]" <<< "$response"
|
|
321
|
+
}
|
|
322
|
+
|
|
138
323
|
# --- Bot identity -------------------------------------------------------------
|
|
139
324
|
# The scripts filter MR/PR comments by the bot's username to recognise their own
|
|
140
325
|
# previous output. The token already knows whose it is, so an empty
|
|
@@ -155,12 +340,84 @@ platform_resolve_bot_user() {
|
|
|
155
340
|
|
|
156
341
|
# --- Public dispatch --------------------------------------------------------
|
|
157
342
|
|
|
158
|
-
platform_mr_meta()
|
|
159
|
-
platform_mr_labels()
|
|
160
|
-
platform_notes()
|
|
161
|
-
platform_issue()
|
|
162
|
-
|
|
163
|
-
platform_set_labels()
|
|
343
|
+
platform_mr_meta() { case "$PIPE_PLATFORM" in gitlab) _gl_mr_meta ;; github) _gh_mr_meta ;; esac; }
|
|
344
|
+
platform_mr_labels() { case "$PIPE_PLATFORM" in gitlab) _gl_mr_labels ;; github) _gh_mr_labels ;; esac; }
|
|
345
|
+
platform_notes() { case "$PIPE_PLATFORM" in gitlab) _gl_notes ;; github) _gh_notes ;; esac; }
|
|
346
|
+
platform_issue() { case "$PIPE_PLATFORM" in gitlab) _gl_issue "$1" ;; github) _gh_issue "$1" ;; esac; }
|
|
347
|
+
platform_post_comment_file() { case "$PIPE_PLATFORM" in gitlab) _gl_post_comment_file "$1" ;; github) _gh_post_comment_file "$1" ;; esac; }
|
|
348
|
+
platform_set_labels() { case "$PIPE_PLATFORM" in gitlab) _gl_set_labels "$1" "$2" ;; github) _gh_set_labels "$1" "$2" ;; esac; }
|
|
349
|
+
|
|
350
|
+
platform_completed_pipelines() {
|
|
351
|
+
[ "$#" -eq 1 ] || return 1
|
|
352
|
+
case "$PIPE_PLATFORM" in
|
|
353
|
+
gitlab) _gl_completed_pipelines "$1" ;;
|
|
354
|
+
github) _gh_completed_pipelines "$1" ;;
|
|
355
|
+
*) return 1 ;;
|
|
356
|
+
esac
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
platform_pipeline_artifacts() {
|
|
360
|
+
[ "$#" -eq 1 ] || return 1
|
|
361
|
+
case "$PIPE_PLATFORM" in
|
|
362
|
+
gitlab) _gl_pipeline_artifacts "$1" ;;
|
|
363
|
+
github) _gh_pipeline_artifacts "$1" ;;
|
|
364
|
+
*) return 1 ;;
|
|
365
|
+
esac
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
platform_download_artifact() {
|
|
369
|
+
[ "$#" -eq 2 ] || return 1
|
|
370
|
+
case "$PIPE_PLATFORM" in
|
|
371
|
+
gitlab) _gl_download_artifact "$1" "$2" ;;
|
|
372
|
+
github) _gh_download_artifact "$1" "$2" ;;
|
|
373
|
+
*) return 1 ;;
|
|
374
|
+
esac
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
platform_latest_success_coverage() {
|
|
378
|
+
[ "$#" -eq 1 ] || return 1
|
|
379
|
+
case "$PIPE_PLATFORM" in
|
|
380
|
+
gitlab) _gl_latest_success_coverage "$1" ;;
|
|
381
|
+
github) _gh_latest_success_coverage "$1" ;;
|
|
382
|
+
*) return 1 ;;
|
|
383
|
+
esac
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
platform_merge_requests_updated_since() {
|
|
387
|
+
[ "$#" -eq 1 ] || return 1
|
|
388
|
+
case "$PIPE_PLATFORM" in
|
|
389
|
+
gitlab) _gl_merge_requests_updated_since "$1" ;;
|
|
390
|
+
github) _gh_merge_requests_updated_since "$1" ;;
|
|
391
|
+
*) return 1 ;;
|
|
392
|
+
esac
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
platform_merged_mrs() {
|
|
396
|
+
[ "$#" -eq 3 ] || return 1
|
|
397
|
+
case "$PIPE_PLATFORM" in
|
|
398
|
+
gitlab) _gl_merged_mrs "$1" "$2" "$3" ;;
|
|
399
|
+
github) _gh_merged_mrs "$1" "$2" "$3" ;;
|
|
400
|
+
*) return 1 ;;
|
|
401
|
+
esac
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
platform_mr_changes() {
|
|
405
|
+
[ "$#" -eq 2 ] || return 1
|
|
406
|
+
case "$PIPE_PLATFORM" in
|
|
407
|
+
gitlab) _gl_mr_changes "$1" "$2" ;;
|
|
408
|
+
github) _gh_mr_changes "$1" "$2" ;;
|
|
409
|
+
*) return 1 ;;
|
|
410
|
+
esac
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
platform_mr_notes() {
|
|
414
|
+
[ "$#" -eq 3 ] || return 1
|
|
415
|
+
case "$PIPE_PLATFORM" in
|
|
416
|
+
gitlab) _gl_mr_notes "$1" "$2" "$3" ;;
|
|
417
|
+
github) _gh_mr_notes "$1" "$2" "$3" ;;
|
|
418
|
+
*) return 1 ;;
|
|
419
|
+
esac
|
|
420
|
+
}
|
|
164
421
|
|
|
165
422
|
# Sets PIPE_DIFF_BASE and PIPE_IS_INCREMENTAL. Call before building the diff.
|
|
166
423
|
platform_compute_diff_base() {
|
|
@@ -110,7 +110,13 @@ capture_omp() {
|
|
|
110
110
|
omp_exit_code: $exit_code
|
|
111
111
|
}' > "$record_file"
|
|
112
112
|
|
|
113
|
-
# If
|
|
113
|
+
# If the agent failed, forward stderr so debugging still works, and keep a
|
|
114
|
+
# durable copy: the failure detector runs after the invocation returns and
|
|
115
|
+
# cannot read a temp file the wrapper already deleted.
|
|
116
|
+
if [ -n "${PIPE_AGENT_STDERR:-}" ]; then
|
|
117
|
+
mkdir -p "$(dirname "$PIPE_AGENT_STDERR")" 2>/dev/null || true
|
|
118
|
+
cp "$stderr_file" "$PIPE_AGENT_STDERR" 2>/dev/null || true
|
|
119
|
+
fi
|
|
114
120
|
[ "$exit_code" -ne 0 ] && cat "$stderr_file" >&2
|
|
115
121
|
rm -f "$stream_file" "$stderr_file"
|
|
116
122
|
return "$exit_code"
|
|
@@ -103,7 +103,13 @@ capture_claude() {
|
|
|
103
103
|
claude_exit_code: $exit_code
|
|
104
104
|
}' > "$record_file"
|
|
105
105
|
|
|
106
|
-
# If
|
|
106
|
+
# If the agent failed, forward stderr so debugging still works, and keep a
|
|
107
|
+
# durable copy: the failure detector runs after the invocation returns and
|
|
108
|
+
# cannot read a temp file the wrapper already deleted.
|
|
109
|
+
if [ -n "${PIPE_AGENT_STDERR:-}" ]; then
|
|
110
|
+
mkdir -p "$(dirname "$PIPE_AGENT_STDERR")" 2>/dev/null || true
|
|
111
|
+
cp "$stderr_file" "$PIPE_AGENT_STDERR" 2>/dev/null || true
|
|
112
|
+
fi
|
|
107
113
|
[ "$exit_code" -ne 0 ] && cat "$stderr_file" >&2
|
|
108
114
|
rm -f "$stream_file" "$stderr_file"
|
|
109
115
|
return "$exit_code"
|