From 080e16c2dae3b5a4704365c45924a89374a95e2e Mon Sep 17 00:00:00 2001 From: ssavutu Date: Sat, 12 Sep 2026 13:48:03 -0400 Subject: [PATCH] Stop the release cron waking main every few hours The `*/10` schedule on scheduled-merge.yml had nothing to merge and still posted two check notifications on `main` per tick -- "merge success on main" is Discord rendering the `merge` job, not a merge -- which over a day is most of what the #github channel said. It was never a working scheduler either: measured here over 577 minutes it delivered 2 of 58 expected runs, the first 4h48m late. The Delta systemd timer (roles/release_scheduler) is what fires this workflow now, so the trigger goes rather than shrinking to an hourly cron that would be both quieter and no more dependable. A run now means there is a labelled pull request to consider. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VT3WmtV3qBRxmTKN8ABMyQ --- .github/workflows/README.md | 42 +++++++++++++++++---------- .github/workflows/scheduled-merge.yml | 25 +++++++++------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 4b5883c..6ebb726 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -33,31 +33,43 @@ default is `squash`. ### What it checks before merging -The workflow wakes every ten minutes and, for each labelled pull request whose -`Merge-at:` has passed, refuses to merge unless the pull request is open, not a -draft, conflict-free, unblocked by branch protection, and every check run and -commit status on its head commit has **finished and passed**. A pull request +The workflow is woken once a minute by the release timer on Delta and, for each +labelled pull request whose `Merge-at:` has passed, refuses to merge unless the +pull request is open, not a draft, conflict-free, unblocked by branch +protection, and every check run and commit status on its head commit has +**finished and passed**. A pull request with checks still running is left alone and reconsidered on the next tick; one with no checks at all is refused outright, so nothing unvalidated ships. If something is actually wrong (failed checks, conflicts, still a draft), the workflow comments with the reason, swaps the `scheduled-merge` label for `scheduled-merge-blocked`, and stops. Dropping the label is deliberate: it -means one explanatory comment instead of one every ten minutes, and it means a -broken release never merges later "by surprise" once the problem clears. Fix the +means one explanatory comment instead of one per tick, and it means a broken +release never merges later "by surprise" once the problem clears. Fix the problem and re-add the label to re-arm it. -### Timing accuracy +### What drives it, and how close to `Merge-at:` it lands -GitHub runs scheduled workflows on a best-effort queue and frequently several -minutes late, occasionally dropping ticks entirely under load. Read `Merge-at:` -as **not before** that time — in practice it lands within about fifteen minutes -after. Don't schedule anything that has to be exact to the minute; for a -genuinely hard deadline, merge by hand. +The ticks come from a systemd timer on Delta (`roles/release_scheduler` in +`triangle-infrastructure`) that polls once a minute and fires this workflow +through `workflow_dispatch`. **It is not on GitHub's `schedule:` trigger**, and +should not be put back on one: a `*/10` cron measured here over 577 minutes +delivered 2 of its 58 expected runs, the first 4h48m late, so a midnight +release would have merged near 04:00. It also posted two check notifications on +`main` per empty tick, which is most of what that trigger ever accomplished. -`workflow_dispatch` runs the same pass immediately, which is the way to test a -setup or push a release out early. Its `pr` input narrows the run to one pull -request and `dry_run` reports what would happen without merging anything. +Read `Merge-at:` as **not before** that time — with the timer it lands within +about a minute of it, plus the length of a deploy before the change is live. +For a genuinely hard deadline, merge by hand. + +If the timer is down, nothing merges on its own and nothing says so; the +pull request simply sits labelled. A release that matters is worth checking +after the fact. + +Dispatching the workflow by hand runs the same pass immediately, which is the +way to test a setup or push a release out early. Its `pr` input narrows the run +to one pull request and `dry_run` reports what would happen without merging +anything. ### Required setup: the release App diff --git a/.github/workflows/scheduled-merge.yml b/.github/workflows/scheduled-merge.yml index d60fa51..2fd5daf 100644 --- a/.github/workflows/scheduled-merge.yml +++ b/.github/workflows/scheduled-merge.yml @@ -21,13 +21,17 @@ name: Scheduled merge # An App can be named as one specifically (actor_type: Integration); a PAT # cannot, and could only bypass by belonging to an org admin -- a far # broader credential to leave in a repository secret. +# +# Nothing here runs on GitHub's `schedule:` trigger. A `*/10` cron was measured +# on this repository over 577 minutes and delivered 2 of 58 expected runs +# (first fire 4h48m late), which is not a scheduler a release can be timed +# against; worse, every no-op tick posted a check suite on `main` and buried +# the channel that reports real merges. The ticks come from a systemd timer on +# Delta (`roles/release_scheduler`) that polls once a minute and dispatches +# this workflow when a `Merge-at:` has passed, so a run happening at all now +# means there is something to consider. The timer holds no merge logic: every +# rule below still applies to every run. on: - schedule: - # Every ten minutes. GitHub runs scheduled workflows on a best-effort queue - # and often several minutes late, so treat `Merge-at:` as "not before this - # time", typically landing within ~15 minutes after it. Never schedule a - # release for a moment that must be hit to the minute. - - cron: "*/10 * * * *" workflow_dispatch: inputs: pr: @@ -183,8 +187,8 @@ jobs: }; // Stop a scheduled release and say why, exactly once: the label comes - // off so the next tick ignores the PR instead of re-commenting every - // ten minutes. Re-adding the label re-arms it. + // off so the next tick ignores the PR instead of re-commenting on + // every one. Re-adding the label re-arms it. const block = async (number, reason) => { core.warning(`#${number} blocked: ${reason}`); await comment(number, [ @@ -207,8 +211,9 @@ jobs: owner, repo, ref: sha, per_page: 100, }); - // This workflow runs on a schedule, not on the PR head, so it - // never reports a check run here and cannot wait on itself. + // This workflow runs against the default branch, not the PR + // head, so it never reports a check run here and cannot wait on + // itself. const pending = runs.filter((run) => run.status !== 'completed'); const failed = runs.filter( (run) => run.status === 'completed'