Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ebcc88d
feat(autofix): keep the round status comment live during long rounds
wenshao Aug 23, 2026
7fc3fd5
fix(autofix): harden the heartbeat self-exit and pin its behavior
wenshao Aug 23, 2026
e0c4c9c
fix(autofix): fail fast on a token-less heartbeat launch; correct orp…
wenshao Aug 23, 2026
32ac987
fix(autofix): close the heartbeat's token paths at the kill and the g…
wenshao Aug 23, 2026
114af68
fix(autofix): degrade pre-merge rounds past the absent heartbeat script
wenshao Aug 23, 2026
b34aae0
fix(autofix): pin the heartbeat's command resolution off the plantabl…
wenshao Aug 24, 2026
ae2c682
fix(autofix): clear the staged heartbeat plant and re-verify at the l…
wenshao Aug 24, 2026
ccbd641
fix(autofix): clear directory plants at staging; gate the launch witn…
wenshao Aug 24, 2026
0b0279e
Merge branch 'main' into feat/autofix-round-heartbeat
wenshao Aug 24, 2026
3c12d96
Merge branch 'main' into feat/autofix-round-heartbeat
wenshao Aug 24, 2026
895ea67
chore: merge origin/main into feat/autofix-round-heartbeat
wenshao Aug 24, 2026
6cb68dd
fix(ci): record ci.yml's accumulated growth in the size baseline
wenshao Aug 24, 2026
190847f
chore: integrate remote branch fixes (heartbeat hardening rounds)
wenshao Aug 24, 2026
a36c121
fix(ci): exact-size the qwen-autofix.yml baseline line after integration
wenshao Aug 24, 2026
edeb7ed
fix(ci): close round-8 heartbeat findings R8-1/R8-2/R8-3
wenshao Aug 24, 2026
aac8a2a
fix(ci): close round-9 finding R9-1 with an env -i finalize child
wenshao Aug 25, 2026
6022bd4
Merge remote-tracking branch 'origin/main' into feat/autofix-round-he…
wenshao Aug 25, 2026
8375fca
fix(autofix): bound the heartbeat pid-identity read (R10-3)
wenshao Aug 25, 2026
81b311a
fix(autofix): close R10-1/R10-2 on the gate kill block and the finali…
wenshao Aug 25, 2026
9f78012
fix(autofix): mint the hermetic gh config per call (R11-1)
wenshao Aug 25, 2026
31065a6
Merge remote-tracking branch 'origin/main' into feat/autofix-round-he…
wenshao Aug 25, 2026
a9f04a5
fix(autofix): gate the gh-config witness on the env-log it reads (R12-1)
wenshao Aug 25, 2026
538f2db
Merge branch 'main' into feat/autofix-round-heartbeat
wenshao Aug 25, 2026
578748b
Merge branch 'main' into feat/autofix-round-heartbeat
wenshao Aug 26, 2026
62c3bbd
fix(autofix): confirm heartbeat pid lifecycle and drain in-flight tic…
wenshao Aug 26, 2026
9bdf37f
Merge branch 'main' into feat/autofix-round-heartbeat
qwen-code-dev-bot Aug 26, 2026
b287dbb
fix(autofix): env-route heartbeat kill targets, cap override magnitud…
qwen-code-dev-bot Aug 27, 2026
dd5cb59
Merge remote-tracking branch 'origin/main' into feat/autofix-round-he…
qwen-code-dev-bot Aug 27, 2026
7ee39a0
fix(autofix): byte-bound pid/stamp reads, digit-bound overrides, clos…
qwen-code-dev-bot Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 275 additions & 0 deletions .github/scripts/autofix-status-heartbeat.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
#!/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-149.
#
# 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. Every killer
# confirms the pid's LIFECYCLE before signaling: the launch also records
# the loop's start time (heartbeat_start_ticks), and a pid reused between
# launch and kill belongs to a different process — a killer signals only
# a pid whose /proc/<pid>/stat start time still matches the launch
# capture, and kills nothing otherwise (a dead pid has no stat and a
# reused one a different start time, so a failed check always means the
# loop is already gone). Each tick additionally stamps
# heartbeat-tick-inflight with its start epoch around its gh call and
# removes it after: finalize drains that stamp before its terminal PATCH,
# because killing the client cannot cancel a request the server already
# accepted.
#
# 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,
# a GH_CONFIG_DIR minted fresh milliseconds before the call and removed right
# after — a long-lived minted dir under the same-UID-writable RUNNER_TEMP is
# plantable between calls, R11-1), so a transport reroute planted in the
# shared HOME's gh config cannot intercept the token. See af-149 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_EPOCH is the test-only clock override — no production launcher
# sets it, so a value can only arrive through an env plant. Bash
# arithmetic expansion recursively evaluates the variable's value, so a
# planted value's embedded command substitution would EXECUTE inside
# this PAT-holding process. Accept a numeric override only; anything
# else falls back to the real clock.
now="${NOW_EPOCH:-}"
[[ "${now}" =~ ^[0-9]+$ ]] || now="$(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, head — 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-149
export PATH="${TRUSTED_PATH}"
# Hermetic pins for every gh call this loop makes (the af-112 doctrine):
# pinned host and planted tokens dropped here, at launch. The config dir
# is NOT minted here: it is minted per tick, milliseconds before each gh
# call, and removed right after (below) — RUNNER_TEMP is same-UID-writable,
# so a dir minted once at launch and reused across ticks is plantable: a
# watcher knowing the stable prefix writes a config.yml carrying
# http_unix_socket into it, and every later tick delivers the PATCH's
# Authorization header (the bot PAT) to the planted socket — for the loop
# the 600s sleep before the first call made the window a certainty, not a
# race (R11-1, probe-verified). The per-call shape shrinks the window to
# one call's mint→use race, the residual the pin documents.
export GH_HOST=github.com
unset GH_ENTERPRISE_TOKEN GH_TOKEN
# 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.
# Shape alone is not enough: the loop sleeps BEFORE its age check,
# so a well-formed huge interval — no production launcher sets
# either variable, so such a value can only arrive through an env
# plant — means the loop never wakes again (zero pulses, and the
# age cap that bounds an orphan's PAT window becomes unreachable),
# while a tiny age cap silently kills the pulse after the first
# sleep — the frozen comment this feature eliminates. Bound the
# magnitude too; the cap's floor is the 330-minute job envelope, so
# a live round's pulse always outlives the round. The digit bound
# runs FIRST: bash arithmetic wraps modulo 2^64, so a 20+-digit plant
# would pass the comparisons on its wrapped value while the original
# string still reaches sleep — the loop never wakes again (R16-2).
[[ "${interval}" =~ ^[1-9][0-9]{0,3}$ ]] || interval=600
(( interval <= 3600 )) || interval=600

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R16-2: (fix-induced) The magnitude guard the round-16 fix added for HB_INTERVAL_SECONDS / HB_MAX_AGE_SECONDS is defeated by any integer literal with 20+ digits: bash arithmetic wraps modulo 2^64, so (( interval <= 3600 )) compares the wrapped value while the original 20-digit string is kept and handed to sleep. The round-16 fix closed the input it reported (a well-formed huge value like 99999999999 now resets correctly), but the arithmetic it introduced opens this new hole in the same guard.

