This is a calendar sync workflow to trigger off of milestones being created or due dates being updated to keep the release calendar automatically in sync example of me running it locally ``` ❯ python sync.py --oauth --client-secret ./gcal-releases-oauth.json --apply # Fleet release calendar sync — 2026-07-01 14:50 Calendar: <redacted>@group.calendar.google.com Repo: fleetdm/fleet ## Milestones 4.88.0 due 2026-07-01 4.89.0 due 2026-07-10 4.90.0 due 2026-07-31 4.91.0 due 2026-08-21 4.92.0 due 2026-09-11 4.93.0 due 2026-10-02 4.94.0 due 2026-10-23 4.95.0 due 2026-11-13 4.96.0 due 2026-12-04 4.97.0 due 2026-12-25 4.98.0 due 2027-01-15 4.99.0 due 2027-02-05 4.100.0 due 2027-02-26 4.101.0 due 2027-03-19 4.102.0 due 2027-04-09 ## Events scanned [develop ] 'Develop (next release - 4.87.0)' start=2026-05-18 end=2026-06-06 [develop ] 'Develop (next release - 4.89.0)' start=2026-06-08 end=2026-06-27 [rc ] 'Release candidate (next release - 4.87.0)' start=2026-06-08 end=2026-06-20 [release_day] 'Release day: minor release - 4.87.0' start=2026-06-19 end=2026-06-20 [develop ] 'Develop (next release - 4.90.0)' start=2026-06-29 end=2026-07-18 [rc ] 'Release candidate (next release - 4.89.0)' start=2026-06-29 end=2026-07-11 [release_day] 'Release day: minor release - 4.89.0' start=2026-07-10 end=2026-07-11 [develop ] 'Develop (next release - 4.91.0)' start=2026-07-20 end=2026-08-08 [rc ] 'Release candidate (next release - 4.90.0)' start=2026-07-20 end=2026-08-01 [release_day] 'Release day: minor release - 4.90.0' start=2026-07-31 end=2026-08-01 [develop ] 'Develop (next release - 4.92.0)' start=2026-08-10 end=2026-08-29 [rc ] 'Release candidate (next release - 4.91.0)' start=2026-08-10 end=2026-08-22 [release_day] 'Release day: minor release - 4.91.0' start=2026-08-21 end=2026-08-22 [develop ] 'Develop (next release - 4.93.0)' start=2026-08-31 end=2026-09-19 [rc ] 'Release candidate (next release - 4.92.0)' start=2026-08-31 end=2026-09-12 [release_day] 'Release day: minor release - 4.92.0' start=2026-09-11 end=2026-09-12 [develop ] 'Develop (next release - 4.94.0)' start=2026-09-21 end=2026-10-10 [rc ] 'Release candidate (next release - 4.93.0)' start=2026-09-21 end=2026-10-03 [release_day] 'Release day: minor release - 4.93.0' start=2026-10-02 end=2026-10-03 [develop ] 'Develop (next release - 4.95.0)' start=2026-10-12 end=2026-10-31 [rc ] 'Release candidate (next release - 4.94.0)' start=2026-10-12 end=2026-10-24 [release_day] 'Release day: minor release - 4.94.0' start=2026-10-23 end=2026-10-24 [develop ] 'Develop (next release - 4.96.0)' start=2026-11-02 end=2026-11-21 [rc ] 'Release candidate (next release - 4.95.0)' start=2026-11-02 end=2026-11-14 [release_day] 'Release day: minor release - 4.95.0' start=2026-11-13 end=2026-11-14 [develop ] 'Develop (next release - 4.97.0)' start=2026-11-23 end=2026-12-12 [rc ] 'Release candidate (next release - 4.96.0)' start=2026-11-23 end=2026-12-05 [release_day] 'Release day: minor release - 4.96.0' start=2026-12-04 end=2026-12-05 [develop ] 'Develop (next release - 4.98.0)' start=2026-12-14 end=2027-01-02 [rc ] 'Release candidate (next release - 4.97.0)' start=2026-12-14 end=2026-12-26 [release_day] 'Release day: minor release - 4.97.0' start=2026-12-25 end=2026-12-26 [rc ] 'Release candidate (next release - 4.90.0)' start=2027-01-04 end=2027-01-16 ## Proposed actions RC 4.98.0: title 'Release candidate (next release - 4.90.0)' -> 'Release candidate (next release - 4.98.0)' + CREATE Release day 4.88.0 on 2026-07-01 + CREATE RC 4.88.0 2026-06-27..2026-07-02 + CREATE Release day 4.98.0 on 2027-01-15 + CREATE Release day 4.99.0 on 2027-02-05 + CREATE RC 4.99.0 2027-02-01..2027-02-06 + CREATE Develop 4.99.0 2027-01-04..2027-01-23 + CREATE Release day 4.100.0 on 2027-02-26 + CREATE RC 4.100.0 2027-02-22..2027-02-27 + CREATE Develop 4.100.0 2027-01-25..2027-02-13 + CREATE Release day 4.101.0 on 2027-03-19 + CREATE RC 4.101.0 2027-03-15..2027-03-20 + CREATE Develop 4.101.0 2027-02-15..2027-03-06 + CREATE Release day 4.102.0 on 2027-04-09 + CREATE RC 4.102.0 2027-04-05..2027-04-10 + CREATE Develop 4.102.0 2027-03-08..2027-03-27 ## Warnings / skipped ! Develop event 'Develop (next release - 4.87.0)' ending 2026-06-05 has no matching minor milestone — skipped ! RC event 'Release candidate (next release - 4.87.0)' ending 2026-06-19 has no matching milestone — skipped ! Release day event 'Release day: minor release - 4.87.0' on 2026-06-19 has no matching milestone (within 5d) — skipped ! Skipping Develop create for 4.88.0: sprint ended 2026-06-17 (past) Total: 1 rename, 0 move, 15 create, 4 warning Applying 16 change(s)... ok: RC 4.98.0: title 'Release candidate (next release - 4.90.0)' -> 'Release candidate (next release - 4.98.0)' ok: + CREATE Release day 4.88.0 on 2026-07-01 ok: + CREATE RC 4.88.0 2026-06-27..2026-07-02 ok: + CREATE Release day 4.98.0 on 2027-01-15 ok: + CREATE Release day 4.99.0 on 2027-02-05 ok: + CREATE RC 4.99.0 2027-02-01..2027-02-06 ok: + CREATE Develop 4.99.0 2027-01-04..2027-01-23 ok: + CREATE Release day 4.100.0 on 2027-02-26 ok: + CREATE RC 4.100.0 2027-02-22..2027-02-27 ok: + CREATE Develop 4.100.0 2027-01-25..2027-02-13 ok: + CREATE Release day 4.101.0 on 2027-03-19 ok: + CREATE RC 4.101.0 2027-03-15..2027-03-20 ok: + CREATE Develop 4.101.0 2027-02-15..2027-03-06 ok: + CREATE Release day 4.102.0 on 2027-04-09 ok: + CREATE RC 4.102.0 2027-04-05..2027-04-10 ok: + CREATE Develop 4.102.0 2027-03-08..2027-03-27 ``` We can now get rid of the recurring unnamed events and just rely on this once it's merged and added to milestone updates to trigger automatically <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a release calendar synchronization tool that aligns Google Calendar events with open release milestone dates. * Supports dry-run previews and an optional apply mode to create, update, and delete events. * Categorizes and matches release-day, release-candidate, and development entries, including duplicate and stale event cleanup. * Added a manually triggered workflow to run the sync securely with configurable apply behavior. * Added support for service-account authentication and optional interactive OAuth. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: test <test@test.com>
6.3 KiB
Release calendar sync
Keeps the "Fleet releases" Google Calendar in sync with the GitHub milestone
due dates on fleetdm/fleet. For each open milestone with a X.Y.Z title, the
script makes sure the calendar has:
Release day: minor release - X.Y.Z(orpatch releasewhenZis not0) on the milestone's due dateRelease candidate (next release - X.Y.Z)ending on the milestone's due dateDevelop (next release - X.Y.Z)ending ~2 weeks before the due date (skipped for out-of-band patch-like milestones)
A release day is labeled minor release when the version ends in .0 and
patch release otherwise (e.g. 4.89.0 is minor, 4.89.1 is a patch).
The script proposes changes by default (dry-run). Pass --apply to write
changes back to the calendar.
The script scans calendar events from 30 days ago through at least
--window-days (default 200) into the future, and always extends that window
to cover the furthest open milestone's due date. This matters: if the window
stopped short of a milestone, the events it created there would be invisible on
the next run and get re-created as duplicates every time.
How matching works
Calendar events are matched to milestones by date proximity, not by title. This means renumbering (e.g. inserting an out-of-band 4.88.0 that shifts every later minor) is handled automatically:
- A
Release day:event whose date matches milestone X.Y.Z's due date is retitled to X.Y.Z (regardless of its current version label). - An RC event whose end date matches a milestone's due date is retitled and its end is adjusted to exactly match.
- A Develop event whose end falls ~14 days before a milestone due date is matched to that milestone.
A milestone is considered out-of-band if its due date is fewer than 14 days after the previous milestone's due date (normal cadence is 21 days). Out-of-band milestones get a Release day + a short RC event only; they do not get a Develop sprint.
Duplicate events for the same version
There should only ever be one Release day / RC / Develop event per version.
If more than one event matches the same milestone (for example two
Release candidate (next release - 4.89.0) events), the script keeps a single
one — the oldest / pre-existing event (by the calendar's creation timestamp,
falling back to the earliest start date) — and deletes the extras. RC and
Develop events keep their existing start date; only their end date is
adjusted to track the milestone due date. (Release day is a single all-day event
anchored to the due date.)
Stale events when a due date moves
If a milestone's due date moves further than the match tolerance (5 days) — for
example 4.89.1 sliding from Jul 17 to Jul 24 — the old-date event no longer
matches the milestone, so a fresh event is created on the new date. The
leftover event on the old date is now stale, so the script deletes it
(rather than leaving a duplicate on the calendar). Only future stale events
are deleted; events whose date has already passed are left alone so release
history isn't rewritten.
Local usage (OAuth user login — easiest for testing)
Log in as yourself in the browser; no service account or calendar sharing needed. You just need an OAuth client ID (one-time, below).
cd tools/release/calendar-sync
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# Optional, raises GitHub rate limit:
export GITHUB_TOKEN=$(gh auth token)
# client_secret.json in this dir (see one-time setup below), then:
python sync.py --oauth # dry-run; opens a browser the first time
python sync.py --oauth --apply # apply changes
The first run opens a browser to approve access and caches a refreshable token
to token.json, so later runs don't prompt. Both client_secret.json and
token.json are gitignored.
One-time: create an OAuth client ID
- Google Cloud Console → APIs & Services → Library → enable Google Calendar API.
- APIs & Services → Credentials → Create credentials → OAuth client ID.
- Application type: Desktop app.
- (If prompted to configure the consent screen, pick Internal for a Workspace org, add yourself as a test user if External.)
- Download JSON and save it as
tools/release/calendar-sync/client_secret.json(or pointGCAL_OAUTH_CLIENT_SECRET/--client-secretat its path).
Local usage (service account)
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
# or:
export GCAL_SERVICE_ACCOUNT_JSON="$(cat /path/to/service-account.json)"
export GITHUB_TOKEN=$(gh auth token) # optional
python sync.py # dry-run, prints proposed changes
python sync.py --apply # apply changes
Auth precedence: --oauth forces the browser login; otherwise a service account
is used if GCAL_SERVICE_ACCOUNT_JSON or GOOGLE_APPLICATION_CREDENTIALS is set;
otherwise it falls back to the OAuth user flow.
Service-account setup (one-time)
- In Google Cloud Console, create (or pick) a project and enable the Google Calendar API.
- Create a service account. Generate a JSON key.
- Note the service account's email (looks like
release-calendar-sync@…iam.gserviceaccount.com). - Open the Fleet releases calendar in Google Calendar, go to Settings and sharing → Share with specific people or groups, and add the service account email with the Make changes to events permission.
- Store the JSON key contents in the GitHub secret
GCAL_SERVICE_ACCOUNT_JSONon thefleetdm/fleetrepo.
GitHub Action
The workflow .github/workflows/calendar-sync.yml is triggered manually
(workflow_dispatch). It dry-runs by default; check the job summary for
the proposed change list, then re-trigger with apply = true to write the
changes.
Editing defaults
Constants near the top of sync.py:
| Constant | Meaning |
|---|---|
CALENDAR_ID |
Calendar to manage. |
OUT_OF_BAND_GAP_DAYS |
Gap below which a milestone is treated as patch-like. |
PATCH_RC_DURATION_DAYS |
Length of the short RC window for out-of-band milestones. |
DEVELOP_END_TO_DUE_TARGET_DAYS |
Expected gap from Develop end to next release. |
The "RC ritual", "Create patch RC", and "Publish patch release" events are intentionally not touched by this script.