Release Publish Skill
Publish packages to NuGet.org and finalize releases.
⚠️ NO UNDO: This is step 3 of 3. See releasing.md for full workflow.
⚠️ Branch Protection (COMPLIANCE REQUIRED)
🛑 NEVER commit directly to
mainorskiasharpbranches. This is a policy violation.
| Repository | Protected Branches | Required Action |
|---|---|---|
| SkiaSharp (parent) | main |
Tags/releases created from release branches, never modify main directly |
| externals/skia (submodule) | main, skiasharp |
Never modify directly |
Publishing creates tags on existing release branches — it does NOT modify protected branches.
Workflow Overview
┌────────────────────────────────────────────────────────────────────┐
│ 1. Confirm Versions → Verify packages exist on preview feed │
│ 2. Publish to NuGet.org → Trigger Azure pipeline (manual) │
│ 3. Verify Published → Poll NuGet.org until indexed │
│ 4. Tag Release → Push git tag (ask_user first!) │
│ 5. Create GitHub Release→ Generate notes, set prerelease flag │
│ 6. Annotate Notes → Add platform/contributor emojis │
│ 7. Close Milestone → Stable releases only │
└────────────────────────────────────────────────────────────────────┘
Preview vs Stable differences:
| Step | Preview | Stable |
|---|---|---|
| 1. NuGet version | X.Y.Z-preview.N.{build} |
X.Y.Z (no build number) |
| 2. Pipeline checkbox | "Push Preview" | "Push Stable" |
| 4. Tag format | vX.Y.Z-preview.N.{build} |
vX.Y.Z |
| 5. GitHub Release | --prerelease flag |
No flag, attach samples |
| 7. Milestone | Skip | Close milestone |
Step 1: Confirm Versions
⚠️ Semver Version Ordering
When identifying which version to publish, use semver ordering, not alphabetical:
3.119.2(bare) is NEWER than3.119.2-preview.3— it's the stable/final release- Always verify you are publishing from the correct branch
- If both
release/3.119.2andrelease/3.119.2-preview.3exist, the bare version is the latest
Prerequisite: release-testing must have passed. Versions should be known from testing.
The user should provide:
- Preview: SkiaSharp version with build number (e.g.,
3.119.2-preview.2.3) - Stable: SkiaSharp base version only (e.g.,
3.119.2) — no build number
⚠️ Stable versions never include a build number. The build number only appears in the prerelease component (e.g., 3.119.2-preview.2.3) or in the internal stable tag (e.g., 3.119.2-stable.3). It is never appended to the base version directly.
If not provided, ask for them using ask_user.
Quick verification — confirm packages exist on preview feed:
# Preview: search for the exact NuGet version
dotnet package search SkiaSharp --source "https://aka.ms/skiasharp-eap/index.json" --exact-match --prerelease --format json | jq -r '.searchResult[].packages[].version' | grep "{expected-version}"
# Stable: search for internal stable builds (NuGet version is just the base, e.g., 3.119.2)
dotnet package search SkiaSharp --source "https://aka.ms/skiasharp-eap/index.json" --exact-match --prerelease --format json | jq -r '.searchResult[].packages[].version' | grep "^{base}-stable\."
If missing, STOP and ask user to verify testing was completed.
Step 2: Publish to NuGet.org
Trigger the publish pipeline to push packages to NuGet.org.
Pipeline Steps
- Open the NuGet.org publish pipeline
- Click "Run pipeline"
- Select "SkiaSharp" from the radio buttons
- Check "Confirm push to NuGet.org" checkbox
- For stable releases ONLY: Check "Push stable packages" checkbox
- ⚠️ Do NOT check this for preview releases
- Click "Next: Resources"
- In "Pipeline artifacts", click the SkiaSharp artifact selector
- From the branch dropdown, select
release/{version}(the release branch) - From the pipeline runs list, select the correct build by checking the build number
- Click "Use selected run"
- Click "Run"
Verification During Pipeline Run
⚠️ Before approving the push step, verify BOTH:
- Run name — The pipeline run will rename itself to the version being released. Confirm this matches your expected version.
- Push type — The publish step will indicate "Push Preview" or "Push Stable". Verify this matches your release type:
- Preview release → should show "Push Preview"
- Stable release → should show "Push Stable"
Only approve the push step when both are correct. Wait for pipeline completion (typically 5-10 minutes after approval).
Ask user to follow these steps and wait for completion.
Step 3: Verify Packages Published
Use curl to verify (more reliable than dotnet package search which has version limits):
# Check if packages exist - HTTP 200 = success
curl -s -o /dev/null -w "%{http_code}" "https://api.nuget.org/v3-flatcontainer/skiasharp/{version}/skiasharp.nuspec"
curl -s -o /dev/null -w "%{http_code}" "https://api.nuget.org/v3-flatcontainer/harfbuzzsharp/{version}/harfbuzzsharp.nuspec"
If packages not yet indexed, poll until available (NuGet.org can take 5-15 minutes):
# Poll every 30 seconds, max 10 minutes
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
skia=$(curl -s -o /dev/null -w "%{http_code}" "https://api.nuget.org/v3-flatcontainer/skiasharp/{version}/skiasharp.nuspec")
hb=$(curl -s -o /dev/null -w "%{http_code}" "https://api.nuget.org/v3-flatcontainer/harfbuzzsharp/{version}/harfbuzzsharp.nuspec")
echo "$(date +%H:%M:%S) - SkiaSharp: $skia, HarfBuzzSharp: $hb"
if [ "$skia" = "200" ] && [ "$hb" = "200" ]; then
echo "✅ Both packages available on NuGet.org!"
break
fi
sleep 30
done
Note: Use explicit list
1 2 3...instead of{1..20}brace expansion for better compatibility with async shell execution.
Or manually check: https://www.nuget.org/packages/SkiaSharp/{version}
Step 4: Tag Release
Tag formats:
- Preview:
vX.Y.Z-preview.N.{build}(e.g.,v3.119.2-preview.2.5) - Stable:
vX.Y.Z(e.g.,v3.119.2)
git fetch origin
git checkout release/{branch-version}
git pull
git tag {tag}
Confirm with ask_user before pushing tag (cannot be undone):
git push origin {tag}
Step 5: Create GitHub Release
Title Format
| Release Type | Title Format | Example |
|---|---|---|
| Preview | Version X.Y.Z (Preview N) |
Version 3.119.2 (Preview 2) |
| Stable | Version X.Y.Z |
Version 3.119.2 |
| Hotfix Preview | Version X.Y.Z.F (Preview N) |
Version 3.119.2.1 (Preview 1) |
| Hotfix Stable | Version X.Y.Z.F |
Version 3.119.2.1 |
Finding the Previous Release Tag
Always use --notes-start-tag to explicitly specify the previous release. The auto-selection may pick the wrong tag.
# List recent tags to find the previous release
git tag -l "v3.119*" --sort=-v:refname | head -10
| Current Release | Previous Tag (--notes-start-tag) |
|---|---|
v3.119.2-preview.2.3 |
v3.119.2-preview.1.2 (previous preview) |
v3.119.2-preview.1.1 |
v3.119.1 (last stable) |
v3.119.2 (stable) |
v3.119.2-preview.N.X (last preview of this version) |
v3.119.2.1-preview.1.1 (hotfix) |
v3.119.2 (stable being hotfixed) |
Commands
# Preview (e.g., v3.119.2-preview.2.3)
gh release create {tag} \
--title "Version {X.Y.Z} (Preview {N})" \
--generate-notes \
--notes-start-tag {previous-tag} \
--prerelease \
--verify-tag
# Stable (e.g., v3.119.2)
gh release create {tag} \
--title "Version {X.Y.Z}" \
--generate-notes \
--notes-start-tag {previous-tag} \
--verify-tag
# Upload samples for stable releases (if available)
gh release upload {tag} samples.zip
--titlesets the release title (use format above)--generate-notesauto-generates release notes from PRs/commits--notes-start-tagspecifies the previous release to diff from (required)--prereleasemarks as prerelease (preview only)--verify-tagensures the tag exists before creating the release
Step 6: Annotate Release Notes with Emojis
After creating the release, annotate each PR line with platform and community emojis.
👉 See references/release-notes.md for:
- Complete emoji reference (platform + contributor)
- Label-to-platform mapping
- Title keyword detection
- Full annotation process and examples
Quick summary:
- Get release body:
gh release view {tag} --json body -q '.body' > /tmp/skiasharp/release/release-body.md - For each PR: determine platform emoji, add ❤️ for non-mattleibow contributors
- Build sections: Breaking Changes (if any), New Features (if any), What's Changed (all)
- Update release:
gh release edit {tag} --notes-file /tmp/skiasharp/release/release-body.md
Step 7: Close Milestone (Stable only)
Skip for preview releases.
gh api repos/:owner/:repo/milestones --jq '.[] | "\(.number): \(.title)"'
gh api repos/:owner/:repo/milestones/{number} -X PATCH -f state=closed
Error Recovery
Pipeline Fails
| Failure Point | Recovery |
|---|---|
| Pipeline won't start | Verify branch name, check Azure DevOps permissions |
| Build fails mid-run | Check logs, fix issue on release branch, re-run pipeline |
| Approval rejected | Re-trigger pipeline with correct settings |
| Push step fails | Check NuGet.org status, retry pipeline |
NuGet.org Issues
| Issue | Recovery |
|---|---|
| Indexing takes >15 min | Normal for large packages. Keep polling. |
| Package shows 404 after publish | Wait up to 30 min. NuGet CDN propagation delay. |
| Wrong version published | Cannot unpublish. Release new corrected version. |
Git/GitHub Issues
| Issue | Recovery |
|---|---|
| Tag push rejected | Check if tag exists: git ls-remote --tags origin | grep {tag} |
| Tag already exists | Cannot delete. Must use different tag or release new version. |
| GitHub release fails | Re-run gh release create with --verify-tag |
| Release notes wrong | Edit with gh release edit {tag} --notes-file ... |
General Recovery
If you've partially completed and need to resume:
- Check what's done:
gh release view {tag}(release exists?),git ls-remote --tags origin(tag exists?) - Skip completed steps
- Continue from where you left off
Resources
- releasing.md — Version patterns, tag formats, workflow diagrams
- references/release-notes.md — Emoji annotation details
Converted and distributed by TomeVault — claim your Tome and manage your conversions.