-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(autofix): keep the round status comment live during long rounds #9771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
ebcc88d
7fc3fd5
e0c4c9c
32ac987
114af68
b34aae0
ae2c682
ccbd641
0b0279e
3c12d96
895ea67
6cb68dd
190847f
a36c121
edeb7ed
aac8a2a
6022bd4
8375fca
81b311a
9f78012
31065a6
a9f04a5
538f2db
578748b
62c3bbd
9bdf37f
b287dbb
dd5cb59
7ee39a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| #!/usr/bin/env bash | ||
| # Live-progress heartbeat for the autofix round status comment. | ||
| # | ||
| # A review-address round can run for hours (130-minute agent step, 330- | ||
| # minute job) while the PR's status comment stays frozen at "working" — | ||
| # a healthy long round and a dead one look identical on the PR page. | ||
| # 'Post autofix status comment' starts this script as a detached loop; | ||
| # every interval it re-PATCHes the SAME status comment with elapsed time | ||
| # and last agent activity, and 'Finalize autofix status comment' kills it | ||
| # before writing the terminal text. Full rationale → qwen-autofix.md#af-148. | ||
| # | ||
| # Subcommands: | ||
| # body — print the full bilingual working-state comment body to stdout. | ||
| # Used for the initial post AND by every loop tick, so the two | ||
| # can never drift apart. | ||
| # loop — sleep–compose–PATCH until killed or a self-exit bound trips. | ||
| # | ||
| # Environment (both): HB_ROUND (display round, already +1'd by the step), | ||
| # HB_CAP, HB_URL, HB_WORKDIR, HB_START_EPOCH; NOW_EPOCH overrides the | ||
| # clock for tests. loop additionally needs: HB_REPO, HB_COMMENT_ID, | ||
| # GITHUB_TOKEN for gh, and TRUSTED_PATH (the launcher's stage-time PATH | ||
| # capture the tick re-pins; a launch without it fails fast); | ||
| # HB_INTERVAL_SECONDS (default 600) and HB_MAX_AGE_SECONDS (default | ||
| # 20400) bound the pulse. | ||
| # | ||
| # Kill contract: the loop writes heartbeat.pid (diagnostics + its own | ||
| # self-exit check), checks heartbeat-stop, and exits on either signal or | ||
| # when its own age cap trips. The killers target the pid the launch | ||
| # recorded in EXPRESSION CONTEXT (steps.post_status.outputs.heartbeat_pid) | ||
| # — WORKDIR is sandbox-writable, so no WORKDIR file is ever read as a kill | ||
| # target — and kill the pid, its process group, AND its whole session: | ||
| # each tick's `timeout 60 gh` subtree runs in its OWN process group | ||
| # (coreutils timeout default) under the loop's setsid session, so a | ||
| # group/pid kill alone leaves it alive holding the PAT for up to 60s. The | ||
| # round's verification gate kills the loop before running any branch code | ||
| # on the host; finalize and the always() cleanup kill again. | ||
| # | ||
| # PAT note: the loop holds the bot PAT in its environment. Its lifetime is | ||
| # bounded to the sandboxed agent phase — the agent executes PR content only | ||
| # inside the docker sandbox there, so no fork code runs on the host beside | ||
| # this loop; the verification gate ends the loop BEFORE the first step that | ||
| # runs branch code on the host. Every gh call additionally runs under the | ||
| # af-112 hermetic pins (pinned GH_HOST, dropped GH_TOKEN/GH_ENTERPRISE_TOKEN, | ||
| # fresh GH_CONFIG_DIR), so a transport reroute planted in the shared HOME's | ||
| # gh config cannot intercept the token. See af-148 for the trade. | ||
|
|
||
| # -e is deliberately absent: the (( ... < 0 )) clamp guards exit non-zero | ||
| # on a false test and are load-bearing here. pipefail matches the sibling | ||
| # scripts' house line. | ||
| set -uo pipefail | ||
|
|
||
| MARKER='<!-- autofix-status -->' | ||
|
|
||
| require() { | ||
| local name | ||
| for name in "$@"; do | ||
| if [[ -z "${!name:-}" ]]; then | ||
| echo "autofix-status-heartbeat: ${name} is required" >&2 | ||
| exit 2 | ||
| fi | ||
| done | ||
| } | ||
|
|
||
| emit_body() { | ||
| require HB_ROUND HB_CAP HB_URL HB_WORKDIR HB_START_EPOCH | ||
| local now elapsed_min mtime active_min line_en line_zh | ||
| now="${NOW_EPOCH:-$(date +%s)}" | ||
| elapsed_min=$(( (now - HB_START_EPOCH) / 60 )) | ||
| (( elapsed_min < 0 )) && elapsed_min=0 | ||
| if [[ -f "${HB_WORKDIR}/agent.log" ]]; then | ||
| # date -r FILE reads the file's mtime on both GNU and BSD date. | ||
| mtime="$(date -r "${HB_WORKDIR}/agent.log" +%s 2>/dev/null || echo "${now}")" | ||
| active_min=$(( (now - mtime) / 60 )) | ||
| (( active_min < 0 )) && active_min=0 | ||
| line_en="⏱ Running for ${elapsed_min} min · agent active ${active_min} min ago" | ||
| line_zh="⏱ 已运行 ${elapsed_min} 分钟 · agent 最近活动在 ${active_min} 分钟前" | ||
| else | ||
| line_en="⏱ Running for ${elapsed_min} min · agent starting" | ||
| line_zh="⏱ 已运行 ${elapsed_min} 分钟 · agent 准备中" | ||
| fi | ||
| printf '%s\n\n🔄 **AutoFix is working on this PR** — round %s/%s. [Watch live progress](%s); this round posts its report here when it finishes.\n%s\n\n<details>\n<summary>中文说明</summary>\n\n🔄 **AutoFix 正在处理此 PR** —— 第 %s/%s 轮。[查看实时进度](%s);本轮结束后会在此发布报告。\n%s\n\n</details>' \ | ||
| "${MARKER}" "${HB_ROUND}" "${HB_CAP}" "${HB_URL}" "${line_en}" \ | ||
| "${HB_ROUND}" "${HB_CAP}" "${HB_URL}" "${line_zh}" | ||
| } | ||
|
|
||
| run_loop() { | ||
| # Validate EVERYTHING a tick needs, not just the loop's own three: a | ||
| # launch missing a body var would otherwise produce an immortal loop | ||
| # that never pulses — the exact "healthy round looks dead" failure this | ||
| # feature eliminates. Fail fast instead. | ||
| require HB_REPO HB_COMMENT_ID HB_WORKDIR HB_ROUND HB_CAP HB_URL HB_START_EPOCH TRUSTED_PATH | ||
| # gh auth rides on the step-level GITHUB_TOKEN only: the hermetic pins | ||
| # below drop any planted GH_TOKEN/GH_ENTERPRISE_TOKEN (a planted channel | ||
| # must not outrank the inline token), so accepting them here would admit | ||
| # a launch the pins then leave credential-less — an immortal loop logging | ||
| # "PATCH failed" every tick and never pulsing. Fail fast instead. | ||
| [[ -n "${GITHUB_TOKEN:-}" ]] || { | ||
| echo "autofix-status-heartbeat: GITHUB_TOKEN is required" >&2 | ||
| exit 2 | ||
| } | ||
| # Binary-resolution channel: the tick resolves its externals (gh, | ||
| # timeout, sleep, date, cat — and the mktemp below) by name, and the | ||
| # ambient PATH carries same-UID-writable dirs ahead of the system ones | ||
| # (the job's own $GITHUB_PATH append puts ${RUNNER_TEMP}/qwen-bin | ||
| # there), so a plant in one of them would be resolved by the next tick | ||
| # with the PAT in env. Pin PATH from the launcher's step-level | ||
| # TRUSTED_PATH instead — the R6-3 doctrine: expression-context | ||
| # derived, and step env outranks $GITHUB_ENV plants. post_status pins | ||
| # its own PATH the same way before the launch; the loop re-pins so no | ||
| # future launcher can hand it an ambient PATH. | ||
| # Full rationale → qwen-autofix.md#af-148 | ||
| export PATH="${TRUSTED_PATH}" | ||
| # Hermetic pins for every gh call this loop makes (the af-112 doctrine): | ||
| # pinned host, planted tokens dropped, and a fresh empty GH_CONFIG_DIR | ||
| # instead of the default ~/.config/gh on the shared attacker-writable | ||
| # HOME — its config.yml can carry http_unix_socket, which would deliver | ||
| # the tick's Authorization header (the bot PAT) to a planted listener. | ||
| local gh_config_dir | ||
| if ! gh_config_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/autofix-gh-config.XXXXXX")"; then | ||
|
qwen-code-dev-bot marked this conversation as resolved.
Outdated
|
||
| echo "autofix-status-heartbeat: could not create a gh config dir" >&2 | ||
| exit 2 | ||
| fi | ||
| export GH_HOST=github.com | ||
| unset GH_ENTERPRISE_TOKEN GH_TOKEN | ||
| export GH_CONFIG_DIR="${gh_config_dir}" | ||
| # Self-detach from the launching step: log to WORKDIR and never hold the | ||
| # step's pipes, or the step would never report completion. | ||
| exec >> "${HB_WORKDIR}/heartbeat.log" 2>&1 < /dev/null | ||
| echo "$$" > "${HB_WORKDIR}/heartbeat.pid" | ||
| local interval="${HB_INTERVAL_SECONDS:-600}" | ||
| # Just past the 330-minute job envelope: a live round's loop dies at the | ||
| # gate or finalize well inside the job, so only a crash-leftover orphan | ||
| # ever reaches the cap — and the cap bounds how long that orphan holds | ||
| # the PAT in /proc/<pid>/environ, so it stays tight. | ||
| local max_age="${HB_MAX_AGE_SECONDS:-20400}" | ||
| # Numeric guards: a malformed or zero override must degrade to the | ||
| # defaults, never into a sleep-less busy loop hammering the API. | ||
| [[ "${interval}" =~ ^[1-9][0-9]*$ ]] || interval=600 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] R16-2: The Witness (real script, both arms identical except the interval; cap=1s, start epoch already 10s past): Bound magnitude after the shape guards (or, doctrine-consistently, pin both command-scoped at the launch like the other [[ "${interval}" =~ ^[1-9][0-9]*$ ]] || interval=600
(( interval <= 3600 )) || interval=600
[[ "${max_age}" =~ ^[1-9][0-9]*$ ]] || max_age=20400
(( max_age <= 21600 )) || max_age=20400Extend the existing 中文说明
证据(真实脚本,两臂除 interval 外完全一致;cap=1s,起始 epoch 已超 10 秒):植入臂 修复:在形状守卫之后加上数值上限(见上方代码块),或按教义一致地在启动处像其他 — qwen3.8-max via Qwen Code /review (v0.22.2) |
||
| [[ "${max_age}" =~ ^[1-9][0-9]*$ ]] || max_age=20400 | ||
| local start="${HB_START_EPOCH}" | ||
| echo "$(date -u +%FT%TZ) heartbeat started: comment ${HB_COMMENT_ID} interval ${interval}s max_age ${max_age}s" | ||
| while :; do | ||
| sleep "${interval}" | ||
| local now age body | ||
| now="$(date +%s)" | ||
| age=$(( now - start )) | ||
| if (( age > max_age )); then | ||
| echo "$(date -u +%FT%TZ) self-exit: age ${age}s exceeds ${max_age}s" | ||
| exit 0 | ||
| fi | ||
| # IDENTITY, not existence: WORKDIR is PR-scoped (/tmp/autofix-review-<pr>), | ||
| # so after a crashed round's reset the NEXT round recreates heartbeat.pid | ||
| # at the same path. An existence check would let the orphaned old loop | ||
| # pass and keep PATCHing with its stale launch env, alternating with the | ||
| # new round's body on the same comment. The file must still hold THIS | ||
| # loop's own pid — removed OR replaced (by a newer round) ends the loop. | ||
| # This reads the file to self-identify only; it never kills anything. | ||
| if [[ "$(cat "${HB_WORKDIR}/heartbeat.pid" 2> /dev/null)" != "$$" ]]; then | ||
| echo "$(date -u +%FT%TZ) self-exit: pid file removed or replaced" | ||
|
wenshao marked this conversation as resolved.
Outdated
|
||
| exit 0 | ||
| fi | ||
| if [[ -f "${HB_WORKDIR}/heartbeat-stop" ]]; then | ||
| echo "$(date -u +%FT%TZ) self-exit: stop marker present" | ||
| exit 0 | ||
| fi | ||
| if ! body="$(emit_body)"; then | ||
| echo "$(date -u +%FT%TZ) body composition failed; skipping this tick" | ||
| continue | ||
| fi | ||
| # Best-effort: a transient API failure skips one tick, never the pulse. | ||
| # `timeout` bounds the request itself — a black-holed connection must | ||
| # not stall the loop past the age cap, which only runs between ticks | ||
| # (a stuck gh would hold the PAT forever). `timeout` is coreutils on | ||
| # the Linux pool; hosts without it (macOS dev runs) fall back to the | ||
| # unbounded call. | ||
| GH_PATCH=(gh) | ||
| if command -v timeout > /dev/null 2>&1; then | ||
| GH_PATCH=(timeout 60 gh) | ||
| fi | ||
|
wenshao marked this conversation as resolved.
wenshao marked this conversation as resolved.
|
||
| if ! "${GH_PATCH[@]}" api --method PATCH \ | ||
| "repos/${HB_REPO}/issues/comments/${HB_COMMENT_ID}" \ | ||
| -f body="${body}" > /dev/null 2>&1; then | ||
| echo "$(date -u +%FT%TZ) PATCH failed; continuing" | ||
| fi | ||
| done | ||
| } | ||
|
|
||
| case "${1:-}" in | ||
| body) emit_body ;; | ||
| loop) run_loop ;; | ||
| *) | ||
| echo "usage: $(basename "$0") {body|loop}" >&2 | ||
| exit 2 | ||
| ;; | ||
| esac | ||
Uh oh!
There was an error while loading. Please reload this page.