A plant of HB_INTERVAL_SECONDS=18446744073709551616 (2^64 — no production launcher sets the variable, so a $GITHUB_ENV plant is the documented carrier) passes the ^[1-9][0-9]*$ shape check; bash 5.2 evaluates $(( 18446744073709551616 )) as 0, the magnitude comparison passes, and the guard keeps the original string. GNU sleep accepts sleep 18446744073709551616 and blocks indefinitely — the loop never wakes again: zero pulses forever (the status comment freezes — the exact failure this feature exists to eliminate) and the age cap that bounds a crash-orphan's PAT window in /proc/<pid>/environ becomes unreachable. The existing magnitude test uses '99999999999' (11 digits, wraps to itself), which cannot expose this corner.

Witness (probed at the reviewed commit):

heartbeat.log: heartbeat started: comment 777 interval 18446744073709551616s max_age 20400s
with the digit-bound fix below: interval 600s max_age 20400s
timeout 3 sleep 18446744073709551616 → rc=124 (still asleep when killed)

Fix — bound digit length before any arithmetic, keeping the existing range comparisons after:

[[ "${interval}" =~ ^[1-9][0-9]{0,3}$ ]] || interval=600   # ≤ 9999, cannot wrap
(( interval <= 3600 )) || interval=600
[[ "${max_age}" =~ ^[1-9][0-9]{0,4}$ ]] || max_age=20400   # ≤ 99999, cannot wrap
(( max_age >= 19800 && max_age <= 21600 )) || max_age=20400

