RFC-0022: Release Automation and Unified Canary/Stable Branches #5457
Replies: 4 comments
-
|
📝 RFC Document Updated View changes: Commit History |
Beta Was this translation helpful? Give feedback.
0 replies
-
|
📝 RFC Document Updated View changes: Commit History |
Beta Was this translation helpful? Give feedback.
0 replies
-
|
📝 RFC Document Updated View changes: Commit History |
Beta Was this translation helpful? Give feedback.
0 replies
-
|
📝 RFC Document Updated View changes: Commit History |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
📄 RFC Doc: 0022-release-automation.md
Release Automation and Unified Canary/Stable Branches
Authors: @lalitm
Status: Draft
Problem
Perfetto's release process today is a patchwork of partially-automated scripts
and manual steps spread across three loosely-coupled pipelines — the SDK/native
binaries, the UI, and the Python package. Each has its own versioning model,
its own branching convention, and its own notion of "canary" vs "stable". The
result is a release workflow that is:
Manual and error-prone.
tools/release/release_perfetto.pyrequires ahuman to run it locally, answer prompts, wait on LUCI builds, hand-upload
artifacts, and manually create the GitHub release. A single release is a
multi-hour, multi-terminal babysitting exercise.
Fragmented across artifacts. The SDK uses
releases/vX.xmaintenancebranches and
vX.Ytags. The UI usesui-canary/ui-stablebranchesplus a
channels.jsonfile inmainthat pins each channel to a specificSHA. The Python package uses a hardcoded version string in
python/setup.pyand a separate
release_python.pyscript that is not run as part of thenormal release flow. These three things drift: the UI on
ui-stablemaycorrespond to a completely different commit than the SDK tagged
vX.Y.No clear "release-in-progress" state. There is no single branch a
contributor can look at and say "this is what v54 will contain". The
decision of what goes into v54 is implicit in whoever ran the release
script and when.
UI channels driven by a file, not a branch.
channels.jsonwas apragmatic choice when it was introduced but it means:
one to
mainto bump the SHA).named
ui-canary.maincan redirect a channel.No PyPI automation. Python releases happen rarely and out-of-band from
SDK releases. The version in
setup.pyis updated by hand, and pushing toPyPI is a manual
twine upload.LUCI build completion is not observable from GitHub. Builds are
triggered by tag push (
refs/tags/upstream/v.+) but there is no hook backinto the GitHub release. Whoever cut the release has to watch LUCI, wait
for all four platforms to finish, download the artifacts, and upload them
to the release by hand.
We want to get to a world where cutting a release is a small number of button
clicks in the GitHub UI, where the branch state at any moment tells you
exactly what is in flight, and where the SDK, UI, and Python package move
together.
Decision
Pending.
Design
Summary
Replace the per-release
releases/vX.xbranches and the UI'schannels.jsonfile with two long-lived branches —
canaryandstable— that are shared bythe SDK, the UI, and the Python package. All transitions between branches
(
main→canary,canary→stable) are triggered by clickingworkflow_dispatchbuttons in the GitHub Actions UI. Thestablepromotionis what tags the release, and the tag push is the single event that fans out
to LUCI (native binaries), Cloud Build (UI), and GitHub Actions (PyPI + GitHub
Release creation). LUCI artifact attachment is reconciled by a final
human-triggered "finalize release" button.
Independent artifact producers (LUCI, Cloud Build, GH Actions) converge on
the same GitHub release using a "create draft if missing, else upload to
existing" idempotent pattern, so no producer needs to wait for any other.
Branches
Three long-lived branches, each protected (no direct pushes; PRs only):
maincanarymain→canary(fast-forward)stablecanary→stable(fast-forward + tag)The existing
releases/vX.xbranches are frozen in place as historicalartifacts. We do not create new ones and we do not backport to them. This
matches current practice: we already only support the latest release.
The existing
ui-canaryandui-stablebranches go away, replaced by theunified
canary/stable. Thechannels.jsonfile is deleted.Version numbering
The version is derived from the top of the
CHANGELOGfile, as today, viatools/write_version_header.py. This now becomes the source of truth forevery artifact:
vX.Y(C++ header generated at build time).vX.Y-<9-char-SHA>(as today).0.X.Y(auto-derived fromCHANGELOGat wheel-build time;the hardcoded string in
python/setup.pyis removed). The0.prefixkeeps the package in the pre-1.0 series while encoding the Perfetto
release in the minor/patch components, giving a monotonic jump from the
current
0.16.0.The CHANGELOG is bumped in a normal PR to
mainbefore themain→canaryfast-forward. The person cutting canary is responsible for confirming that
CHANGELOGalready contains the intendedvX.Yentry.Release lifecycle
The four buttons
All four are
workflow_dispatchworkflows under.github/workflows/.Button 1:
cut-canary.yml. Creates a merge commit oncanarywhosetree equals the current tip of
main, using thegit merge -s ours+git read-tree -m -upattern (same approach as the existingmerge-main-to-canary.yml). This always moves forward (no force-pushneeded) and handles the normal case where cherry-picks on
canaryhavedifferent SHAs than their
maincounterparts, so a true "fast-forward"would never succeed after one release cycle.
Convention: every fix lands on
mainfirst and is then cherry-picked tocanary. Anything that exists only oncanarywill be silently dropped atthe next cut, because we're replacing
canary's tree withmain's. Thedropped commit is still in canary's git history and can be re-cherry-picked
if needed. Opens no PR; the push itself is the event. Pushes to
canarytrigger Cloud Build to redeploy the canary UI.
Button 2:
promote-stable.yml. Applies the same tree-replacing mergepattern to push
canary's tree ontostable, then creates and pushes thevX.Ytag (version read fromCHANGELOG) pointing at the resultingstableHEAD. This single action is what triggers every downstream build.Button 3:
finalize-release.yml. Takes a version input (e.g.v54.0).Downloads the LUCI artifacts from GCS, verifies they match the expected
manifest, and attaches them to the draft GitHub release. Leaves the
release as a draft — a maintainer reviews the release notes and clicks
"Publish" manually from the GitHub UI. Idempotent — safe to re-run.
Cherry-picks are opened as regular PRs targeting
canary. There is no"hotfix directly to stable" path: even emergency fixes go
main→canary→
stablevia the normal buttons. This keeps the invariant thatstableisalways an ancestor of
canary, which in turn keeps the promotion button apure fast-forward.
Artifact flow on tag push
The
vX.Ytag push is the single event that fans out. Four independentproducers each converge on the same GitHub release using the
"create-if-missing, upload-on-exists" pattern:
Producers:
LUCI native builds (Linux, macOS, Windows, Android). Triggered as
today by the
refs/tags/upstream/v.+scheduler rule. Each builder alreadyuploads to
gs://perfetto-luci-artifacts/<git-revision>/<arch>/<binary>(with a parallel
latest/alias), and the SDK source zips land atgs://perfetto-luci-artifacts/<version>/sdk/. Button 3 reconciles theseexisting paths into the GitHub release — no new bucket or layout is
required.
UI Cloud Build. The existing trigger (formerly keyed on changes to
channels.json) is re-keyed to fire on pushes to thestablebranch.Builds the UI from
stableHEAD and deploys to the stable channel.PyPI publish workflow (
publish-pypi.yml). Triggered by tag push.Builds a pure-Python wheel with version derived from
CHANGELOG, publishesto PyPI using OIDC trusted publishing (no long-lived token). Does not ship
native binaries in v1.
GitHub Release draft (
draft-release.yml). Triggered by tag push.Creates the draft release; the body is pre-populated with the
corresponding
vX.Ysection of theCHANGELOGverbatim, so even if nohuman touches it the release still has useful content. The draft exists
from the moment the tag is pushed; LUCI artifacts are attached
asynchronously.
Release notes themselves are human-authored, not automated. Before
clicking
finalize-releasea maintainer is expected to edit the draft'sbody in the GitHub UI to replace the raw CHANGELOG with prose release
notes — thematic grouping, highlights, links to docs, etc. The existing
tools/release/gen_release_notes.pyscript (an AI-prompt generator forexactly this authoring step) is retained for this workflow. It is
deliberately not invoked from CI: the release notes are a curated
artifact, and the cost of a bad auto-generated announcement is higher
than the cost of one manual editing step per release.
LUCI → GitHub bridge
The tag-push → LUCI path already works today. The missing piece is notifying
GitHub when LUCI is done so artifacts can be attached.
For v1 we deliberately avoid a webhook. Instead, button 3 (
finalize-release)is a manually-triggered GH Actions workflow that a human clicks once LUCI is
green. It reads from the existing
gs://perfetto-luci-artifacts/paths(which are world-readable via
storage.cloud.google.com) and uploads intothe draft release. This keeps the secret surface minimal — GH Actions already
has
GITHUB_TOKEN, and no GCS credential is needed since the bucket ispublic — at the cost of one extra click per release.
UI:
channels.jsonremovalThe
channels.jsonfile and the associatedui-canary/ui-stablebranchesgo away. The UI Cloud Build trigger is reconfigured to build:
autopushchannel on every push tomain(as today).canarychannel on every push tocanary.stablechannel on every push tostable.Because
canaryandstableare protected branches, there is no way toaccidentally redirect a channel without going through a PR, which is a
strictly stronger guarantee than
channels.jsonprovides today.The UI version string (
vX.Y-<SHA>) continues to be computed fromCHANGELOGchannels.jsonis not a public API, but as it has lived in the repo for along time the removal will be called out in the CHANGELOG and in the first
release's GitHub release notes.
Python packaging
For v1, the PyPI release is a pure-Python wheel (no bundled
trace_processor_shell) on stable tags only. The version is auto-derivedfrom
CHANGELOG, killing the hardcoded0.16.0inpython/setup.py.Bundling
trace_processor_shellinto the wheel — so thatpip install perfettogives you a working CLI — is an attractive future step but hasnon-trivial packaging implications (multi-platform wheels, LUCI artifact →
PyPI pipeline, manylinux compatibility). Out of scope for v1.
What happens to existing scripts
tools/release/release_perfetto.py: deleted. Its responsibilities aresplit across the four GH Actions workflows.
tools/release/release_python.py: deleted. Replaced bypublish-pypi.yml.tools/release/gen_release_notes.py: retained. Invoked by the draft-releaseworkflow if
--generate-notesis insufficient.tools/release/package-github-release-artifacts: retained, invoked fromLUCI builders to produce the per-platform zips.
tools/release/roll-prebuilts: unchanged (separate concern: rollingprebuilts into the repo, not cutting a release).
Protected branch configuration
main,canary,stable: all require PR, no direct push, noforce-push, require status checks.
canaryandstableadditionally require a review from a designatedrelease-approver group (to prevent drive-by cherry-picks).
vX.Ytag namespace is restricted to being created by thepromote-stable.ymlworkflow's GH App / token (not by humans).Alternatives considered
Keep
releases/vX.xmaintenance branchesPro:
Con:
years ago.
which has no meaning for how we actually develop.
Keep
channels.jsonPro:
Con:
ui-stablebranch exists but is not the source of truth.LUCI → GitHub via webhook instead of manual finalize button
Pro:
Con:
credential.
one more at the end is marginal.
Revisit once the rest of the flow is proven.
Tag on canary cut instead of on stable promotion
Pro:
Con:
vX.Y-canary,vX.Y) is more machinery.marginal benefit.
need binary prereleases.
Monorepo-style unified version (e.g.
54.0.0everywhere)Pro:
Con:
vX.Yconvention and existing consumers' parsers.X.Y.Zsemantics don't quite map (we never ship.Zpatchversions).
Keep the existing
vX.Yfor SDK/UI; map it to0.X.Yfor Python.Open questions
None at this time.
💬 Discussion Guidelines:
Beta Was this translation helpful? Give feedback.
All reactions