Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 27 additions & 15 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 15 additions & 10 deletions .github/workflows/scheduled-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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, [
Expand All @@ -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'
Expand Down
Loading