Fix witness: extend the existing magnitude-degrade test in autofix-status-heartbeat.test.mjs (the one seeding HB_INTERVAL_SECONDS: '99999999999') with a '18446744073709551616' arm asserting the degraded interval 600s max_age 20400s log line — removing the digit bound must turn that arm red.

中文说明

[Critical] R16-2:(修复引入)第 16 轮修复为 HB_INTERVAL_SECONDS / HB_MAX_AGE_SECONDS 新增的数值上限防护,会被任意 20 位及以上的整数字面量绕过:bash 算术按 2^64 取模回绕,因此 (( interval <= 3600 )) 比较的是回绕后的值,而原始的 20 位字符串会被保留并传给 sleep。第 16 轮的修复封闭了它当时报告的输入(99999999999 这类合法超大值现在会被正确重置),但它引入的算术在同一处防护上打开了这个新洞。

失败场景:植入 HB_INTERVAL_SECONDS=18446744073709551616(2^64——生产环境没有任何启动者设置该变量,$GITHUB_ENV 植入是文档认定的载体)可通过 ^[1-9][0-9]*$ 形状检查;bash 5.2 把 $(( 18446744073709551616 )) 求值为 0,数值比较通过,防护保留原始字符串。GNU sleep 接受 sleep 18446744073709551616 并无限期阻塞——循环再也不会醒来:永远零次脉搏(状态评论冻结——正是本功能要消除的故障),并且限制崩溃孤儿在 /proc/<pid>/environ 中持有 PAT 窗口的年龄上限变得不可达。现有数值测试使用 '99999999999'(11 位,回绕后仍为自身),无法暴露这个角落。

证据(在被审提交上实测):见英文部分 witness 代码块(原始臂日志为 interval 18446744073709551616s;应用修复后为 interval 600stimeout 3 sleep 2^64 返回 124,仍在睡眠)。

修复——在任何算术之前先限制位数,保留其后的现有范围比较(代码见英文部分)。

修复见证:把现有数值降级测试(注入 '99999999999' 的那个)扩展一个 '18446744073709551616' 臂,断言降级后的 interval 600s max_age 20400s 日志行——移除位数限制后该臂必须变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

