@plainconceptsplatform/workflows 1.18.0 → 1.19.0
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/loops/actions/capture-mockup/capture.mjs +7 -2
- package/loops/actions/decide-agent-retry/action.yml +44 -15
- package/loops/actions/decide-agent-retry/classify-failure.sh +47 -2
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +195 -1
- package/loops/docs/diagrams.md +6 -2
- package/loops/workflows/agent-apply-review.md +18 -3
- package/loops/workflows/agent-implement.md +18 -3
- package/loops/workflows/agent-merge-gate.md +4 -2
- package/loops/workflows/agent-refine.md +18 -3
- package/loops/workflows/agent-triage.md +18 -3
- package/loops/workflows/work-router.yml +7 -2
- package/package.json +1 -1
|
@@ -25,8 +25,13 @@ export const VIEWPORTS = [
|
|
|
25
25
|
const SECTION_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
26
26
|
const STATES = new Set(["default", "empty", "loading", "error", "success", "disabled", "mobile"]);
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Literal patterns only: a RegExp built from a variable is a blocking Semgrep finding
|
|
29
|
+
// (detect-non-literal-regexp) in every consumer that runs the family scan.
|
|
30
|
+
const ATTRIBUTE_PATTERNS = {
|
|
31
|
+
"data-mockup-section": /\bdata-mockup-section\s*=\s*["']([^"']*)["']/gi,
|
|
32
|
+
"data-component": /\bdata-component\s*=\s*["']([^"']*)["']/gi,
|
|
33
|
+
};
|
|
34
|
+
const attributeValues = (html, name) => [...html.matchAll(ATTRIBUTE_PATTERNS[name])].map((match) => match[1].trim());
|
|
30
35
|
|
|
31
36
|
// What the contract forbids, as the reasons a person reading the run would want.
|
|
32
37
|
export function htmlProblems(html) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/decide-agent-retry/action.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
2
|
name: Decide agent retry
|
|
3
|
-
description: Say whether an agent job that failed is worth running again, from how long it ran
|
|
3
|
+
description: Say whether an agent job that failed is worth running again, from how long it ran, how many attempts have been made, and which step it stopped in.
|
|
4
4
|
inputs:
|
|
5
5
|
token:
|
|
6
6
|
description: GitHub token with actions read access, used to read this run's own jobs.
|
|
@@ -26,8 +26,14 @@ outputs:
|
|
|
26
26
|
description: How long the agent job ran, or -1 when that could not be read.
|
|
27
27
|
value: ${{ steps.decide.outputs.minutes }}
|
|
28
28
|
cause:
|
|
29
|
-
description: 'Why a retry was chosen: short-run, runner-shutdown or cut-off. "none" when there is none.'
|
|
29
|
+
description: 'Why a retry was chosen: short-run, runner-shutdown or cut-off. "configuration" when a preflight step failed and nothing is retried; "cancelled" for a cancelled job; "none" when there is none.'
|
|
30
30
|
value: ${{ steps.decide.outputs.cause }}
|
|
31
|
+
failed-step:
|
|
32
|
+
description: The name of the first step the agent job failed in, or empty.
|
|
33
|
+
value: ${{ steps.decide.outputs.failed-step }}
|
|
34
|
+
reason:
|
|
35
|
+
description: For a configuration failure, one paragraph for the issue naming the step and the fix. Empty otherwise.
|
|
36
|
+
value: ${{ steps.decide.outputs.reason }}
|
|
31
37
|
runs:
|
|
32
38
|
using: composite
|
|
33
39
|
steps:
|
|
@@ -47,6 +53,12 @@ runs:
|
|
|
47
53
|
# apply-review parked instead -- an Odyssey refine died in three minutes on
|
|
48
54
|
# `Model 'glm-5-2' not found`, a gateway fault that clears in seconds, and waited on the
|
|
49
55
|
# janitor's six-hourly sweep. Five copies of sixty lines is how one rule becomes five.
|
|
56
|
+
#
|
|
57
|
+
# And one failure is never repeated, however short: a preflight step (the harness sync and
|
|
58
|
+
# doctor, the CI provider merge) that refused the repository's own setup. It ended a consumer's
|
|
59
|
+
# refine in a minute, which read as a short run, and five attempts were spent and commented as
|
|
60
|
+
# a provider outage when the fix was one missing file (#527). classify-failure.sh names it
|
|
61
|
+
# `configuration` from the failed step's name, and that verdict wins over the duration.
|
|
50
62
|
- name: Decide whether this failure is worth repeating
|
|
51
63
|
id: decide
|
|
52
64
|
shell: bash
|
|
@@ -72,24 +84,39 @@ runs:
|
|
|
72
84
|
[ -n "$attempts" ] || attempts=0
|
|
73
85
|
retry=false
|
|
74
86
|
cause=none
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
failed_step=""
|
|
88
|
+
reason=""
|
|
89
|
+
work="$(mktemp -d)"
|
|
90
|
+
# The reads are best effort: a step list, a log or an annotation list that cannot be
|
|
91
|
+
# fetched leaves the decision to the duration, exactly as before these checks existed.
|
|
92
|
+
if [ -n "${job_id:-}" ]; then
|
|
93
|
+
gh api "repos/$REPO/actions/jobs/$job_id" --jq '.steps[]? | select(.conclusion == "failure") | .name' >"$work/failed-steps" 2>/dev/null || true
|
|
94
|
+
failed_step="$(head -1 "$work/failed-steps" 2>/dev/null || true)"
|
|
79
95
|
fi
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# as before this check existed.
|
|
83
|
-
if [ "$retry" != true ] && [ "$attempts" -lt "$PARK_AT" ] && [ -n "${job_id:-}" ]; then
|
|
84
|
-
work="$(mktemp -d)"
|
|
96
|
+
kind=none
|
|
97
|
+
if [ -n "${job_id:-}" ] && [ "${conclusion:-}" != cancelled ]; then
|
|
85
98
|
gh api "repos/$REPO/check-runs/$job_id/annotations" --paginate --jq '.[].message' >"$work/annotations" 2>/dev/null || true
|
|
86
99
|
gh api "repos/$REPO/actions/jobs/$job_id/logs" >"$work/log" 2>/dev/null || true
|
|
87
|
-
kind="$(bash "$ACTION_PATH/classify-failure.sh" "$work/log" "$work/annotations" || echo none)"
|
|
88
|
-
|
|
100
|
+
kind="$(bash "$ACTION_PATH/classify-failure.sh" "$work/log" "$work/annotations" "$work/failed-steps" || echo none)"
|
|
101
|
+
fi
|
|
102
|
+
rm -rf "$work"
|
|
103
|
+
if [ "$kind" = configuration ]; then
|
|
104
|
+
# Not a provider outage and not the agent's verdict: the model never started. The next
|
|
105
|
+
# attempt would fail the same way in the same minute, so no attempt is spent on it.
|
|
106
|
+
cause=configuration
|
|
107
|
+
fix="$(bash "$ACTION_PATH/classify-failure.sh" --fix "$failed_step")"
|
|
108
|
+
reason="The agent job stopped in its preflight step **${failed_step}** before the model started. That is this repository's configuration, not a provider outage, so it was not retried. ${fix} Once that is fixed, start the work again."
|
|
109
|
+
else
|
|
110
|
+
# An unknown duration is treated as a long run: never retry on a guess.
|
|
111
|
+
if [ "$minutes" -ge 0 ] && [ "$minutes" -lt "$UNDER_MINUTES" ] && [ "$attempts" -lt "$PARK_AT" ]; then
|
|
112
|
+
retry=true
|
|
113
|
+
cause=short-run
|
|
114
|
+
fi
|
|
115
|
+
# A long run is repeated only when it was killed, not when it was wrong.
|
|
116
|
+
if [ "$retry" != true ] && [ "$attempts" -lt "$PARK_AT" ] && [ "$kind" != none ]; then
|
|
89
117
|
retry=true
|
|
90
118
|
cause="$kind"
|
|
91
119
|
fi
|
|
92
|
-
rm -rf "$work"
|
|
93
120
|
fi
|
|
94
121
|
# A cancelled agent job is a person or a newer run taking the work back, not an outage.
|
|
95
122
|
# Pliny-Bot #816: refine was cancelled by hand and its label removed, then this step read
|
|
@@ -104,5 +131,7 @@ runs:
|
|
|
104
131
|
echo "next=$((attempts + 1))"
|
|
105
132
|
echo "minutes=$minutes"
|
|
106
133
|
echo "cause=$cause"
|
|
134
|
+
echo "failed-step=$failed_step"
|
|
135
|
+
echo "reason=$reason"
|
|
107
136
|
} >> "$GITHUB_OUTPUT"
|
|
108
|
-
echo "agent job ran for ${minutes}m; attempts so far ${attempts}; retry=${retry}; cause=${cause}"
|
|
137
|
+
echo "agent job ran for ${minutes}m; attempts so far ${attempts}; failed step '${failed_step}'; retry=${retry}; cause=${cause}"
|
|
@@ -20,19 +20,64 @@
|
|
|
20
20
|
# shutdown is read from the annotations, which only GitHub writes, and the cut-off from the one line
|
|
21
21
|
# immediately above the sandbox teardown.
|
|
22
22
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
23
|
+
# One cause is the opposite, and repeating it is the wrong answer:
|
|
24
|
+
#
|
|
25
|
+
# configuration the job stopped in a preflight step before the model was ever started: the
|
|
26
|
+
# harness sync and doctor (`sync-agents.mjs --strict`, `harness-doctor.mjs
|
|
27
|
+
# --strict`) or the CI provider merge. Those read this repository's own files,
|
|
28
|
+
# so the next attempt fails the same way in the same minute. A consumer with no
|
|
29
|
+
# specialist `*-engineer.md` saw five of them, each commented as a provider
|
|
30
|
+
# outage (#527). It is read from the name of the step GitHub marked failed,
|
|
31
|
+
# which the jobs API reports and no tool output can forge.
|
|
32
|
+
#
|
|
33
|
+
# Usage: classify-failure.sh LOG_FILE ANNOTATIONS_FILE [FAILED_STEPS_FILE]
|
|
34
|
+
# classify-failure.sh --fix STEP_NAME
|
|
35
|
+
# The first form prints one word: runner-shutdown, configuration, cut-off or none. FAILED_STEPS_FILE
|
|
36
|
+
# holds the names of the job's failed steps, one per line. The second form prints what a person
|
|
37
|
+
# does about a configuration failure in that step.
|
|
38
|
+
|
|
39
|
+
# The fix sentences quote commands in backticks for Markdown, not for the shell.
|
|
40
|
+
# shellcheck disable=SC2016
|
|
25
41
|
|
|
26
42
|
set -euo pipefail
|
|
27
43
|
|
|
44
|
+
# The steps in loops/workflows/shared/opencode-ci.md that only fail on the repository's own setup.
|
|
45
|
+
# Kept here, next to the sentence that tells a person how to fix each, so the two cannot drift.
|
|
46
|
+
preflight_fix() {
|
|
47
|
+
case "$1" in
|
|
48
|
+
"Sync harness agents and print model routing")
|
|
49
|
+
echo 'Run `node .opencode/scripts/sync-agents.mjs --strict` and `node .opencode/scripts/harness-doctor.mjs --strict` locally and fix what they report. `no *-engineer.md templates found` means the repository has no specialist engineer yet: run `/make-engineer` from agent-harness and commit the result.'
|
|
50
|
+
;;
|
|
51
|
+
"Merge the CI-only OpenCode provider into opencode.jsonc")
|
|
52
|
+
echo 'The step error names the file: `opencode.ci.json` must exist and be valid JSON (`workflows update` installs it), and a tracked `opencode.jsonc` must be comment-free JSON.'
|
|
53
|
+
;;
|
|
54
|
+
*) return 1 ;;
|
|
55
|
+
esac
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if [ "${1:-}" = "--fix" ]; then
|
|
59
|
+
preflight_fix "${2:-}" || echo 'Read the failed step in the run log and fix the repository setup it reports.'
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
28
63
|
log="${1:-}"
|
|
29
64
|
annotations="${2:-}"
|
|
65
|
+
failed_steps="${3:-}"
|
|
30
66
|
|
|
31
67
|
if [ -n "$annotations" ] && [ -f "$annotations" ] && grep -qiE 'exit code 143|runner has received a shutdown signal|lost communication with the server|runner.*(was|has been) (deleted|terminated)' "$annotations"; then
|
|
32
68
|
echo runner-shutdown
|
|
33
69
|
exit 0
|
|
34
70
|
fi
|
|
35
71
|
|
|
72
|
+
if [ -n "$failed_steps" ] && [ -f "$failed_steps" ]; then
|
|
73
|
+
while IFS= read -r step; do
|
|
74
|
+
if preflight_fix "$step" >/dev/null; then
|
|
75
|
+
echo configuration
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
done < "$failed_steps"
|
|
79
|
+
fi
|
|
80
|
+
|
|
36
81
|
if [ -n "$log" ] && [ -f "$log" ]; then
|
|
37
82
|
# The line the sandbox teardown starts on. grep exits 1 for none, which is an answer here.
|
|
38
83
|
stop=$(grep -n '\[INFO\] Stopping containers' "$log" | head -1 | cut -d: -f1 || true)
|
|
@@ -25,6 +25,18 @@ MERGE_GATE_WORKER_MD="${WORKFLOWS_DIR}/agent-merge-gate.md"
|
|
|
25
25
|
router_env() {
|
|
26
26
|
sed -n "s/^ $1: *//p" "$ROUTER_YML" | head -1 | sed -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'$/\1/"
|
|
27
27
|
}
|
|
28
|
+
# The App's login is the repository variable BOT_LOGIN with the package default, written as the
|
|
29
|
+
# one expression `vars.BOT_LOGIN || 'platform-devbox[bot]'` wherever it is needed: the router's
|
|
30
|
+
# TRUSTED_BOTS, bot-approve's `if:` and every worker's GH_AW_ALLOWED_BOTS. Resolve it the way
|
|
31
|
+
# GitHub does, for a repository that sets the variable (pass the login) and one that does not
|
|
32
|
+
# (pass nothing). A `${{ }}` copy resolves to the login, a bare one in an `if:` to a quoted string.
|
|
33
|
+
resolve_bot_login() {
|
|
34
|
+
local login="${1:-}" with='\1'
|
|
35
|
+
[ -z "$login" ] || with="$login"
|
|
36
|
+
sed -E \
|
|
37
|
+
-e "s/\\\$\\{\\{ vars\\.BOT_LOGIN \\|\\| '([^']*)' \\}\\}/${with}/g" \
|
|
38
|
+
-e "s/vars\\.BOT_LOGIN \\|\\| '([^']*)'/'${with}'/g"
|
|
39
|
+
}
|
|
28
40
|
AUDIT_CRON="$(router_env AUDIT_CRON)"
|
|
29
41
|
DOCS_AUDIT_CRON="$(router_env DOCS_AUDIT_CRON)"
|
|
30
42
|
export AUDIT_CRON DOCS_AUDIT_CRON
|
|
@@ -396,7 +408,7 @@ fi
|
|
|
396
408
|
# Bot logins are the same shape of problem. Most sites read env.TRUSTED_BOTS, but a job-level
|
|
397
409
|
# `if:` cannot: GitHub does not expose the env context there, so bot-approve keeps literals and
|
|
398
410
|
# they have to agree. Assert every bot login written anywhere in the router is in the list.
|
|
399
|
-
trusted="$(router_env TRUSTED_BOTS)"
|
|
411
|
+
trusted="$(router_env TRUSTED_BOTS | resolve_bot_login)"
|
|
400
412
|
if [ -z "$trusted" ]; then
|
|
401
413
|
MIRROR_OK=0; echo "FAIL: work-router.yml defines no TRUSTED_BOTS in its env: block" >&2
|
|
402
414
|
else
|
|
@@ -2808,6 +2820,188 @@ assert "the hourly sweep's assume pass reaches refine in assume mode" assume \
|
|
|
2808
2820
|
|
|
2809
2821
|
if [ "$RETRY_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
2810
2822
|
|
|
2823
|
+
echo "── Bot login ─────────────────────────────────────────────────────────────"
|
|
2824
|
+
|
|
2825
|
+
# A repository with its own GitHub App was silently untrusted (#527): the App's bot-working label
|
|
2826
|
+
# reached the router from an actor the router did not know, `authorize` said false, call-refine
|
|
2827
|
+
# skipped, and the issue sat at `refine,bot-working` with no comment and no error. The login is
|
|
2828
|
+
# the repository variable BOT_LOGIN now, defaulting to platform-devbox[bot]. Each assertion here
|
|
2829
|
+
# runs the real router and worker text with the variable resolved both ways.
|
|
2830
|
+
BOT_OK=1
|
|
2831
|
+
CUSTOM_BOT='custom-app[bot]'
|
|
2832
|
+
|
|
2833
|
+
# One expression, one default. A second spelling is a second thing to keep in step, and a bare
|
|
2834
|
+
# literal is the hard-coded login coming back.
|
|
2835
|
+
bot_files=("$ROUTER_YML")
|
|
2836
|
+
for route in "${ALL_WORKER_ROUTES[@]}"; do
|
|
2837
|
+
if worker_installed "$route"; then bot_files+=("${WORKFLOWS_DIR}/agent-${route}.md"); fi
|
|
2838
|
+
done
|
|
2839
|
+
bot_exprs=$(cat "${bot_files[@]}" | grep -v '^ *#' | { grep -oE "vars\.BOT_LOGIN[^}')]*('[^']*')?" || true; } | sort -u)
|
|
2840
|
+
if [ -n "$bot_exprs" ] && [ "$bot_exprs" != "vars.BOT_LOGIN || 'platform-devbox[bot]'" ]; then
|
|
2841
|
+
BOT_OK=0
|
|
2842
|
+
echo "FAIL: BOT_LOGIN is spelled more than one way: $(printf '%s' "$bot_exprs" | tr '\n' ';')" >&2
|
|
2843
|
+
fi
|
|
2844
|
+
stray=$(grep -n 'platform-devbox\[bot\]' "${bot_files[@]}" | grep -vE ':[0-9]+: *#' |
|
|
2845
|
+
grep -vF "vars.BOT_LOGIN || 'platform-devbox[bot]'" || true)
|
|
2846
|
+
if [ -n "$stray" ]; then
|
|
2847
|
+
BOT_OK=0
|
|
2848
|
+
echo "FAIL: platform-devbox[bot] is hard-coded outside the BOT_LOGIN expression:" >&2
|
|
2849
|
+
printf '%s\n' "$stray" >&2
|
|
2850
|
+
fi
|
|
2851
|
+
|
|
2852
|
+
# The router's authorize step, executed. `gh` answers nothing, as it does for an App, which has
|
|
2853
|
+
# no collaborator permission, so only TRUSTED_BOTS can make it trusted.
|
|
2854
|
+
authorize_script=$(awk '
|
|
2855
|
+
/- name: Check the actor can start an agent/ { found = 1 }
|
|
2856
|
+
found && /^ +run: \|$/ { body = 1; next }
|
|
2857
|
+
body && /^ {0,9}[^ ]/ { exit }
|
|
2858
|
+
body { print substr($0, 11) }' "$ROUTER_YML")
|
|
2859
|
+
fake_bin="$(mktemp -d)"
|
|
2860
|
+
printf '#!/usr/bin/env bash\nexit 1\n' > "${fake_bin}/gh"
|
|
2861
|
+
chmod +x "${fake_bin}/gh"
|
|
2862
|
+
authorize_as() {
|
|
2863
|
+
local actor="$1" login="$2" out
|
|
2864
|
+
out="$(mktemp)"
|
|
2865
|
+
if ! PATH="${fake_bin}:$PATH" GITHUB_OUTPUT="$out" GITHUB_REPOSITORY=o/r EVENT=issues ACTOR="$actor" \
|
|
2866
|
+
TRUSTED_BOTS="$(router_env TRUSTED_BOTS | resolve_bot_login "$login")" \
|
|
2867
|
+
bash -c "$authorize_script" >/dev/null 2>&1; then
|
|
2868
|
+
echo error
|
|
2869
|
+
else
|
|
2870
|
+
sed -n 's/^trusted=//p' "$out"
|
|
2871
|
+
fi
|
|
2872
|
+
rm -f "$out"
|
|
2873
|
+
}
|
|
2874
|
+
if [ -z "$authorize_script" ]; then
|
|
2875
|
+
BOT_OK=0
|
|
2876
|
+
echo "FAIL: could not extract the authorize step from work-router.yml" >&2
|
|
2877
|
+
else
|
|
2878
|
+
assert "default config still trusts platform-devbox[bot]" true "$(authorize_as 'platform-devbox[bot]' '')"
|
|
2879
|
+
assert "default config does not trust an unknown App" false "$(authorize_as "$CUSTOM_BOT" '')"
|
|
2880
|
+
assert "BOT_LOGIN makes the router trust that App" true "$(authorize_as "$CUSTOM_BOT" "$CUSTOM_BOT")"
|
|
2881
|
+
fi
|
|
2882
|
+
rm -rf "$fake_bin"
|
|
2883
|
+
# And the trusted App's hand-off reaches refine: the classifier routes it, the caller requires the
|
|
2884
|
+
# trust asserted above, and the worker's pre_activation admits the same login.
|
|
2885
|
+
assert_route "a trusted custom App's bot-working label routes refine" refine \
|
|
2886
|
+
EVENT=issues ACTION=labeled LABEL=bot-working ACTOR="$CUSTOM_BOT" \
|
|
2887
|
+
'ISSUE_LABELS=["refine","bot-working"]' EVENT_ISSUE_NUMBER=42
|
|
2888
|
+
if ! grep -qE "^ if: needs\.classify\.outputs\.route == 'refine' && needs\.authorize\.outputs\.trusted == 'true'" "$ROUTER_YML"; then
|
|
2889
|
+
BOT_OK=0
|
|
2890
|
+
echo "FAIL: call-refine no longer gates on authorize.outputs.trusted" >&2
|
|
2891
|
+
fi
|
|
2892
|
+
for route in "${ALL_WORKER_ROUTES[@]}"; do
|
|
2893
|
+
worker_md="${WORKFLOWS_DIR}/agent-${route}.md"
|
|
2894
|
+
[ -f "$worker_md" ] || continue
|
|
2895
|
+
allowed=$(sed -n 's/^ GH_AW_ALLOWED_BOTS: *//p' "$worker_md" | head -1)
|
|
2896
|
+
[ -n "$allowed" ] || continue
|
|
2897
|
+
case ",$(printf '%s' "$allowed" | resolve_bot_login | tr -d '"')," in
|
|
2898
|
+
*",platform-devbox[bot],"*) ;;
|
|
2899
|
+
*) BOT_OK=0; echo "FAIL: agent-${route} no longer admits platform-devbox[bot] by default" >&2 ;;
|
|
2900
|
+
esac
|
|
2901
|
+
case ",$(printf '%s' "$allowed" | resolve_bot_login "$CUSTOM_BOT" | tr -d '"')," in
|
|
2902
|
+
*",${CUSTOM_BOT},"*) ;;
|
|
2903
|
+
*) BOT_OK=0; echo "FAIL: agent-${route} GH_AW_ALLOWED_BOTS ignores BOT_LOGIN, so pre_activation rejects that App" >&2 ;;
|
|
2904
|
+
esac
|
|
2905
|
+
done
|
|
2906
|
+
|
|
2907
|
+
# bot-approve's `if:` cannot read env, so it reads the variable itself.
|
|
2908
|
+
approve_if=$(awk '/^ bot-approve:$/{found=1} found && /^ if: >$/{body=1; next} body && /^ [a-z-]+:/{exit} body{print}' "$ROUTER_YML")
|
|
2909
|
+
if ! printf '%s' "$approve_if" | resolve_bot_login | grep -qF "github.actor == ('platform-devbox[bot]')"; then
|
|
2910
|
+
BOT_OK=0
|
|
2911
|
+
echo "FAIL: bot-approve no longer accepts platform-devbox[bot] by default" >&2
|
|
2912
|
+
fi
|
|
2913
|
+
if ! printf '%s' "$approve_if" | resolve_bot_login "$CUSTOM_BOT" | grep -qF "github.actor == ('${CUSTOM_BOT}')"; then
|
|
2914
|
+
BOT_OK=0
|
|
2915
|
+
echo "FAIL: bot-approve does not accept the App named by BOT_LOGIN" >&2
|
|
2916
|
+
fi
|
|
2917
|
+
|
|
2918
|
+
if [ "$BOT_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
2919
|
+
|
|
2920
|
+
echo "── Preflight failures are configuration ──────────────────────────────────"
|
|
2921
|
+
|
|
2922
|
+
# A preflight step that refused the repository's setup ended a consumer's refine in a minute; the
|
|
2923
|
+
# retry rule read a short run, commented "That is what a provider outage looks like" and spent all
|
|
2924
|
+
# five attempts (#527). It is classified from the failed step's name, never retried, and reported
|
|
2925
|
+
# with the step and the fix.
|
|
2926
|
+
PREFLIGHT_OK=1
|
|
2927
|
+
RETRY_DIR="${HERE}/../decide-agent-retry"
|
|
2928
|
+
classify_steps() {
|
|
2929
|
+
local steps annotations
|
|
2930
|
+
steps="$(mktemp)"; annotations="$(mktemp)"
|
|
2931
|
+
printf '%s\n' "$1" > "$steps"
|
|
2932
|
+
printf '%s\n' "${2:-}" > "$annotations"
|
|
2933
|
+
bash "${RETRY_DIR}/classify-failure.sh" "" "$annotations" "$steps"
|
|
2934
|
+
rm -f "$steps" "$annotations"
|
|
2935
|
+
}
|
|
2936
|
+
assert "a failed harness sync is a configuration failure" configuration \
|
|
2937
|
+
"$(classify_steps 'Sync harness agents and print model routing')"
|
|
2938
|
+
assert "a failed CI provider merge is a configuration failure" configuration \
|
|
2939
|
+
"$(classify_steps 'Merge the CI-only OpenCode provider into opencode.jsonc')"
|
|
2940
|
+
assert "a failed agent step is not a configuration failure" none \
|
|
2941
|
+
"$(classify_steps 'Execute OpenCode CLI')"
|
|
2942
|
+
assert "a runner shutdown still wins over the step it interrupted" runner-shutdown \
|
|
2943
|
+
"$(classify_steps 'Sync harness agents and print model routing' 'Process completed with exit code 143.')"
|
|
2944
|
+
if ! bash "${RETRY_DIR}/classify-failure.sh" --fix 'Sync harness agents and print model routing' | grep -qF '/make-engineer'; then
|
|
2945
|
+
PREFLIGHT_OK=0
|
|
2946
|
+
echo "FAIL: the harness sync fix no longer names /make-engineer" >&2
|
|
2947
|
+
fi
|
|
2948
|
+
# The names are matched exactly, so a renamed step in the shared setup disables the rule silently.
|
|
2949
|
+
while IFS= read -r step; do
|
|
2950
|
+
[ -n "$step" ] || continue
|
|
2951
|
+
if ! grep -qF -- "- name: ${step}" "${WORKFLOWS_DIR}/shared/opencode-ci.md"; then
|
|
2952
|
+
PREFLIGHT_OK=0
|
|
2953
|
+
echo "FAIL: classify-failure.sh names preflight step '${step}', which shared/opencode-ci.md no longer has" >&2
|
|
2954
|
+
fi
|
|
2955
|
+
done < <(sed -n 's/^ "\(.*\)")$/\1/p' "${RETRY_DIR}/classify-failure.sh")
|
|
2956
|
+
|
|
2957
|
+
# The decision itself, executed with a fake `gh`: a one-minute run that died in the harness sync
|
|
2958
|
+
# is not retried even with every attempt left, and the same minute dying in the agent step is.
|
|
2959
|
+
decide_script=$(awk '/^ run: \|$/{body=1; next} body{print substr($0, 9)}' "${RETRY_DIR}/action.yml")
|
|
2960
|
+
decide_with_failed_step() {
|
|
2961
|
+
local step="$1" bin out
|
|
2962
|
+
bin="$(mktemp -d)"; out="$(mktemp)"
|
|
2963
|
+
{
|
|
2964
|
+
echo '#!/usr/bin/env bash'
|
|
2965
|
+
echo 'case "$*" in'
|
|
2966
|
+
echo ' *"/runs/"*"/jobs"*) echo "7 2026-01-01T00:00:00Z 2026-01-01T00:01:00Z failure" ;;'
|
|
2967
|
+
echo " *\"/actions/jobs/7 \"*) echo \"${step}\" ;;"
|
|
2968
|
+
echo ' *) exit 0 ;;'
|
|
2969
|
+
echo 'esac'
|
|
2970
|
+
} > "${bin}/gh"
|
|
2971
|
+
chmod +x "${bin}/gh"
|
|
2972
|
+
PATH="${bin}:$PATH" GITHUB_OUTPUT="$out" REPO=o/r RUN_ID=1 ACTION_PATH="$RETRY_DIR" \
|
|
2973
|
+
ATTEMPTS=0 PARK_AT=5 UNDER_MINUTES=10 bash -c "$decide_script" >/dev/null 2>&1 || echo "error=true" >> "$out"
|
|
2974
|
+
tr '\n' ' ' < "$out"
|
|
2975
|
+
rm -rf "$bin" "$out"
|
|
2976
|
+
}
|
|
2977
|
+
config_decision=$(decide_with_failed_step 'Sync harness agents and print model routing')
|
|
2978
|
+
case "$config_decision" in
|
|
2979
|
+
*"retry=false "*"cause=configuration "*"failed-step=Sync harness agents and print model routing "*"reason=The agent job stopped in its preflight step"*) PASS=$((PASS + 1)) ;;
|
|
2980
|
+
*) FAIL=$((FAIL + 1)); echo "FAIL: a failed harness sync is not reported as configuration without a retry: ${config_decision}" >&2 ;;
|
|
2981
|
+
esac
|
|
2982
|
+
outage_decision=$(decide_with_failed_step 'Execute OpenCode CLI')
|
|
2983
|
+
case "$outage_decision" in
|
|
2984
|
+
*"retry=true "*"cause=short-run "*) PASS=$((PASS + 1)) ;;
|
|
2985
|
+
*) FAIL=$((FAIL + 1)); echo "FAIL: a short run that died in the agent step is no longer retried: ${outage_decision}" >&2 ;;
|
|
2986
|
+
esac
|
|
2987
|
+
|
|
2988
|
+
# Every worker that uses the rule reports the configuration failure, keeps the outage wording off
|
|
2989
|
+
# it, and parks it without `stalled`, which the janitor would retry into the same failure.
|
|
2990
|
+
for route in refine implement triage apply-review; do
|
|
2991
|
+
worker_md="${WORKFLOWS_DIR}/agent-${route}.md"
|
|
2992
|
+
[ -f "$worker_md" ] || continue
|
|
2993
|
+
if ! grep -qF "steps.decide.outputs.cause == 'configuration'" "$worker_md" ||
|
|
2994
|
+
! grep -qF '${{ steps.decide.outputs.reason }}' "$worker_md"; then
|
|
2995
|
+
PREFLIGHT_OK=0
|
|
2996
|
+
echo "FAIL: agent-${route} does not report a configuration failure with its step and fix" >&2
|
|
2997
|
+
fi
|
|
2998
|
+
if ! grep -qF "steps.decide.outputs.cause != 'configuration' && env.STALLED_LABEL" "$worker_md"; then
|
|
2999
|
+
PREFLIGHT_OK=0
|
|
3000
|
+
echo "FAIL: agent-${route} flags a configuration failure as stalled, so the janitor retries it" >&2
|
|
3001
|
+
fi
|
|
3002
|
+
done
|
|
3003
|
+
if [ "$PREFLIGHT_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
3004
|
+
|
|
2811
3005
|
echo "── Ways a run ends with nothing ──────────────────────────────────────────"
|
|
2812
3006
|
|
|
2813
3007
|
# A run can end without a pull request in five ways and they need different answers. One sentence
|
package/loops/docs/diagrams.md
CHANGED
|
@@ -336,10 +336,12 @@ flowchart TD
|
|
|
336
336
|
implRetry{"Died under 6 minutes<br/>with no answer?"}
|
|
337
337
|
implRetry -->|"yes: provider outage"| implAgain(("Retry<br/>attempt N of 5, label kept"))
|
|
338
338
|
implRetry -.->|"no: it answered, wrongly"| implFail
|
|
339
|
+
implRetry -.->|"no: a preflight step refused<br/>the repository's setup"| implConfig
|
|
339
340
|
implAgain -->|"re-enters via the router"| implStart
|
|
340
341
|
implHandoff(("Handed off<br/>PR open, bot-working removed,<br/>the gate decides next"))
|
|
341
342
|
implIdle(("Idle<br/>closed, or held by future"))
|
|
342
343
|
implFail(("Parked<br/>review added, a rerun<br/>would fail the same way"))
|
|
344
|
+
implConfig(("Parked as configuration<br/>review, no stalled, one comment<br/>naming the step and the fix"))
|
|
343
345
|
|
|
344
346
|
implStackStart("check-stack-branch<br/>(router job, rung 1)<br/>the issue carries<br/>stack-root-branch?") -.->|"no: flat mode,<br/>nothing happens"| implIdle
|
|
345
347
|
implStackStart -->|"yes: the workflow dispatches<br/>this worker with ref: feat/N<br/>so the agent never fetches"| implStart
|
|
@@ -354,14 +356,16 @@ flowchart TD
|
|
|
354
356
|
class implReserve,implFacts,implTodos,implCode,implPr,implStackStart action
|
|
355
357
|
class implEligible,implCheck,implVerify,implRetry decision
|
|
356
358
|
class implIdle idle
|
|
357
|
-
class implFail failure
|
|
359
|
+
class implFail,implConfig failure
|
|
358
360
|
class implHandoff,implAgain success
|
|
359
361
|
```
|
|
360
362
|
|
|
361
363
|
The retry belt exists because a provider outage kills a run in a couple of minutes with no answer,
|
|
362
364
|
and that used to burn the issue and hand it to a human. A run that worked for half an hour and then
|
|
363
365
|
failed produced an answer that was wrong; repeating it costs the whole fleet the same half hour to
|
|
364
|
-
be wrong again, so only the short deaths are retried.
|
|
366
|
+
be wrong again, so only the short deaths are retried. A short death in a preflight step (the harness
|
|
367
|
+
sync and doctor, the CI provider merge) is the exception: it is the repository's setup, not the
|
|
368
|
+
provider, so it is reported as a configuration failure with the step and the fix and never retried.
|
|
365
369
|
|
|
366
370
|
A stacked child enters the same worker through a different door: the router's `check-stack-branch`
|
|
367
371
|
job reads the issue's markers before anything runs, and dispatches the compiled workflow with
|
|
@@ -37,7 +37,9 @@ env:
|
|
|
37
37
|
RETRY_COMMENT: "That is what a provider outage looks like -- the run ended before it could produce an answer -- so this is being tried again from the start. It is a fresh run rather than a continuation: nothing is carried over from the attempt that failed."
|
|
38
38
|
INCOMPLETE_COMMENT: "Applying the review feedback ended without an outcome. This worker has no retry of its own: it runs again when somebody reviews or comments on the pull request, and the issue is flagged so it is not lost until then."
|
|
39
39
|
ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/issue-context.json
|
|
40
|
-
|
|
40
|
+
# The bot actors pre_activation admits. BOT_LOGIN is the repository variable naming the App's
|
|
41
|
+
# `<slug>[bot]` login; unset, it is platform-devbox[bot]. The router trusts the same login.
|
|
42
|
+
GH_AW_ALLOWED_BOTS: "${{ vars.BOT_LOGIN || 'platform-devbox[bot]' }},github-actions[bot]"
|
|
41
43
|
GIT_AUTHOR_NAME: "github-actions[bot]"
|
|
42
44
|
GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
|
|
43
45
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
|
@@ -383,6 +385,9 @@ jobs:
|
|
|
383
385
|
token: ${{ steps.app-token.outputs.token }}
|
|
384
386
|
issue-number: ${{ needs.subject.outputs.issue }}
|
|
385
387
|
labels: ${{ env.WORKING_LABEL }}
|
|
388
|
+
# A configuration failure is parked for a person without `stalled`: the janitor retries
|
|
389
|
+
# stalled work, and a preflight step that refused the repository's setup fails the same way
|
|
390
|
+
# on every retry until somebody changes the repository (#527).
|
|
386
391
|
- name: Flag for human review
|
|
387
392
|
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
388
393
|
uses: ./.github/actions/add-issue-labels
|
|
@@ -391,9 +396,19 @@ jobs:
|
|
|
391
396
|
issue-number: ${{ needs.subject.outputs.issue }}
|
|
392
397
|
labels: |-
|
|
393
398
|
${{ env.REVIEW_LABEL }}
|
|
394
|
-
${{ env.STALLED_LABEL }}
|
|
399
|
+
${{ steps.decide.outputs.cause != 'configuration' && env.STALLED_LABEL || '' }}
|
|
400
|
+
- name: Report the configuration failure
|
|
401
|
+
if: steps.decide.outputs.cause == 'configuration'
|
|
402
|
+
uses: ./.github/actions/create-issue-comment
|
|
403
|
+
with:
|
|
404
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
405
|
+
issue-number: ${{ needs.subject.outputs.issue }}
|
|
406
|
+
body: |
|
|
407
|
+
${{ env.REVIEW_MARKER }}
|
|
408
|
+
${{ steps.decide.outputs.reason }}
|
|
409
|
+
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
395
410
|
- name: Report missing review feedback outcome
|
|
396
|
-
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
411
|
+
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled' && steps.decide.outputs.cause != 'configuration'
|
|
397
412
|
uses: ./.github/actions/create-issue-comment
|
|
398
413
|
with:
|
|
399
414
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -83,7 +83,9 @@ env:
|
|
|
83
83
|
# copied here before the agent starts. Context only: the prompt says how it may be used.
|
|
84
84
|
MOCKUP_BRANCH: mockups
|
|
85
85
|
MOCKUP_DIR: /tmp/gh-aw/agent/mockup
|
|
86
|
-
|
|
86
|
+
# The bot actors pre_activation admits. BOT_LOGIN is the repository variable naming the App's
|
|
87
|
+
# `<slug>[bot]` login; unset, it is platform-devbox[bot]. The router trusts the same login.
|
|
88
|
+
GH_AW_ALLOWED_BOTS: "${{ vars.BOT_LOGIN || 'platform-devbox[bot]' }},github-actions[bot]"
|
|
87
89
|
description: |
|
|
88
90
|
Implements an issue and opens a pull request. Stops there: the merge decision belongs to
|
|
89
91
|
`agent-merge-gate.md`, which runs once CI has reported. Replaces the `impl-*` chain in
|
|
@@ -617,6 +619,9 @@ jobs:
|
|
|
617
619
|
labels: |
|
|
618
620
|
${{ env.WORKING_LABEL }}
|
|
619
621
|
implement
|
|
622
|
+
# A configuration failure is parked for a person without `stalled`: the janitor retries
|
|
623
|
+
# stalled work, and a preflight step that refused the repository's setup fails the same way
|
|
624
|
+
# on every retry until somebody changes the repository (#527).
|
|
620
625
|
- name: Flag for human review
|
|
621
626
|
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
622
627
|
uses: ./.github/actions/add-issue-labels
|
|
@@ -625,9 +630,19 @@ jobs:
|
|
|
625
630
|
issue-number: ${{ inputs.issue-number }}
|
|
626
631
|
labels: |-
|
|
627
632
|
${{ env.REVIEW_LABEL }}
|
|
628
|
-
${{ env.STALLED_LABEL }}
|
|
633
|
+
${{ steps.decide.outputs.cause != 'configuration' && env.STALLED_LABEL || '' }}
|
|
634
|
+
- name: Report the configuration failure
|
|
635
|
+
if: steps.decide.outputs.cause == 'configuration'
|
|
636
|
+
uses: ./.github/actions/create-issue-comment
|
|
637
|
+
with:
|
|
638
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
639
|
+
issue-number: ${{ inputs.issue-number }}
|
|
640
|
+
body: |
|
|
641
|
+
${{ env.IMPLEMENT_MARKER }}
|
|
642
|
+
${{ steps.decide.outputs.reason }}
|
|
643
|
+
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
629
644
|
- name: Report missing implementation outcome
|
|
630
|
-
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
645
|
+
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled' && steps.decide.outputs.cause != 'configuration'
|
|
631
646
|
uses: ./.github/actions/create-issue-comment
|
|
632
647
|
with:
|
|
633
648
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -105,10 +105,12 @@ env:
|
|
|
105
105
|
# one unusable report can keep every other bot pull request in the repository waiting.
|
|
106
106
|
PARK_AT_UNUSABLE_OUTPUT: "2"
|
|
107
107
|
ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/issue-context.json
|
|
108
|
-
|
|
108
|
+
# The bot actors pre_activation admits. BOT_LOGIN is the repository variable naming the App's
|
|
109
|
+
# `<slug>[bot]` login; unset, it is platform-devbox[bot]. The router trusts the same login.
|
|
110
|
+
GH_AW_ALLOWED_BOTS: "${{ vars.BOT_LOGIN || 'platform-devbox[bot]' }},github-actions[bot]"
|
|
109
111
|
# The bots whose pull requests the gate takes, in every spelling GitHub uses for one account.
|
|
110
112
|
# Any other author needs write access to the repository, as they would to push the branch.
|
|
111
|
-
TRUSTED_BOTS: "github-actions[bot] app/github-actions platform-devbox[bot]"
|
|
113
|
+
TRUSTED_BOTS: "github-actions[bot] app/github-actions ${{ vars.BOT_LOGIN || 'platform-devbox[bot]' }}"
|
|
112
114
|
GIT_AUTHOR_NAME: "github-actions[bot]"
|
|
113
115
|
GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
|
|
114
116
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
|
@@ -92,7 +92,9 @@ env:
|
|
|
92
92
|
INCOMPLETE_COMMENT: "Automated refinement ended without an outcome. The refine label remains for a retry."
|
|
93
93
|
SAFE_OUTPUT_COMMENT_PREFIX: "Refinement update"
|
|
94
94
|
ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/issue-context.json
|
|
95
|
-
|
|
95
|
+
# The bot actors pre_activation admits. BOT_LOGIN is the repository variable naming the App's
|
|
96
|
+
# `<slug>[bot]` login; unset, it is platform-devbox[bot]. The router trusts the same login.
|
|
97
|
+
GH_AW_ALLOWED_BOTS: "${{ vars.BOT_LOGIN || 'platform-devbox[bot]' }},github-actions[bot]"
|
|
96
98
|
REFINE_ISSUE_PATH: /tmp/gh-aw/refine-issue.json
|
|
97
99
|
REFINE_COMMENTS_PATH: /tmp/gh-aw/refine-comments.json
|
|
98
100
|
GIT_AUTHOR_NAME: "github-actions[bot]"
|
|
@@ -939,6 +941,9 @@ jobs:
|
|
|
939
941
|
token: ${{ steps.app-token.outputs.token }}
|
|
940
942
|
issue-number: ${{ inputs.issue-number }}
|
|
941
943
|
labels: ${{ env.WORKING_LABEL }}
|
|
944
|
+
# A configuration failure is parked for a person without `stalled`: the janitor retries
|
|
945
|
+
# stalled work, and a preflight step that refused the repository's setup fails the same way
|
|
946
|
+
# on every retry until somebody changes the repository (#527).
|
|
942
947
|
- name: Flag for human review
|
|
943
948
|
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
944
949
|
uses: ./.github/actions/add-issue-labels
|
|
@@ -947,9 +952,19 @@ jobs:
|
|
|
947
952
|
issue-number: ${{ inputs.issue-number }}
|
|
948
953
|
labels: |-
|
|
949
954
|
${{ env.REVIEW_LABEL }}
|
|
950
|
-
${{ env.STALLED_LABEL }}
|
|
955
|
+
${{ steps.decide.outputs.cause != 'configuration' && env.STALLED_LABEL || '' }}
|
|
956
|
+
- name: Report the configuration failure
|
|
957
|
+
if: steps.decide.outputs.cause == 'configuration'
|
|
958
|
+
uses: ./.github/actions/create-issue-comment
|
|
959
|
+
with:
|
|
960
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
961
|
+
issue-number: ${{ inputs.issue-number }}
|
|
962
|
+
body: |
|
|
963
|
+
${{ env.REFINE_MARKER }}
|
|
964
|
+
${{ steps.decide.outputs.reason }}
|
|
965
|
+
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
951
966
|
- name: Report missing refinement outcome
|
|
952
|
-
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
967
|
+
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled' && steps.decide.outputs.cause != 'configuration'
|
|
953
968
|
uses: ./.github/actions/create-issue-comment
|
|
954
969
|
with:
|
|
955
970
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -45,7 +45,9 @@ env:
|
|
|
45
45
|
SAFE_OUTPUT_COMMENT_PREFIX: "Triage assessment"
|
|
46
46
|
ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/issue-context.json
|
|
47
47
|
OPEN_ISSUES_PATH: /tmp/gh-aw/agent/open-issues.json
|
|
48
|
-
|
|
48
|
+
# The bot actors pre_activation admits. BOT_LOGIN is the repository variable naming the App's
|
|
49
|
+
# `<slug>[bot]` login; unset, it is platform-devbox[bot]. The router trusts the same login.
|
|
50
|
+
GH_AW_ALLOWED_BOTS: "${{ vars.BOT_LOGIN || 'platform-devbox[bot]' }},github-actions[bot]"
|
|
49
51
|
GIT_AUTHOR_NAME: "github-actions[bot]"
|
|
50
52
|
GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
|
|
51
53
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
|
@@ -389,6 +391,9 @@ jobs:
|
|
|
389
391
|
token: ${{ steps.app-token.outputs.token }}
|
|
390
392
|
issue-number: ${{ inputs.issue-number }}
|
|
391
393
|
labels: ${{ env.WORKING_LABEL }}
|
|
394
|
+
# A configuration failure is parked for a person without `stalled`: the janitor retries
|
|
395
|
+
# stalled work, and a preflight step that refused the repository's setup fails the same way
|
|
396
|
+
# on every retry until somebody changes the repository (#527).
|
|
392
397
|
- name: Flag for human review
|
|
393
398
|
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
394
399
|
uses: ./.github/actions/add-issue-labels
|
|
@@ -397,9 +402,19 @@ jobs:
|
|
|
397
402
|
issue-number: ${{ inputs.issue-number }}
|
|
398
403
|
labels: |-
|
|
399
404
|
${{ env.REVIEW_LABEL }}
|
|
400
|
-
${{ env.STALLED_LABEL }}
|
|
405
|
+
${{ steps.decide.outputs.cause != 'configuration' && env.STALLED_LABEL || '' }}
|
|
406
|
+
- name: Report the configuration failure
|
|
407
|
+
if: steps.decide.outputs.cause == 'configuration'
|
|
408
|
+
uses: ./.github/actions/create-issue-comment
|
|
409
|
+
with:
|
|
410
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
411
|
+
issue-number: ${{ inputs.issue-number }}
|
|
412
|
+
body: |
|
|
413
|
+
${{ env.TRIAGE_MARKER }}
|
|
414
|
+
${{ steps.decide.outputs.reason }}
|
|
415
|
+
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
401
416
|
- name: Report missing triage outcome
|
|
402
|
-
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled'
|
|
417
|
+
if: steps.decide.outputs.retry != 'true' && steps.decide.outputs.cause != 'cancelled' && steps.decide.outputs.cause != 'configuration'
|
|
403
418
|
uses: ./.github/actions/create-issue-comment
|
|
404
419
|
with:
|
|
405
420
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -138,7 +138,12 @@ env:
|
|
|
138
138
|
# separated, and every login the same account can present: GitHub reports an App as
|
|
139
139
|
# `name[bot]` on some endpoints and `app/name` on others, so both spellings belong here.
|
|
140
140
|
# Adding a third bot used to mean finding four hand-written comparisons across this file.
|
|
141
|
-
|
|
141
|
+
# The App the repository's BOT_APP_ID belongs to is the repository variable BOT_LOGIN, its
|
|
142
|
+
# `<slug>[bot]` login; unset, it is platform-devbox[bot]. The same expression sits in
|
|
143
|
+
# bot-approve's `if:` and in every worker's GH_AW_ALLOWED_BOTS, so one variable moves all three.
|
|
144
|
+
# Without it a repository with its own App was silently untrusted: the App's bot-working label
|
|
145
|
+
# left `authorize` false and the worker never ran, with no comment and no error (#527).
|
|
146
|
+
TRUSTED_BOTS: "github-actions[bot] app/github-actions ${{ vars.BOT_LOGIN || 'platform-devbox[bot]' }}"
|
|
142
147
|
# How long an issue may sit parked with `stalled` before the janitor retries it, and how many
|
|
143
148
|
# times it tries before leaving the issue to a person. Only machine failures are retried; a
|
|
144
149
|
# verdict the agent reached on purpose is reported and never re-run, because re-running a
|
|
@@ -1215,7 +1220,7 @@ jobs:
|
|
|
1215
1220
|
needs: classify
|
|
1216
1221
|
if: >
|
|
1217
1222
|
needs.classify.outputs.route == 'bot-approve' &&
|
|
1218
|
-
(github.actor == 'app/github-actions' || github.actor == 'platform-devbox[bot]')
|
|
1223
|
+
(github.actor == 'app/github-actions' || github.actor == (vars.BOT_LOGIN || 'platform-devbox[bot]'))
|
|
1219
1224
|
runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
|
|
1220
1225
|
timeout-minutes: 5
|
|
1221
1226
|
permissions:
|