-
-
Notifications
You must be signed in to change notification settings - Fork 159
ci: two-hourly sweep backstop — no push sweep ever reached a runner (measured) #8357
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 all commits
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,15 @@ | ||
| ### CI: two-hourly cron backstop for the main sweep (`main-gate`) | ||
|
|
||
| Measured 2026-08-16..18: **no push-triggered sweep ever reached a runner.** | ||
| The coalescing group keeps one running + one pending run and replaces the | ||
| pending with each newer merge — and merges landed faster than the | ||
| pending→running transition, even with an idle queue, so `main-gate` had never | ||
| once executed (and main-line rust-cache/sccache saves only happened on | ||
| nightlies). A `47 */2 * * *` cron now fires the sweep reliably; | ||
| `scripts/ci_plan.py` maps the nightly cron to `full` and any other cron to | ||
| `sweep` (self-tested both ways). The push arm stays for quiet periods. | ||
|
|
||
| Note: the sweep's full `cargo-test` legitimately fails on #8222 today, so | ||
| `main-gate` reports red until that lands — deliberately not registered in | ||
| `gate_freshness.json` until then (a freshness alarm on a known-red gate is | ||
| noise). | ||
|
Comment on lines
+12
to
+15
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. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Make the changeset wording durable. “Fails on Based on learnings: Perry changelog fragments should describe final shipped behavior as one coherent release-note entry and include affected paths and validation notes. 🤖 Prompt for AI AgentsSource: Learnings |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,7 @@ python3 scripts/ci_plan.py --self-test # the policy's own invariants | |
| | tier | trigger | what it is for | fan-in job | | ||
| |---|---|---|---| | ||
| | **pr** | every `pull_request` push | the required gate. Small, fast, must be green on `main`. | `pr-gate` — **the only required status context** | | ||
| | **sweep** | every `push` to `main`, coalesced | post-merge truth for `main`: the PR tier unscoped plus the medium-weight jobs that do not fit the PR budget | `main-gate` | | ||
| | **sweep** | every `push` to `main` (coalesced) **+ a two-hourly cron backstop** | post-merge truth for `main`: the PR tier unscoped plus the medium-weight jobs that do not fit the PR budget | `main-gate` | | ||
| | **full** | nightly `schedule`, `v*` tags, `workflow_dispatch`, PRs labelled `run-extended-tests` | everything, incl. parity, compile-smoke, doc-tests, package smokes, the 8-shard auto-optimize gap suite | `full-suite-gate` — what `release-packages.yml` waits for | | ||
|
|
||
| ## The job × tier matrix | ||
|
|
@@ -110,7 +110,11 @@ which costs no runner slot. | |
| `push` to `main` uses ONE constant concurrency group with `cancel-in-progress: false`: | ||
| GitHub keeps at most one running + one pending run per group and replaces the pending | ||
| run with the newest, so a burst of merges is tested at its tip instead of queueing 58 | ||
| sweeps. Sweep-only jobs are chained behind `check` so a sweep's fan-out does not take | ||
| sweeps. **The push arm alone is not sufficient**: measured 2026-08-16..18, no push | ||
| sweep ever reached a runner — merges replaced the pending run faster than the | ||
| pending→running transition happened, even with an idle queue. The two-hourly cron | ||
| (`47 */2 * * *`; the planner maps any non-nightly cron to the sweep tier) is the | ||
| reliable arm; the push trigger stays for quiet periods. Sweep-only jobs are chained behind `check` so a sweep's fan-out does not take | ||
|
Comment on lines
+113
to
+117
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. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Document the freshness-monitoring exception. The PR deliberately excludes 🤖 Prompt for AI Agents |
||
| every runner slot the moment a merge lands. Attribution of a sweep failure is by | ||
| window (`previous sweep SHA .. this sweep SHA`), exactly as for the six-hourly gates. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -128,6 +128,11 @@ | |
|
|
||
| EXTENDED_LABEL = "run-extended-tests" | ||
|
|
||
| # The nightly (full-tier) cron in test.yml `on.schedule`. Any OTHER cron firing | ||
| # the workflow is the two-hourly sweep backstop. Keep in lockstep with the | ||
| # workflow; the self-test asserts both mappings. | ||
| NIGHTLY_CRON = "0 4 * * *" | ||
|
|
||
|
Comment on lines
+131
to
+135
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. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
workflow = Path(".github/workflows/test.yml").read_text(encoding="utf-8")
planner = Path("scripts/ci_plan.py").read_text(encoding="utf-8")
schedules = re.findall(r"^\s*-\s*cron:\s*'([^']+)'", workflow, re.MULTILINE)
nightly = re.search(r'^NIGHTLY_CRON = "([^"]+)"$', planner, re.MULTILINE)
assert schedules == ["0 4 * * *", "47 */2 * * *"], schedules
assert nightly and nightly.group(1) == schedules[0]
PYRepository: PerryTS/perry Length of output: 151 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- planner context ---'
sed -n '110,155p' scripts/ci_plan.py
printf '%s\n' '--- workflow schedule context ---'
rg -n -A8 -B4 'cron:' .github/workflows/test.yml
printf '%s\n' '--- planner self-test and workflow references ---'
rg -n -A8 -B8 'NIGHTLY_CRON|schedules|cron|self.?test|ci_plan' scripts/ci_plan.py .github/workflows/test.ymlRepository: PerryTS/perry Length of output: 50369 Make the workflow/planner cron contract executable.
🤖 Prompt for AI Agents |
||
| # --------------------------------------------------------------------------- | ||
| # PR scope classification. | ||
| # --------------------------------------------------------------------------- | ||
|
|
@@ -218,7 +223,13 @@ def classify(changed: list[str]) -> dict[str, bool]: | |
| # --------------------------------------------------------------------------- | ||
| # Tier derivation. | ||
| # --------------------------------------------------------------------------- | ||
| def derive_tier(event: str, ref: str, labels: list[str], tier_input: str | None) -> str: | ||
| def derive_tier( | ||
| event: str, | ||
| ref: str, | ||
| labels: list[str], | ||
| tier_input: str | None, | ||
| schedule: str | None = None, | ||
| ) -> str: | ||
| if event == "pull_request": | ||
| return "full" if EXTENDED_LABEL in labels else "pr" | ||
| if event == "push": | ||
|
|
@@ -230,6 +241,14 @@ def derive_tier(event: str, ref: str, labels: list[str], tier_input: str | None) | |
| # `on.push.branches`), but be explicit if one ever does. | ||
| return "sweep" | ||
| if event == "schedule": | ||
| # Two crons fire this workflow. The nightly is the full tier; the | ||
| # two-hourly is the SWEEP BACKSTOP: the push-triggered sweep uses one | ||
| # constant concurrency group (1 running + 1 pending, newest replaces | ||
| # pending), and measured 2026-08-16..18 NO push sweep ever reached a | ||
| # runner -- merges landed faster than pending->running could happen, | ||
| # even with an idle queue. The cron string is passed via --schedule. | ||
| if schedule and schedule != NIGHTLY_CRON: | ||
| return "sweep" | ||
| return "full" | ||
| if event == "workflow_dispatch": | ||
| return tier_input or "full" | ||
|
|
@@ -243,9 +262,10 @@ def plan( | |
| changed: list[str] | None = None, | ||
| tier_input: str | None = None, | ||
| update_gap_snapshot: bool = False, | ||
| schedule: str | None = None, | ||
| ) -> dict: | ||
| labels = labels or [] | ||
| tier = derive_tier(event, ref, labels, tier_input) | ||
| tier = derive_tier(event, ref, labels, tier_input, schedule) | ||
| if event == "pull_request": | ||
| scope = classify(changed or []) | ||
| if scope["unknown"]: | ||
|
|
@@ -334,6 +354,10 @@ def check(name: str, cond: bool): | |
| check("push main is sweep", derive_tier("push", "refs/heads/main", [], None) == "sweep") | ||
| check("tag is full", derive_tier("push", "refs/tags/v0.5.9999", [], None) == "full") | ||
| check("schedule is full", derive_tier("schedule", "refs/heads/main", [], None) == "full") | ||
| check("nightly cron is full", derive_tier("schedule", "refs/heads/main", [], None, NIGHTLY_CRON) == "full") | ||
| check("any other cron is the sweep backstop", derive_tier("schedule", "refs/heads/main", [], None, "47 */2 * * *") == "sweep") | ||
| backstop = plan("schedule", "refs/heads/main", schedule="47 */2 * * *") | ||
| check("sweep backstop runs windows but not parity", backstop["jobs"]["windows_build"] and not backstop["jobs"]["parity"]) | ||
| check("dispatch default is full", derive_tier("workflow_dispatch", "refs/heads/x", [], None) == "full") | ||
| check("dispatch tier honoured", derive_tier("workflow_dispatch", "refs/heads/x", [], "sweep") == "sweep") | ||
|
|
||
|
|
@@ -431,6 +455,7 @@ def main(argv: list[str]) -> int: | |
| ap.add_argument("--labels", default="", help="comma-separated PR label names") | ||
| ap.add_argument("--changed-files", help="file with one changed path per line (PR only)") | ||
| ap.add_argument("--tier", choices=TIERS, help="workflow_dispatch tier input") | ||
| ap.add_argument("--schedule", help="the firing cron string (github.event.schedule)") | ||
| ap.add_argument("--update-gap-snapshot", action="store_true") | ||
| ap.add_argument("--table", action="store_true", help="print the tier table as markdown") | ||
| ap.add_argument("--self-test", action="store_true") | ||
|
|
@@ -457,6 +482,7 @@ def main(argv: list[str]) -> int: | |
| changed=changed, | ||
| tier_input=args.tier, | ||
| update_gap_snapshot=args.update_gap_snapshot, | ||
| schedule=args.schedule, | ||
| ) | ||
| out = json.dumps(p, separators=(",", ":"), sort_keys=True) | ||
| print(json.dumps(p, indent=2, sort_keys=True)) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 224
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 1395
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 1313
Rename the changeset to
changelog.d/8357-sweep-schedule-backstop.md. The current PR number is8357, not8360.🤖 Prompt for AI Agents
Source: Learnings