Skip to content

Commit c7e4339

Browse files
authored
Merge pull request #163 from githubnext/copilot/fix-autoloop-pre-step-state-files
Fix autoloop scheduler starvation: clone repo-memory before pre-step, deterministic tiebreak
2 parents dbb807f + 4930b2e commit c7e4339

1 file changed

Lines changed: 42 additions & 3 deletions

File tree

.github/workflows/autoloop.md

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,33 @@ imports:
8484
- shared/reporting.md
8585

8686
steps:
87+
- name: Clone repo-memory for scheduler
88+
# The "Check which programs are due" step below reads program state from
89+
# /tmp/gh-aw/repo-memory/autoloop/, but gh-aw's built-in repo-memory clone
90+
# runs *after* this pre-step (and clones to a different directory). Without
91+
# this step, every program looks like a "first run" and the tiebreaker
92+
# starves programs that lose the original-insertion-order tiebreak.
93+
# See issue: "Autoloop pre-step can't read state files".
94+
env:
95+
GITHUB_TOKEN: ${{ github.token }}
96+
GITHUB_REPOSITORY: ${{ github.repository }}
97+
run: |
98+
mkdir -p /tmp/gh-aw/repo-memory
99+
if [ -d /tmp/gh-aw/repo-memory/autoloop/.git ]; then
100+
echo "repo-memory/autoloop already cloned; skipping"
101+
else
102+
# Pass the token via an http.extraHeader rather than embedding it in the
103+
# URL — keeps it out of process listings and any logged remote URLs.
104+
AUTH_HEADER="Authorization: Basic $(printf 'x-access-token:%s' "${GITHUB_TOKEN}" | base64 -w0)"
105+
git -c "http.extraHeader=${AUTH_HEADER}" clone --depth=1 --branch memory/autoloop \
106+
"https://github.com/${GITHUB_REPOSITORY}.git" \
107+
/tmp/gh-aw/repo-memory/autoloop \
108+
|| {
109+
echo "memory/autoloop branch not found (first run); creating empty dir"
110+
mkdir -p /tmp/gh-aw/repo-memory/autoloop
111+
}
112+
fi
113+
87114
- name: Check which programs are due
88115
env:
89116
GITHUB_TOKEN: ${{ github.token }}
@@ -409,7 +436,8 @@ steps:
409436
"next_due": (last_run + schedule_delta).isoformat()})
410437
continue
411438
412-
due.append({"name": name, "last_run": lr, "file": pf, "target_metric": target_metric})
439+
due.append({"name": name, "last_run": lr, "file": pf, "target_metric": target_metric,
440+
"schedule_seconds": schedule_delta.total_seconds() if schedule_delta else None})
413441
414442
# Pick the program to run
415443
selected = None
@@ -457,8 +485,19 @@ steps:
457485
pass
458486
print(f"FORCED: running program '{forced_program}' (manual dispatch)")
459487
elif due:
460-
# Normal scheduling: pick the single most-overdue program
461-
due.sort(key=lambda p: p["last_run"] or "") # None/empty sorts first (never run)
488+
# Normal scheduling: pick the single most-overdue program.
489+
# Tiebreaker rationale: programs that have never run (no last_run) take
490+
# priority over ever-run programs; among never-run programs, prefer the
491+
# shortest schedule (so "every 30m" beats "every 6h"), then alphabetical
492+
# by name. Programs with no parseable schedule sort last among never-run
493+
# programs (float('inf')). This avoids permanent starvation when state
494+
# is missing — see issue: "Autoloop pre-step can't read state files".
495+
def _due_sort_key(p):
496+
if p["last_run"]:
497+
return (1, p["last_run"], p["name"])
498+
sched = p.get("schedule_seconds")
499+
return (0, sched if sched is not None else float("inf"), p["name"])
500+
due.sort(key=_due_sort_key)
462501
selected = due[0]["name"]
463502
selected_file = due[0]["file"]
464503
selected_target_metric = due[0].get("target_metric")

0 commit comments

Comments
 (0)