Code Freezes
A code freeze blocks deployments to one or more environments for a period of time — a holiday, a release cutoff, or an incident window. On the Ybor platform you declare freezes declaratively as SyncCalendar resources in your organization's .platform repository and reference them from the environments they apply to. The platform turns them into Argo CD sync windows, so Argo CD stops syncing that environment's applications while the freeze is active.
Everything here is GitOps: you make the change with a pull request, review it, and merge it. There is no manual kubectl, and the freeze — including when it was added, changed, or lifted — is captured in your Git history.
What You'll Learn
- What a
SyncCalendaris and where it lives in your.platformrepository - How to declare freeze windows, both fixed calendar dates and recurring schedules
- How to apply a calendar to an environment with
syncCalendarRefs - How to roll a freeze out via GitOps and confirm it is active
How it works
- A
SyncCalendaris a reusable, named list of freeze/allow windows. It has no environment of its own. - An
Environmentopts in by listing calendar names inspec.syncCalendarRefs. One calendar can be shared by many environments. - When you merge the change, Argo CD syncs the resources and the platform compiles each window onto that environment's Argo CD project as a sync window.
- While a
denywindow is active, Argo CD will not sync the environment's applications — that is the freeze.allowwindows carve out exceptions inside a broaderdenywindow.
The SyncCalendar lives under the platform-resources/ folder of your organization's .platform repository; you opt an environment in from organization/environments.yaml. You do not need to manage any RBAC or Argo CD project permissions - the platform handles that for the SyncCalendar kind automatically.
Step 1 — Declare a SyncCalendar
Create (or edit) platform-resources/synccalendars.yaml in your .platform repository. A single calendar can hold your whole year of freezes as separate entries in the windows list:
# platform-resources/synccalendars.yaml
apiVersion: meta.p6m.dev/v1alpha1
kind: SyncCalendar
metadata:
name: company-holidays-2026
spec:
description: "2026 production change freezes"
windows:
# Fixed-date holiday that recurs every year (cron schedule)
- action: deny
schedule: "0 0 4 7 *" # July 4, 00:00
duration: "30h"
timeZone: "America/New_York"
applications: ["*"] # required: apply to every app in the environment
# Thanksgiving - a fixed calendar range for this year
- action: deny
startsAt: "2026-11-26T00:00:00-05:00"
endsAt: "2026-11-29T06:00:00-05:00"
timeZone: "America/New_York"
applications: ["*"]
# Year-end freeze - a multi-week range
- action: deny
startsAt: "2026-12-20T00:00:00-05:00"
endsAt: "2027-01-02T06:00:00-05:00"
timeZone: "America/New_York"
manualSync: false
applications: ["*"]
applications is required for a window to take effectA sync window only applies to the apps it selects. A window with no applications, namespaces, or clusters matches nothing, so the freeze silently does nothing. Set applications: ["*"] to freeze every application in the environment, or list specific app names (globs allowed) to narrow it.
Each entry is one window. You choose the style per entry (see Window types below):
- A fixed calendar range with
startsAt/endsAt(RFC3339 timestamps). Best for one-off events and holidays whose dates move year to year. - A recurring window with
schedule(cron) +duration. Best for dates that repeat, like a fixed-date holiday or a weekly window.
Step 2 — Reference it from an Environment
Open organization/environments.yaml and add syncCalendarRefs to each environment that should honor the freeze. An environment can reference more than one calendar:
# organization/environments.yaml
apiVersion: meta.p6m.dev/v1alpha1
kind: Environment
metadata:
name: prd
spec:
type: prd
# ...your existing environment fields...
syncCalendarRefs:
- name: company-holidays-2026
# - name: incident-freeze
Because a calendar carries no environment of its own, the same company-holidays-2026 can be referenced from stg, another org's environment, and so on — define it once, apply it wherever you need it.
Step 3 — Roll it out (GitOps)
- Commit the
synccalendars.yamlandenvironments.yamlchanges to a branch and open a pull request against your.platformrepository. - Get it reviewed and merge it.
- Argo CD syncs the resources, and the platform compiles the windows onto the environment's project. No manual steps.
To lift or change a freeze, edit the calendar (adjust or remove the window) and merge again. Freeze definitions live in the platform-resources/ config path, which is synced by a separate project that is never itself frozen - so you can always add, change, or lift a freeze, even during an active freeze.
Window types
Fixed calendar range (absolute)
- action: deny
startsAt: "2026-12-20T00:00:00-05:00"
endsAt: "2027-01-02T06:00:00-05:00"
timeZone: "America/New_York"
applications: ["*"]
Use startsAt / endsAt as RFC3339 timestamps that include the offset (for example -05:00). The window is active for exactly that range. This is the simplest choice for holidays.
Recurring window (cron)
- action: deny
schedule: "0 17 * * 5" # Fridays at 17:00
duration: "63h" # ...through Monday 08:00
timeZone: "America/New_York"
applications: ["*"]
Use schedule (a standard cron expression marking when the window opens) plus duration (a Go duration such as 63h or 1h30m). The window reopens every time the cron matches.
The schedule is evaluated in the window's timeZone, and the cron is minute-resolution. Set timeZone explicitly so a freeze fires at the intended local time rather than UTC.
Example: a full year of US bank holidays
A common policy is to freeze deployments around each holiday, not just on the day itself: no changes land the day before or the day after, so nobody ships right before leaving or on the way back. Each entry below is a fixed range from 00:00 the day before the holiday through 00:00 two days later - covering the day before, the holiday, and the day after.
Offsets follow US Eastern time, which is -05:00 (EST) and -04:00 (EDT) after daylight saving begins on 2026-03-08 and ends on 2026-11-01. Adjust the dates, offsets, and timeZone for your own region and holiday calendar.
# platform-resources/synccalendars.yaml
apiVersion: meta.p6m.dev/v1alpha1
kind: SyncCalendar
metadata:
name: us-bank-holidays-2026
spec:
description: "2026 US federal (bank) holidays, with a 1-day buffer on each side"
windows:
# New Year's Day - Thu Jan 1
- action: deny
startsAt: "2025-12-31T00:00:00-05:00"
endsAt: "2026-01-03T00:00:00-05:00"
timeZone: "America/New_York"
applications: ["*"]
# Martin Luther King Jr. Day - Mon Jan 19
- action: deny
startsAt: "2026-01-18T00:00:00-05:00"
endsAt: "2026-01-21T00:00:00-05:00"
timeZone: "America/New_York"
applications: ["*"]
# Presidents' Day - Mon Feb 16
- action: deny
startsAt: "2026-02-15T00:00:00-05:00"
endsAt: "2026-02-18T00:00:00-05:00"
timeZone: "America/New_York"
applications: ["*"]
# Memorial Day - Mon May 25
- action: deny
startsAt: "2026-05-24T00:00:00-04:00"
endsAt: "2026-05-27T00:00:00-04:00"
timeZone: "America/New_York"
applications: ["*"]
# Juneteenth - Fri Jun 19
- action: deny
startsAt: "2026-06-18T00:00:00-04:00"
endsAt: "2026-06-21T00:00:00-04:00"
timeZone: "America/New_York"
applications: ["*"]
# Independence Day - Sat Jul 4 (observed Fri Jul 3)
- action: deny
startsAt: "2026-07-03T00:00:00-04:00"
endsAt: "2026-07-06T00:00:00-04:00"
timeZone: "America/New_York"
applications: ["*"]
# Labor Day - Mon Sep 7
- action: deny
startsAt: "2026-09-06T00:00:00-04:00"
endsAt: "2026-09-09T00:00:00-04:00"
timeZone: "America/New_York"
applications: ["*"]
# Columbus Day - Mon Oct 12
- action: deny
startsAt: "2026-10-11T00:00:00-04:00"
endsAt: "2026-10-14T00:00:00-04:00"
timeZone: "America/New_York"
applications: ["*"]
# Veterans Day - Wed Nov 11
- action: deny
startsAt: "2026-11-10T00:00:00-05:00"
endsAt: "2026-11-13T00:00:00-05:00"
timeZone: "America/New_York"
applications: ["*"]
# Thanksgiving - Thu Nov 26
- action: deny
startsAt: "2026-11-25T00:00:00-05:00"
endsAt: "2026-11-28T00:00:00-05:00"
timeZone: "America/New_York"
applications: ["*"]
# Christmas Day - Fri Dec 25
- action: deny
startsAt: "2026-12-24T00:00:00-05:00"
endsAt: "2026-12-27T00:00:00-05:00"
timeZone: "America/New_York"
applications: ["*"]
Reference it from an environment exactly like any other calendar:
# organization/environments.yaml
spec:
syncCalendarRefs:
- name: us-bank-holidays-2026
Behavior and gotchas
manualSync: false (the default effect for a freeze) blocks all syncs during a deny window, including manual ones — a hard freeze. Set manualSync: true if you still want to be able to trigger a manual sync during the window.
If a referenced calendar is missing, or a window is malformed, the platform records it on the environment (a SyncCalendarsResolved: False condition and a warning event) and lets deployments proceed rather than wedging your pipeline. The trade-off is that the intended freeze is then not enforced — so after adding or editing a freeze, check that SyncCalendarsResolved is True.
- A window must use either the absolute form (
startsAt/endsAt) or the recurring form (schedule/duration) - not both. actiondefaults todeny. Useallowonly to carve out an exception window.- Set
applications(use["*"]for the whole environment). A window with noapplications,namespaces, orclustersselector matches no applications, so the freeze does nothing.
Reference
SyncCalendar window fields
| Field | Description |
|---|---|
action | deny blocks syncs (a freeze); allow permits them. Defaults to deny. |
startsAt / endsAt | RFC3339 timestamps (with offset) for a fixed calendar range. Use instead of schedule/duration. |
schedule | Cron expression marking when a recurring window opens. |
duration | How long a recurring window stays open, as a Go duration (e.g. 63h, 1h30m). |
timeZone | IANA time zone the window is evaluated in (e.g. America/New_York). |
manualSync | When true, manual syncs are still allowed during a deny window. Omit for a hard freeze. |
applications / namespaces / clusters | Selectors for which apps the window applies to. At least one must be set - a window with none matches no apps. Use applications: ["*"] for the whole environment, or list specific app names / namespaces / clusters to narrow it. |
Environment.spec.syncCalendarRefs
A list of references, each with a name that matches a SyncCalendar in your organization. Multiple entries are allowed; their windows are combined.
Related
- Platform Repository Anatomy — the structure of your
.platformrepository - ArgoCD Deployments — how GitOps deployment works on the platform