diff --git a/.gitea/workflows/renovate.yaml b/.gitea/workflows/renovate.yaml index 81f14ed..91798e6 100644 --- a/.gitea/workflows/renovate.yaml +++ b/.gitea/workflows/renovate.yaml @@ -74,12 +74,26 @@ name: renovate on: schedule: - # 03:15 UTC daily. renovate.json ALSO carries `"schedule": ["after 3am and before 6am"]` in - # America/New_York, and that inner schedule is the authoritative one — Renovate re-checks it and - # will decline to open PRs outside it. The two are deliberately both present: this cron decides - # when the process RUNS, renovate.json decides when it may ACT. Keeping the cron slightly ahead - # of the repo window means a run is always in flight when the window opens. - - cron: "15 3 * * *" + # 08:15 UTC daily. renovate.json ALSO carries `"schedule": ["after 3am and before 6am"]` with + # `"timezone": "America/New_York"`, and THAT inner schedule is authoritative — Renovate re-checks + # it and DECLINES TO OPEN PRs outside it. So this cron must fire INSIDE the local window, not + # merely near it. + # + # WAS `15 3 * * *`, WHICH NEVER WOULD HAVE WORKED (fixed 2026-10-18, before the first scheduled + # run). Gitea Actions cron is UTC, so 03:15 UTC is 23:15 the PREVIOUS EVENING in New York -- + # 3h45m BEFORE the window opens, not "slightly ahead" of it as the original comment claimed. + # Every nightly run would have started, logged "not within schedule", opened zero PRs and + # exited SUCCESSFULLY. A green tick forever, and no updates ever: the exact "failure mode is a + # green tick" shape this repo keeps getting burned by, and indistinguishable from "nothing + # needed updating". + # + # THE WINDOW IN UTC MOVES WITH US DST, and a cron cannot follow it: + # EDT (UTC-4, Mar-Nov): local 03:00-06:00 == 07:00-10:00 UTC + # EST (UTC-5, Nov-Mar): local 03:00-06:00 == 08:00-11:00 UTC + # 08:15 UTC is the only quarter-hour comfortably inside BOTH (04:15 EDT / 03:15 EST), so the + # schedule keeps working across both switchovers without an edit. Do not "tidy" this back to an + # early-morning-UTC value without redoing this arithmetic. + - cron: "15 8 * * *" workflow_dispatch: inputs: dry_run: diff --git a/decisions/2026-10-18-renovate-cron-window.md b/decisions/2026-10-18-renovate-cron-window.md new file mode 100644 index 0000000..9907d0f --- /dev/null +++ b/decisions/2026-10-18-renovate-cron-window.md @@ -0,0 +1,78 @@ +--- +date: 2026-10-18 +branch: fix/renovate-cron-window +title: "The Renovate cron fired outside Renovate's own schedule, so it would never have opened a PR" +--- + +## What changed + +`.gitea/workflows/renovate.yaml` shipped (PR #73) with `cron: "15 3 * * *"`. **That schedule could +never have produced a single PR.** + +Gitea Actions cron is **UTC**. `renovate.json` carries `"schedule": ["after 3am and before 6am"]` +with `"timezone": "America/New_York"`, and that inner schedule is **authoritative** — Renovate +re-checks it and declines to open PRs outside it. So: + +``` +cron 03:15 UTC -> 23:15 EDT, the PREVIOUS EVENING in New York +window -> 03:00-06:00 local +inside? -> NO (3h45m before the window opens) +``` + +The original comment claimed the cron was "slightly ahead of the repo window ... so a run is always +in flight when the window opens". It is 3h45m *behind*, on the previous day. Every nightly run would +have started, logged "not within schedule", opened **zero** PRs and exited **successfully** — a +green tick forever, indistinguishable from "nothing needed updating". That is the same +"failure-mode-is-a-green-tick" shape this repo keeps hitting, and it would have hidden the ~30 +pending updates PR #73 was written to surface. + +Changed to `cron: "15 8 * * *"`, chosen to survive DST rather than to be tidy. The window's UTC +position moves: + +| | local window | in UTC | +|---|---|---| +| EDT (UTC-4, Mar–Nov) | 03:00–06:00 | 07:00–10:00 | +| EST (UTC-5, Nov–Mar) | 03:00–06:00 | 08:00–11:00 | + +`08:15 UTC` is inside **both** (04:15 EDT / 03:15 EST), so no edit is needed at either switchover. +A cron cannot follow DST, so this is the only way to get it right once. + +## What I verified + +* **The defect, by arithmetic rather than assertion:** `datetime(2026,10,18,03,15, UTC)` converted to + `America/New_York` via `zoneinfo` is `2026-10-17 23:15 EDT`; `3 <= hour < 6` is **False**. +* **The fix, at both DST extremes:** `08:15 UTC` → `04:15 EDT` in July and `03:15 EST` in December, + both inside `03:00–06:00`. Computed, not reasoned. +* **The inner schedule really is authoritative and really is set:** `renovate.json` on `main` carries + `timezone = America/New_York` and `schedule = ['after 3am and before 6am']`. +* **`RENOVATE_TOKEN` now exists** as a repository Actions secret (listed by name via the Gitea API + alongside `SETTINGS_API_TOKEN`), so the workflow's fail-hard preflight will pass. It did not exist + when PR #73 was authored. +* The edited workflow parses as YAML. + +## What I did NOT verify + +* **Renovate has still never executed.** This fixes *when* it runs, not *whether it works*. No run + has happened, so nothing here proves it extracts dependencies, authenticates to Gitea, or opens a + usable PR. +* **`enabledManagers` may silently disable the custom managers — UNRESOLVED and the likelier + remaining bug.** `renovate.json` lists `enabledManagers: [..., 'regex', ...]` and defines + `customManagers` (correct modern key; `regexManagers` is absent, which is right). But modern + Renovate names the custom manager **`custom.regex`** in `enabledManagers`, not bare `regex`. If + the bare name is invalid or ignored, every inline `# renovate:` marker in the tree does nothing — + including the `external-snapshotter` `newTag` pin PR #73 flagged as probably inert. **I did not + change it**, because guessing at a config key that could fail validation and take the whole run + down is worse than leaving a question that one dry run answers definitively. The `dry_run: true` + dispatch log settles it: look for a per-manager dependency-extraction count, and for any config + validation warning naming `regex`. +* **Whether a dry run inside vs outside the window differs usefully.** Renovate honours its schedule + even in dry-run, so a manual dispatch outside 03:00–06:00 local will also log "not within + schedule". That makes the first dispatch's output less informative than it looks; to see what + Renovate *would* do, either dispatch inside the local window or override the schedule for that + run. Not tested either way. +* **No alert fires if the schedule stops firing.** PR #73 recorded this gap and it is still open: a + Renovate workflow that silently stops running (disabled Actions, expired token, deleted secret) + produces no signal at all. Closing it needs a check on last-successful-run age, which nothing + does. +* No conformance test asserts the cron falls inside `renovate.json`'s window, so this exact bug can + return. A static check comparing the two is feasible and is not written.