Files
George Karrandtest 6d68bd6487 Adding milestone release calendar sync workflow (#48593)
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>
2026-07-22 14:57:50 -05:00

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 (or patch release when Z is not 0) on the milestone's due date
  • Release candidate (next release - X.Y.Z) ending on the milestone's due date
  • Develop (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

  1. Google Cloud Console → APIs & Services → Library → enable Google Calendar API.
  2. 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.)
  3. Download JSON and save it as tools/release/calendar-sync/client_secret.json (or point GCAL_OAUTH_CLIENT_SECRET / --client-secret at 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)

  1. In Google Cloud Console, create (or pick) a project and enable the Google Calendar API.
  2. Create a service account. Generate a JSON key.
  3. Note the service account's email (looks like release-calendar-sync@…iam.gserviceaccount.com).
  4. 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.
  5. Store the JSON key contents in the GitHub secret GCAL_SERVICE_ACCOUNT_JSON on the fleetdm/fleet repo.

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.