@cxi-lmai/ci-agent-platform 3.1.1 → 3.1.3
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 +7 -0
- package/package.json +1 -1
- package/payload/ci-templates/claude-pipeline.gitlab-ci.yml +10 -3
- package/payload/ci-templates/scripts/lib/issue-loop.sh +93 -8
- package/payload/ci-templates/scripts/lib/pipeline-common.sh +28 -0
- package/payload/ci-templates/scripts/orchestrate.sh +95 -12
package/README.md
CHANGED
|
@@ -144,6 +144,13 @@ picked, not both. Claude-harness model usage is billed by Anthropic, while
|
|
|
144
144
|
omp-harness model usage is billed by OpenRouter. GitLab only for now; GitHub
|
|
145
145
|
Actions omp support is not shipped yet.
|
|
146
146
|
|
|
147
|
+
`omp` is a Bun program, so the GitLab template's `before_script` installs
|
|
148
|
+
`bun` next to it (`npm install -g bun`) — the default `node:22-bookworm` image
|
|
149
|
+
ships no Bun runtime, and without it every agent call fails while the job
|
|
150
|
+
still reports success. A custom `PIPE_CI_IMAGE` needs nothing beyond node +
|
|
151
|
+
npm for that install to work. If the harness binary cannot execute, the job
|
|
152
|
+
now fails immediately instead of going green with no review.
|
|
153
|
+
|
|
147
154
|
### What it costs
|
|
148
155
|
|
|
149
156
|
Every pipeline job spends paid model usage, so the defaults are deliberately
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cxi-lmai/ci-agent-platform",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
4
4
|
"description": "Autonomous dev pipeline on plain GitLab CI or GitHub Actions, driven by Claude Code. A labeled issue goes in, an open merge request comes out.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -31,8 +31,9 @@ variables:
|
|
|
31
31
|
PIPE_SCRIPTS_DIR: ".claude-pipeline/scripts"
|
|
32
32
|
|
|
33
33
|
# --- CI image (see ci-templates decision note) -----------------------------
|
|
34
|
-
# Must provide: node + npm, git, curl. jq
|
|
35
|
-
#
|
|
34
|
+
# Must provide: node + npm, git, curl. jq, claude-code, and (when
|
|
35
|
+
# PIPE_HARNESS=omp) bun + omp are installed in before_script when missing.
|
|
36
|
+
# The runner scripts talk to the GitLab API with
|
|
36
37
|
# curl and jq directly, so no glab is needed in the image; glab is only used
|
|
37
38
|
# interactively, by the install wizard.
|
|
38
39
|
# Python caveat (E2E finding N-7): node:22-bookworm ships python3 but NOT
|
|
@@ -162,7 +163,7 @@ variables:
|
|
|
162
163
|
|
|
163
164
|
# --- Hidden base job: shared config + runtime variable mapping ---------------
|
|
164
165
|
# Maps GitLab's CI_ runtime variables onto the PIPE_ names the scripts read, and
|
|
165
|
-
# installs
|
|
166
|
+
# installs the selected harness (and its runtime) if the image does not ship it.
|
|
166
167
|
.claude-base:
|
|
167
168
|
image: $PIPE_CI_IMAGE
|
|
168
169
|
variables:
|
|
@@ -193,6 +194,12 @@ variables:
|
|
|
193
194
|
fi
|
|
194
195
|
- |
|
|
195
196
|
if [ "$PIPE_HARNESS" = "omp" ]; then
|
|
197
|
+
# omp is a Bun program (`#!/usr/bin/env bun`, engines.bun >= 1.3.14)
|
|
198
|
+
# and node:22-bookworm ships no bun, so the installed `omp` shim is
|
|
199
|
+
# unrunnable without it. The `bun` npm package postinstalls the real
|
|
200
|
+
# binary, so no curl|bash and no image change is needed. Deliberately
|
|
201
|
+
# no `|| true`: a missing harness runtime must fail the job loudly.
|
|
202
|
+
command -v bun >/dev/null 2>&1 || npm install -g bun
|
|
196
203
|
command -v omp >/dev/null 2>&1 || npm install -g @oh-my-pi/pi-coding-agent
|
|
197
204
|
else
|
|
198
205
|
command -v claude >/dev/null 2>&1 || npm install -g @anthropic-ai/claude-code
|
|
@@ -120,6 +120,35 @@ _gli_issue_state() {
|
|
|
120
120
|
curl -s -H "PRIVATE-TOKEN: $PIPE_TOKEN" "$(_gli_api)/issues/$1" | jq -r '.state // "opened"'
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
_gli_merged_closer() {
|
|
124
|
+
# $1=iid. Prints the newest merged MR that closes the issue as compact JSON,
|
|
125
|
+
# or nothing. A later reopen invalidates an earlier merge.
|
|
126
|
+
local response closer events merged_at
|
|
127
|
+
response=$(curl -s --fail -H "PRIVATE-TOKEN: $PIPE_TOKEN" \
|
|
128
|
+
"$(_gli_api)/issues/$1/closed_by?per_page=100") || return 1
|
|
129
|
+
closer=$(jq -c --arg branch "$PIPE_TARGET_BRANCH" '
|
|
130
|
+
[.[] | select(.state == "merged" and .merged_at != null and .target_branch == $branch)]
|
|
131
|
+
| max_by(.merged_at)
|
|
132
|
+
| if . == null then empty else
|
|
133
|
+
{kind: "MR",
|
|
134
|
+
reference: (.references.relative // .reference // ("!" + (.iid | tostring))),
|
|
135
|
+
url: .web_url,
|
|
136
|
+
merged_at: .merged_at}
|
|
137
|
+
end
|
|
138
|
+
' <<< "$response") || return 1
|
|
139
|
+
[ -n "$closer" ] || return 0
|
|
140
|
+
|
|
141
|
+
events=$(curl -s --fail -H "PRIVATE-TOKEN: $PIPE_TOKEN" \
|
|
142
|
+
"$(_gli_api)/issues/$1/resource_state_events?per_page=100") || return 1
|
|
143
|
+
merged_at=$(jq -r '.merged_at' <<< "$closer")
|
|
144
|
+
if jq -e --arg merged_at "$merged_at" \
|
|
145
|
+
'any(.[]; .state == "opened" and (.created_at // "") > $merged_at)' \
|
|
146
|
+
<<< "$events" >/dev/null; then
|
|
147
|
+
return 0
|
|
148
|
+
fi
|
|
149
|
+
printf '%s\n' "$closer"
|
|
150
|
+
}
|
|
151
|
+
|
|
123
152
|
_gli_blockers_open_count() {
|
|
124
153
|
# $1=iid. Count of open issues that block this one (native links).
|
|
125
154
|
curl -s -H "PRIVATE-TOKEN: $PIPE_TOKEN" "$(_gli_api)/issues/$1/links" \
|
|
@@ -168,8 +197,9 @@ _gli_link_blocked_by() {
|
|
|
168
197
|
}
|
|
169
198
|
|
|
170
199
|
_gli_set_issue_labels() {
|
|
171
|
-
# $1=iid $2=add(comma) $3=remove(comma).
|
|
172
|
-
curl -s -X PUT
|
|
200
|
+
# $1=iid $2=add(comma) $3=remove(comma). HTTP failures must stop dispatch.
|
|
201
|
+
curl --fail-with-body -s -X PUT \
|
|
202
|
+
-H "PRIVATE-TOKEN: $PIPE_TOKEN" -H "Content-Type: application/json" \
|
|
173
203
|
"$(_gli_api)/issues/$1" \
|
|
174
204
|
-d "$(jq -n --arg a "$2" --arg r "$3" '{add_labels:$a, remove_labels:$r}')" > /dev/null
|
|
175
205
|
}
|
|
@@ -310,10 +340,57 @@ _ghi_issue_meta() {
|
|
|
310
340
|
|
|
311
341
|
_ghi_issue_state() {
|
|
312
342
|
# Prints "opened" or "closed" (normalised to the GitLab vocabulary).
|
|
313
|
-
local s
|
|
343
|
+
local s
|
|
344
|
+
s=$(gh api "repos/$PIPE_REPO/issues/$1" --jq '.state // "open"' 2>/dev/null) || return 1
|
|
314
345
|
[ "$s" = "closed" ] && echo closed || echo opened
|
|
315
346
|
}
|
|
316
347
|
|
|
348
|
+
_ghi_merged_closer() {
|
|
349
|
+
# $1=iid. GitHub's GraphQL closing-reference connection is exact for both
|
|
350
|
+
# keyword and manually linked PRs. One query also carries reopen events.
|
|
351
|
+
local owner="${PIPE_REPO%%/*}" repo="${PIPE_REPO#*/}" response
|
|
352
|
+
response=$(gh api graphql \
|
|
353
|
+
-f "owner=$owner" -f "name=$repo" -F "number=$1" \
|
|
354
|
+
-f query='
|
|
355
|
+
query($owner: String!, $name: String!, $number: Int!) {
|
|
356
|
+
repository(owner: $owner, name: $name) {
|
|
357
|
+
issue(number: $number) {
|
|
358
|
+
closedByPullRequestsReferences(first: 100, includeClosedPrs: true) {
|
|
359
|
+
nodes {
|
|
360
|
+
number
|
|
361
|
+
url
|
|
362
|
+
merged
|
|
363
|
+
mergedAt
|
|
364
|
+
baseRefName
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
timelineItems(last: 100, itemTypes: [REOPENED_EVENT]) {
|
|
368
|
+
nodes {
|
|
369
|
+
... on ReopenedEvent {
|
|
370
|
+
createdAt
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
') || return 1
|
|
378
|
+
jq -c --arg branch "$PIPE_TARGET_BRANCH" '
|
|
379
|
+
.data.repository.issue as $issue
|
|
380
|
+
| ([$issue.closedByPullRequestsReferences.nodes[]
|
|
381
|
+
| select(.merged == true and .mergedAt != null and .baseRefName == $branch)]
|
|
382
|
+
| max_by(.mergedAt)) as $closer
|
|
383
|
+
| ([$issue.timelineItems.nodes[]?.createdAt] | max // "") as $reopened_at
|
|
384
|
+
| if $closer != null and ($reopened_at == "" or $closer.mergedAt > $reopened_at)
|
|
385
|
+
then {kind: "PR",
|
|
386
|
+
reference: ("#" + ($closer.number | tostring)),
|
|
387
|
+
url: $closer.url,
|
|
388
|
+
merged_at: $closer.mergedAt}
|
|
389
|
+
else empty
|
|
390
|
+
end
|
|
391
|
+
' <<< "$response"
|
|
392
|
+
}
|
|
393
|
+
|
|
317
394
|
_ghi_link_blocked_by() {
|
|
318
395
|
# GitHub has no native blocking link. Persist the real issue number in the
|
|
319
396
|
# dependent issue body after all local decomposition keys have been mapped.
|
|
@@ -351,8 +428,14 @@ _ghi_set_issue_labels() {
|
|
|
351
428
|
# $1=iid $2=add(comma) $3=remove(comma)
|
|
352
429
|
local l
|
|
353
430
|
local IFS=','
|
|
354
|
-
for l in $2; do
|
|
355
|
-
|
|
431
|
+
for l in $2; do
|
|
432
|
+
[ -z "$l" ] || gh api -X POST "repos/$PIPE_REPO/issues/$1/labels" \
|
|
433
|
+
-f "labels[]=$l" > /dev/null 2>&1 || return 1
|
|
434
|
+
done
|
|
435
|
+
for l in $3; do
|
|
436
|
+
[ -z "$l" ] || gh api -X DELETE "repos/$PIPE_REPO/issues/$1/labels/$l" \
|
|
437
|
+
> /dev/null 2>&1 || return 1
|
|
438
|
+
done
|
|
356
439
|
}
|
|
357
440
|
|
|
358
441
|
_ghi_close_issue() {
|
|
@@ -428,6 +511,7 @@ issue_notes() { case "$PIPE_PLATFORM" in gitlab) _gli_issue_notes "
|
|
|
428
511
|
issue_body() { case "$PIPE_PLATFORM" in gitlab) _gli_issue_body "$1" ;; github) _ghi_issue_body "$1" ;; esac; }
|
|
429
512
|
issue_meta() { case "$PIPE_PLATFORM" in gitlab) _gli_issue_meta "$1" ;; github) _ghi_issue_meta "$1" ;; esac; }
|
|
430
513
|
issue_state() { case "$PIPE_PLATFORM" in gitlab) _gli_issue_state "$1" ;; github) _ghi_issue_state "$1" ;; esac; }
|
|
514
|
+
issue_merged_closer() { case "$PIPE_PLATFORM" in gitlab) _gli_merged_closer "$1" ;; github) _ghi_merged_closer "$1" ;; esac; }
|
|
431
515
|
issue_create_file() { case "$PIPE_PLATFORM" in gitlab) _gli_create_issue_file "$1" "$2" "$3" ;; github) _ghi_create_issue_file "$1" "$2" "$3" ;; esac; }
|
|
432
516
|
issue_set_labels() { case "$PIPE_PLATFORM" in gitlab) _gli_set_issue_labels "$1" "$2" "$3" ;; github) _ghi_set_issue_labels "$1" "$2" "$3" ;; esac; }
|
|
433
517
|
issue_close() { case "$PIPE_PLATFORM" in gitlab) _gli_close_issue "$1" ;; github) _ghi_close_issue "$1" ;; esac; }
|
|
@@ -442,12 +526,13 @@ issue_blockers_open_count() {
|
|
|
442
526
|
case "$PIPE_PLATFORM" in
|
|
443
527
|
gitlab) _gli_blockers_open_count "$1" ;;
|
|
444
528
|
github)
|
|
445
|
-
local body refs n open=0
|
|
446
|
-
body=$(_ghi_issue_body "$1")
|
|
529
|
+
local body refs n state open=0
|
|
530
|
+
body=$(_ghi_issue_body "$1") || return 1
|
|
447
531
|
# Match "Blocked by #12" / "blocked by #12, #13" case-insensitively.
|
|
448
532
|
refs=$(printf '%s' "$body" | grep -ioE 'blocked by[^\n]*' | grep -oE '#[0-9]+' | grep -oE '[0-9]+' | sort -u || true)
|
|
449
533
|
for n in $refs; do
|
|
450
|
-
|
|
534
|
+
state=$(_ghi_issue_state "$n") || return 1
|
|
535
|
+
[ "$state" = "opened" ] && open=$((open + 1))
|
|
451
536
|
done
|
|
452
537
|
echo "$open"
|
|
453
538
|
;;
|
|
@@ -246,12 +246,39 @@ pipe_scrub_agent_secrets() {
|
|
|
246
246
|
done < <(compgen -e | while IFS= read -r v; do pipe_agent_secret_var "$v" && echo "$v"; done)
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
# --- Harness preflight ------------------------------------------------------
|
|
250
|
+
# Every pipe_run_* invocation ends in `|| true`, so a harness binary that
|
|
251
|
+
# exists but cannot execute used to produce a green job with no review, no
|
|
252
|
+
# marker file and one line on stderr (issue #1: `omp` is a Bun program and the
|
|
253
|
+
# default node:22-bookworm image ships no bun, so `command -v omp` succeeded
|
|
254
|
+
# while every run failed with "/usr/bin/env: 'bun': No such file or
|
|
255
|
+
# directory"). Executing the binary once, before the tolerant invocation,
|
|
256
|
+
# converts any harness-runtime breakage into an immediate job failure.
|
|
257
|
+
|
|
258
|
+
pipe_require_harness() {
|
|
259
|
+
# $1 = harness binary name (claude|omp). Exits the job on failure: a
|
|
260
|
+
# non-runnable harness means no agent can run, so there is nothing left for
|
|
261
|
+
# the caller to recover from.
|
|
262
|
+
local bin="$1" out
|
|
263
|
+
if ! command -v "$bin" >/dev/null 2>&1; then
|
|
264
|
+
pipe_log "FATAL: harness binary '$bin' not found on PATH (PIPE_HARNESS=${PIPE_HARNESS:-claude}); install it in before_script"
|
|
265
|
+
exit 1
|
|
266
|
+
fi
|
|
267
|
+
if ! out=$("$bin" --version 2>&1); then
|
|
268
|
+
pipe_log "FATAL: harness binary '$bin' ($(command -v "$bin")) cannot execute: '$bin --version' failed"
|
|
269
|
+
printf '%s\n' "$out" >&2
|
|
270
|
+
pipe_log "omp requires bun on PATH (npm install -g bun); claude requires node"
|
|
271
|
+
exit 1
|
|
272
|
+
fi
|
|
273
|
+
}
|
|
274
|
+
|
|
249
275
|
pipe_run_claude() {
|
|
250
276
|
# $1 = agent label for metrics, $2 = skill invocation (e.g. "/review-mr"),
|
|
251
277
|
# $3 = allowed tools (optional), $4 = model for the main loop (optional,
|
|
252
278
|
# e.g. $PIPE_MODEL_REVIEW; subagents keep their frontmatter models),
|
|
253
279
|
# stdin = extra prompt (optional, usually empty)
|
|
254
280
|
local label="$1" skill="$2" tools="${3:-Agent,Read,Write,Edit,Glob,Grep,Bash}" model="${4:-}"
|
|
281
|
+
pipe_require_harness claude
|
|
255
282
|
local stdin_file; stdin_file=$(mktemp)
|
|
256
283
|
cat > "$stdin_file" || true
|
|
257
284
|
|
|
@@ -350,6 +377,7 @@ pipe_run_omp() {
|
|
|
350
377
|
# frontmatter), stdin = extra prompt (optional, usually empty).
|
|
351
378
|
local label="$1" skill="$2" tools="${3:-Agent,Read,Write,Edit,Glob,Grep,Bash}" model="${4:-}"
|
|
352
379
|
local omp_tools; omp_tools=$(pipe_translate_tools_to_omp "$tools")
|
|
380
|
+
pipe_require_harness omp
|
|
353
381
|
local stdin_file; stdin_file=$(mktemp)
|
|
354
382
|
cat > "$stdin_file" || true
|
|
355
383
|
|
|
@@ -87,6 +87,100 @@ emit_decision_metric() {
|
|
|
87
87
|
'{issue_iid: $i, verdict: $v, decision_type: $dt, reason: $r} + $x')"
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
dispatch_restore_ready() {
|
|
91
|
+
# $1=iid. Best-effort recovery back to ready after a mid-dispatch failure.
|
|
92
|
+
# Nothing further can be attempted from this runner if this also fails, so
|
|
93
|
+
# it logs loudly and meters a terminal, distinctly countable label-error.
|
|
94
|
+
if issue_set_labels "$1" "$PIPE_LABEL_READY" "$PIPE_LABEL_WIP"; then
|
|
95
|
+
return 0
|
|
96
|
+
fi
|
|
97
|
+
pipe_log " ERROR: could not restore $PIPE_LABEL_READY on #$1; lifecycle labels may be inconsistent, manual check required"
|
|
98
|
+
emit_decision_metric "$1" skipped label-error "Failed to restore ready after a dispatch-time error"
|
|
99
|
+
return 1
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
dispatch_queued_issue() {
|
|
103
|
+
# $1=iid $2=branch. Claim atomically, then re-check repository state at the
|
|
104
|
+
# last possible point before spending a coder invocation. Every subsequent
|
|
105
|
+
# label transition is checked: a silently failed transition is exactly how
|
|
106
|
+
# an issue gets stuck in wip or loses its ready label forever.
|
|
107
|
+
local iid="$1" branch="$2" open closer kind reference url reason comment_file
|
|
108
|
+
|
|
109
|
+
if ! issue_set_labels "$iid" "$PIPE_LABEL_WIP" "$PIPE_LABEL_READY"; then
|
|
110
|
+
pipe_log " ERROR: could not claim #$iid, skipping to avoid a double trigger"
|
|
111
|
+
emit_decision_metric "$iid" skipped claim-failed "Could not atomically claim the ready issue"
|
|
112
|
+
return 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
if ! open=$(issue_blockers_open_count "$iid"); then
|
|
116
|
+
reason="Could not verify whether issue prerequisites are still open"
|
|
117
|
+
pipe_log " ERROR: $reason for #$iid; restoring $PIPE_LABEL_READY"
|
|
118
|
+
dispatch_restore_ready "$iid" || return 0
|
|
119
|
+
emit_decision_metric "$iid" deferred blocker-validation-error "$reason"
|
|
120
|
+
return 0
|
|
121
|
+
fi
|
|
122
|
+
if [ "${open:-1}" != "0" ]; then
|
|
123
|
+
reason="Issue has ${open:-unknown} open prerequisite(s)"
|
|
124
|
+
if issue_set_labels "$iid" "$PIPE_LABEL_BLOCKED" "$PIPE_LABEL_WIP,$PIPE_LABEL_READY"; then
|
|
125
|
+
pipe_log " #$iid returned to $PIPE_LABEL_BLOCKED: $reason"
|
|
126
|
+
emit_decision_metric "$iid" deferred blocked-prerequisite "$reason"
|
|
127
|
+
else
|
|
128
|
+
pipe_log " ERROR: could not move #$iid to $PIPE_LABEL_BLOCKED, restoring $PIPE_LABEL_READY: $reason"
|
|
129
|
+
dispatch_restore_ready "$iid" || return 0
|
|
130
|
+
emit_decision_metric "$iid" deferred blocked-transition-error "$reason"
|
|
131
|
+
fi
|
|
132
|
+
return 0
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if ! closer=$(issue_merged_closer "$iid"); then
|
|
136
|
+
reason="Could not verify whether a closing MR/PR has already merged"
|
|
137
|
+
pipe_log " ERROR: $reason for #$iid; restoring $PIPE_LABEL_READY"
|
|
138
|
+
dispatch_restore_ready "$iid" || return 0
|
|
139
|
+
emit_decision_metric "$iid" deferred validation-error "$reason"
|
|
140
|
+
return 0
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
if [ -n "$closer" ]; then
|
|
144
|
+
kind=$(jq -r '.kind' <<< "$closer")
|
|
145
|
+
reference=$(jq -r '.reference' <<< "$closer")
|
|
146
|
+
url=$(jq -r '.url' <<< "$closer")
|
|
147
|
+
reason="$kind $reference already merged into $PIPE_TARGET_BRANCH"
|
|
148
|
+
comment_file="$PIPE_CONTEXT_DIR/orchestrator-already-merged-$iid.md"
|
|
149
|
+
{
|
|
150
|
+
printf '**Dispatch skipped:** %s %s has already merged into `%s`.\n\n' \
|
|
151
|
+
"$kind" "$reference" "$PIPE_TARGET_BRANCH"
|
|
152
|
+
printf '%s\n\n' "$url"
|
|
153
|
+
printf 'The pipeline removed `%s`; apply it again only if more work is required.\n' \
|
|
154
|
+
"$PIPE_LABEL_READY"
|
|
155
|
+
} > "$comment_file"
|
|
156
|
+
# Comment before the final label removal: if the comment never lands,
|
|
157
|
+
# the ready label must survive so the skip is retried and the change is
|
|
158
|
+
# named next time, instead of silently vanishing off the board.
|
|
159
|
+
if ! issue_comment_file "$iid" "$comment_file"; then
|
|
160
|
+
pipe_log " ERROR: could not comment on #$iid before skipping; restoring $PIPE_LABEL_READY for retry"
|
|
161
|
+
dispatch_restore_ready "$iid" || return 0
|
|
162
|
+
emit_decision_metric "$iid" deferred comment-error "$reason"
|
|
163
|
+
return 0
|
|
164
|
+
fi
|
|
165
|
+
pipe_log " skipping coder for #$iid: $reason ($url)"
|
|
166
|
+
if ! issue_set_labels "$iid" "" "$PIPE_LABEL_WIP,$PIPE_LABEL_READY"; then
|
|
167
|
+
pipe_log " ERROR: could not clear lifecycle labels on #$iid after commenting; restoring $PIPE_LABEL_READY"
|
|
168
|
+
dispatch_restore_ready "$iid"
|
|
169
|
+
fi
|
|
170
|
+
emit_decision_metric "$iid" skipped already-merged "$reason" \
|
|
171
|
+
"$(jq -n --arg kind "$kind" --arg reference "$reference" --arg url "$url" \
|
|
172
|
+
'{closer_kind: $kind, closer_reference: $reference, closer_url: $url}')"
|
|
173
|
+
return 0
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
if code_trigger "$iid" "$branch"; then
|
|
177
|
+
pipe_log " triggered coder for #$iid on $branch"
|
|
178
|
+
else
|
|
179
|
+
pipe_log " ERROR: trigger failed for #$iid, reverting label"
|
|
180
|
+
dispatch_restore_ready "$iid"
|
|
181
|
+
fi
|
|
182
|
+
}
|
|
183
|
+
|
|
90
184
|
while IFS= read -r ISSUE; do
|
|
91
185
|
[ "${#QUEUE[@]}" -ge "$PIPE_CODER_CAP" ] && { pipe_log "Coder cap ($PIPE_CODER_CAP) reached, deferring the rest"; break; }
|
|
92
186
|
|
|
@@ -297,16 +391,5 @@ pipe_log "Triggering ${#QUEUE[@]} coder run(s)"
|
|
|
297
391
|
for ITEM in "${QUEUE[@]}"; do
|
|
298
392
|
IID="${ITEM%%:*}"
|
|
299
393
|
BRANCH="${ITEM#*:}"
|
|
300
|
-
|
|
301
|
-
# not double-trigger. Revert on trigger failure.
|
|
302
|
-
if issue_set_labels "$IID" "$PIPE_LABEL_WIP" "$PIPE_LABEL_READY"; then
|
|
303
|
-
if code_trigger "$IID" "$BRANCH"; then
|
|
304
|
-
pipe_log " triggered coder for #$IID on $BRANCH"
|
|
305
|
-
else
|
|
306
|
-
pipe_log " ERROR: trigger failed for #$IID, reverting label"
|
|
307
|
-
issue_set_labels "$IID" "$PIPE_LABEL_READY" "$PIPE_LABEL_WIP"
|
|
308
|
-
fi
|
|
309
|
-
else
|
|
310
|
-
pipe_log " ERROR: could not claim #$IID, skipping to avoid a double trigger"
|
|
311
|
-
fi
|
|
394
|
+
dispatch_queued_issue "$IID" "$BRANCH"
|
|
312
395
|
done
|