| name | release-branch |
| description | Create a release branch for SkiaSharp. Use when user says "release X", "start release X", "create release branch for X", "I want to release", or "release now". This is the FIRST step of releasing - creates branch and pushes to trigger CI. Can auto-detect next preview version from main branch.
|
Release Branch Skill
Create release branches for SkiaSharp versions.
⚠️ NO UNDO: This is Step 1 of 4 in the release pipeline. See releasing.md for full workflow.
Pipeline: Step 1 (this skill) → Step 2: release-status → Step 3: release-testing → Step 4: release-publish
⚠️ Branch Protection (COMPLIANCE REQUIRED)
🛑 NEVER commit directly to main or skiasharp branches. This is a policy violation.
| Repository | Protected Branches | Required Action |
|---|
| SkiaSharp (parent) | main | Create release/X.Y.Z branch, never commit to main |
| externals/skia (submodule) | main, skiasharp | Must use feature branch if submodule changes needed |
Release branches are cut FROM an integration branch (main or release/X.Y.x), but never modify those branches directly — always go through a branch + PR.
mono/skia counterpart: every SkiaSharp release/{version} branch needs an
identically-named release/{version} branch in mono/skia at the
externals/skia submodule commit (see Step 5).
release/* is not protected in mono/skia, so this is a plain ref creation — not a PR.
Concept: Integration Branches
There is not one "main" — there is an integration branch per release line.
Each one always sits at the next unreleased version for its line, with
PREVIEW_LABEL: preview.0:
| Integration branch | Line it serves | Example state |
|---|
main | Newest in-development line (not yet forked) | 4.150.0 / preview.0 |
release/X.Y.x | An established / maintenance line | release/3.119.x @ 3.119.5; release/4.148.x @ 4.148.0 |
- Every release (preview, rc, stable, patch) is cut FROM the line's
integration branch —
release/{version} is branched off it.
- As soon as a stable is cut, the integration branch is bumped to the
next version (see Step 6) — immediately, not after the release publishes.
After branching, PRs keep merging into the integration branch, and they must
land on the next version, not the one that was just cut. Fixes for the cut
release go on its own
release/{version} branch.
- A new minor's
release/X.Y.x is forked from main when stabilization begins;
main is then bumped to the next minor. From that point on, all X.Y.*
releases come from release/X.Y.x, not main.
Step 1: Determine Version
Auto-detect (user says "release now")
- Fetch the integration branch (
main for the newest line, or release/X.Y.x for a maintenance line) and read SKIASHARP_VERSION from scripts/azure-templates-variables.yml
- List existing branches:
git branch -r | grep "release/{version}-preview"
- Next preview = highest + 1 (or 1 if none)
- ⚠️ Semver check: Also verify no bare
release/{version} branch exists — if it does, the stable release is already cut and you should NOT create another preview. Ask the user to confirm.
- Confirm with user: "Next release will be
X.Y.Z-preview.N. Proceed?"
User provides version
Use the provided version directly.
Step 2: Determine Release Type
⚠️ Semver ordering: A bare version X.Y.Z is ALWAYS newer than X.Y.Z-preview.N. When listing
branches to find the latest, remember that release/3.119.2 > release/3.119.2-preview.3.
Do NOT use alphabetical sorting — it gives wrong results for semver.
| Version Format | Type | Base (integration branch) | PREVIEW_LABEL |
|---|
X.Y.Z-preview.N / X.Y.Z-rc.N | Preview / RC | release/X.Y.x (or main if the line isn't forked yet) | preview.N / rc.N |
X.Y.Z | Stable | release/X.Y.x | stable |
X.Y.Z.F-preview.N | Hotfix Preview | tag vX.Y.Z | preview.N |
X.Y.Z.F | Hotfix Stable | release/X.Y.Z.F-preview.{latest} | stable |
Base = the line's integration branch, NOT a previous preview/rc branch.
A stable X.Y.Z is cut from release/X.Y.x (which already produced its
previews/rcs), not from release/X.Y.Z-preview.{latest}.
Find the integration branch:
git branch -r | grep -E "release/X\.Y\.x$"
NuGet version format by release type:
- Preview:
{base}-{PREVIEW_LABEL}.{build} (e.g., 3.119.2-preview.2.3) — build number is part of the prerelease tag
- Stable:
{base} only (e.g., 3.119.2) — the build number is NEVER appended to stable versions. On the internal feed, stable builds appear as {base}-stable.{build} but the published version is just {base}.
Step 3: Create Branch and Update PREVIEW_LABEL
- Checkout the base — the line's integration branch (
release/X.Y.x, or
main / a tag for the special cases above)
- Create branch
release/{version}
- Set
PREVIEW_LABEL with the helper script (edits + verifies
scripts/azure-templates-variables.yml):
pwsh .agents/skills/release-branch/scripts/bump-version.ps1 -PreviewLabel {label}
- Commit:
git commit -m "Bump the version to {version}"
- Show diff summary to user and confirm with the user before pushing
Step 4: Push Branch
git push -u origin release/{version}
This triggers the CI pipeline chain (2-4 hours total):
SkiaSharp-Native → SkiaSharp → SkiaSharp-Tests
(~60-90 min) (~30-60 min) (~15-30 min)
CI builds Skia from the pinned submodule commit, so it runs regardless of the
counterpart branch in Step 5 — that branch exists to preserve the source, not to
feed the build.
Step 5: Create the mono/skia Counterpart Branch
Every SkiaSharp release/{version} branch must have an identically-named
release/{version} branch in the mono/skia fork, pointing at the exact
externals/skia submodule commit that the SkiaSharp branch references. This
locks/preserves the Skia source for the release so it stays auditable, reproducible,
and safe from garbage collection.
This applies to every release branch cut — preview, rc, stable, and the
release/X.Y.x integration forks — not just stables.
- Read the submodule SHA the release branch points at (no submodule init needed):
SKIA_SHA=$(git rev-parse HEAD:externals/skia)
- Create the matching branch in mono/skia at that commit:
gh api repos/mono/skia/git/refs \
-f ref="refs/heads/release/{version}" \
-f sha="$SKIA_SHA"
- The branch name must match the SkiaSharp branch name exactly.
release/* is not a protected branch in mono/skia, so this ref creation is allowed.
- If it already exists (HTTP 422 "Reference already exists"), verify it points at
$SKIA_SHA and move on — do not force-update.
- Verify the counterpart matches:
gh api repos/mono/skia/branches/release/{version} --jq '.commit.sha'
Step 6: Bump the Integration Branch (Stable Only)
Do this right after cutting & pushing the branch (Steps 3–5) — as soon as the stable release branch is cut and
pushed, advance that line's integration branch to the next version. Do not
wait for the release to publish.
Why immediately: once release/{version} is branched off, PRs keep merging into
the integration branch (release/X.Y.x, and main for its line). Those changes
must land on the next version, not the one that was just cut. Any fixes for
the cut release go onto its own release/{version} branch instead. If the
integration branch isn't bumped at cut time, post-branch merges collide with the
released version.
Previews/RCs do not trigger this — they're iterations toward the same
X.Y.Z. Cutting the stable does, because that finalizes X.Y.Z. (e.g. when
3.119.4 was cut, release/3.119.x was bumped to 3.119.5 — "Bump to the next
version after release".)
How each line advances:
| Released line | Integration branch | How it advances |
|---|
| Maintenance line | release/X.Y.x | Next patch (X.Y.Z → X.Y.(Z+1)) — use the helper below. assembly stays pinned at X.Y.0.0; only file / nuget / SKIASHARP_VERSION move. |
| Newest line | main | Next minor (X.Y → next milestone). This is a Skia milestone update (e.g. [skia-sync] Update Skia to milestone mNNN) that also rewrites the milestone/soname/increment lines, assembly, cgmanifest.json and native sources — a separate process, not this helper. |
The helper below covers the maintenance-line patch bump only. It refuses a
major.minor change (that's a milestone update — out of scope).
-
Create branch bump-version-{next} from the maintenance branch
(e.g. git checkout -b bump-version-{next} origin/release/X.Y.x)
-
Apply the patch bump with the helper script. It edits + verifies
SKIASHARP_VERSION in azure-templates-variables.yml and all SkiaSharp /
HarfBuzzSharp file+nuget lines in VERSIONS.txt (SkiaSharp assembly
stays X.Y.0.0, HarfBuzzSharp assembly stays 1.0.0.0):
pwsh .agents/skills/release-branch/scripts/bump-version.ps1 \
-SkiaSharpVersion {next} \
-HarfBuzzSharpVersion {hb-next} \
-PreviewLabel preview.0
{hb-next}: the next HarfBuzzSharp version — normally increment/append the
4th digit in lockstep with the SkiaSharp patch (14.2.0 → 14.2.0.1,
8.3.1.4 → 8.3.1.5); only on an actual native HarfBuzz upgrade do you
reset to the 3-digit native version (e.g. 14.3.0).
-
Commit: git commit -m "Bump to the next version ({next}) after release"
-
Show diff to user, then:
git push -u origin bump-version-{next}
gh pr create --title "Bump to the next version ({next}) after release" --body ""
gh pr merge --merge --delete-branch
Next: Track the Build
Once the branch is cut and pushed (Steps 3–4), its skia counterpart created
(Step 5), and — for a stable — the integration branch bumped (Step 6), hand off to
Step 2 of the release pipeline, the release-status
skill, to follow the CI chain (SkiaSharp-Native → SkiaSharp → SkiaSharp-Tests).
Quick check:
python3 .agents/skills/release-status/scripts/pipeline-status.py release/{version}
Resources
- scripts/bump-version.ps1 — Sets
PREVIEW_LABEL and/or bumps SkiaSharp + HarfBuzzSharp versions in VERSIONS.txt and azure-templates-variables.yml, with a verification gate (-DryRun to preview)
- releasing.md — Version patterns, HarfBuzz versioning, workflow diagrams