[[ "${max_age}" =~ ^[1-9][0-9]{0,4}$ ]] || max_age=20400
(( max_age >= 19800 && max_age <= 21600 )) || 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.
# The read is BOUNDED in time AND bytes: WORKDIR is sandbox-writable,
# so the path can hold a planted FIFO whose open blocks the read
# indefinitely, or a symlink to an endless non-NUL stream
# (/dev/urandom) that an unbounded read would pull into bash's
# substitution buffer GB-scale inside one tick of this PAT-holding
# loop — pulse death for the rest of the round (R17-1). A pid is
# ≤ ~10 digits, so 64 bytes cover any real pid file. Mirrors the gh
# wrapper's conditional timeout form below; a timeout kill yields
# empty → identity mismatch → the clean self-exit just below.
local pid_now
if command -v timeout > /dev/null 2>&1; then
pid_now="$(timeout 5 head -c 64 -- "${HB_WORKDIR}/heartbeat.pid" 2> /dev/null)"
else
pid_now="$(head -c 64 -- "${HB_WORKDIR}/heartbeat.pid" 2> /dev/null)"
fi
if [[ "${pid_now}" != "$$" ]]; then
echo "$(date -u +%FT%TZ) self-exit: pid file removed or replaced"
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
# Hermetic gh config, minted milliseconds before the call and removed
# right after (the R11-1 shape): a dir reused across ticks is plantable
# under the same-UID-writable RUNNER_TEMP, and a planted http_unix_socket
# would capture the PATCH's Authorization header (the bot PAT). Fail
# CLOSED: a failed mint skips the tick, never runs gh with the PAT
# against the shared ~/.config/gh — a skip degrades one pulse, never
# the loop (the age cap still bounds it).
local gh_config_dir
if ! gh_config_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/autofix-gh-config.XXXXXX")"; then
echo "$(date -u +%FT%TZ) gh config mint 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
Comment thread
wenshao marked this conversation as resolved.
Comment thread
wenshao marked this conversation as resolved.
# In-flight stamp for finalize's drain: killing this loop ends the
# client, but a PATCH the server already ACCEPTED still commits (the
# race that flipped a terminal comment back to "working"), so
# finalize must wait the last tick's request out before writing the
# terminal text. The stamp carries the tick's start epoch; the
# bounded call above gives finalize the 65s completion bound it
# drains against. The write is bounded like the pid-identity read:
# WORKDIR is sandbox-writable, and a planted FIFO at this path would
# otherwise block the loop inside the tick, past the age cap. A
# failed stamp degrades to the pre-drain race, never stalls the
# pulse.
if command -v timeout > /dev/null 2>&1; then
# shellcheck disable=SC2016
timeout 5 bash -c 'date +%s > "$0"' "${HB_WORKDIR}/heartbeat-tick-inflight" 2> /dev/null || true

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R15-4: shellcheck SC2016 — Expressions don't expand in single quotes, use double quotes for that. — is still flagged on the bounded stamp write at the reviewed commit by this repo's own pinned shellcheck pipeline (0.11.0, --severity=style): .github/scripts/autofix-status-heartbeat.sh:235:25: warning: … [SC2016] (16 warnings on the new file, SC2016 ×1). The single-quoting is intentional — $0 is the inner bash's positional, not the outer shell's — so the fix is the repo's established handling of this exact pattern (8 existing occurrences across workflows): a disable directive. Do NOT switch to double quotes: the outer shell would then expand $0 before the inner bash starts, breaking the bounded stamp write.

Suggested change
timeout 5 bash -c 'date +%s > "$0"' "${HB_WORKDIR}/heartbeat-tick-inflight" 2> /dev/null || true
# shellcheck disable=SC2016
timeout 5 bash -c 'date +%s > "$0"' "${HB_WORKDIR}/heartbeat-tick-inflight" 2> /dev/null || true

Re-running node scripts/lint.js --shellcheck afterwards reports no SC2016 on this file.

中文说明

在被审提交上,本仓库自钉的 shellcheck 流水线(0.11.0,--severity=style)仍对受限戳写入这一行报 SC2016——"Expressions don't expand in single quotes, use double quotes for that.":.github/scripts/autofix-status-heartbeat.sh:235:25: warning: … [SC2016](新文件共 16 条警告,SC2016 ×1)。这里的单引号是有意的——$0 是内层 bash 的位置参数,不是外层 shell 的——因此修复应采用本仓库对该模式既有的处理方式(工作流中已有 8 处同类):加一条 disable 指令。切勿改成双引号:那样外层 shell 会在内层 bash 启动前展开 $0,破坏受限戳写入。

修复后重跑 node scripts/lint.js --shellcheck,该文件应不再出现 SC2016。

— qwen3.8-max via Qwen Code /review (v0.22.2)

else
date +%s > "${HB_WORKDIR}/heartbeat-tick-inflight" 2> /dev/null || true
fi
if ! GH_CONFIG_DIR="${gh_config_dir}" "${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
rm -f "${HB_WORKDIR}/heartbeat-tick-inflight" 2> /dev/null || true
rm -rf "${gh_config_dir}"
done
}

case "${1:-}" in
body) emit_body ;;
loop) run_loop ;;
*)
echo "usage: $(basename "$0") {body|loop}" >&2
exit 2
;;
esac
Loading
Loading