--- 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.