.agents/skills/release-branch/SKILL.md
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
🛑 NEVER commit directly to
mainorskiasharpbranches. 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-namedrelease/{version}branch in mono/skia at theexternals/skiasubmodule commit (see Step 5).release/*is not protected in mono/skia, so this is a plain ref creation — not a PR.
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 |
release/{version} is branched off it.release/{version} branch.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.main for the newest line, or release/X.Y.x for a maintenance line) and read SKIASHARP_VERSION from scripts/azure-templates-variables.ymlgit branch -r | grep "release/{version}-preview"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.X.Y.Z-preview.N. Proceed?"Use the provided version directly.
⚠️ 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.Zis cut fromrelease/X.Y.x(which already produced its previews/rcs), not fromrelease/X.Y.Z-preview.{latest}.Find the integration branch:
bashgit branch -r | grep -E "release/X\.Y\.x$" # established line (substitute real X.Y) # newest line not yet forked → use main
NuGet version format by release type:
{base}-{PREVIEW_LABEL}.{build} (e.g., 3.119.2-preview.2.3) — build number is part of the prerelease tag{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}.release/X.Y.x, or
main / a tag for the special cases above)release/{version}PREVIEW_LABEL with the helper script (edits + verifies
scripts/azure-templates-variables.yml):
pwsh .agents/skills/release-branch/scripts/bump-version.ps1 -PreviewLabel {label}
# {label} = stable | preview.N | rc.N (add -DryRun to preview)
git commit -m "Bump the version to {version}"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.
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.xintegration forks — not just stables.
SKIA_SHA=$(git rev-parse HEAD:externals/skia)
gh api repos/mono/skia/git/refs \
-f ref="refs/heads/release/{version}" \
-f sha="$SKIA_SHA"
release/* is not a protected branch in mono/skia, so this ref creation is allowed.$SKIA_SHA and move on — do not force-update.gh api repos/mono/skia/branches/release/{version} --jq '.commit.sha' # == $SKIA_SHA
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.minorchange (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
# add -DryRun first to preview
{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
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}
PREVIEW_LABEL and/or bumps SkiaSharp + HarfBuzzSharp versions in VERSIONS.txt and azure-templates-variables.yml, with a verification gate (-DryRun to preview)