Back to Skiasharp

Release Branch Skill

.agents/skills/release-branch/SKILL.md

4.150.010.9 KB
Original Source

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-statusStep 3: release-testingStep 4: release-publish

⚠️ Branch Protection (COMPLIANCE REQUIRED)

🛑 NEVER commit directly to main or skiasharp branches. This is a policy violation.

RepositoryProtected BranchesRequired Action
SkiaSharp (parent)mainCreate release/X.Y.Z branch, never commit to main
externals/skia (submodule)main, skiasharpMust 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 branchLine it servesExample state
mainNewest in-development line (not yet forked)4.150.0 / preview.0
release/X.Y.xAn established / maintenance linerelease/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")

  1. 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
  2. List existing branches: git branch -r | grep "release/{version}-preview"
  3. Next preview = highest + 1 (or 1 if none)
  4. ⚠️ 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.
  5. 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 FormatTypeBase (integration branch)PREVIEW_LABEL
X.Y.Z-preview.N / X.Y.Z-rc.NPreview / RCrelease/X.Y.x (or main if the line isn't forked yet)preview.N / rc.N
X.Y.ZStablerelease/X.Y.xstable
X.Y.Z.F-preview.NHotfix Previewtag vX.Y.Zpreview.N
X.Y.Z.FHotfix Stablerelease/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:

bash
git 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:

  • 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

  1. Checkout the base — the line's integration branch (release/X.Y.x, or main / a tag for the special cases above)
  2. Create branch release/{version}
  3. Set PREVIEW_LABEL with the helper script (edits + verifies scripts/azure-templates-variables.yml):
    bash
    pwsh .agents/skills/release-branch/scripts/bump-version.ps1 -PreviewLabel {label}
    # {label} = stable | preview.N | rc.N   (add -DryRun to preview)
    
  4. Commit: git commit -m "Bump the version to {version}"
  5. Show diff summary to user and confirm with the user before pushing

Step 4: Push Branch

bash
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.

  1. Read the submodule SHA the release branch points at (no submodule init needed):
    bash
    SKIA_SHA=$(git rev-parse HEAD:externals/skia)
    
  2. Create the matching branch in mono/skia at that commit:
    bash
    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.
  3. Verify the counterpart matches:
    bash
    gh api repos/mono/skia/branches/release/{version} --jq '.commit.sha'   # == $SKIA_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 lineIntegration branchHow it advances
Maintenance linerelease/X.Y.xNext patch (X.Y.ZX.Y.(Z+1)) — use the helper below. assembly stays pinned at X.Y.0.0; only file / nuget / SKIASHARP_VERSION move.
Newest linemainNext 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).

  1. Create branch bump-version-{next} from the maintenance branch (e.g. git checkout -b bump-version-{next} origin/release/X.Y.x)

  2. 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):

    bash
    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.014.2.0.1, 8.3.1.48.3.1.5); only on an actual native HarfBuzz upgrade do you reset to the 3-digit native version (e.g. 14.3.0).
  3. Commit: git commit -m "Bump to the next version ({next}) after release"

  4. Show diff to user, then:

    bash
    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:

bash
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