mono / release-publish

Publish SkiaSharp packages and finalize the release. Use when user says "publish X", "finalize X", "tag X", or "finish release X". This is the FINAL step - after release-testing passes. Publishes to NuGet.org, creates tag, GitHub release, and closes milestone. Triggers: "publish the release", "push to nuget", "create github release", "tag the release", "close the milestone", "annotate release notes", "testing passed what's next", "finalize 3.119.2", "release is ready".

0 views
0 installs

Skill Content

---
name: release-publish
description: >
  Publish SkiaSharp packages and finalize the release.
  
  Use when user says "publish X", "finalize X", "tag X", or "finish release X".
  
  This is the FINAL step - after release-testing passes.
  Publishes to NuGet.org, creates tag, GitHub release, and closes milestone.
  
  Triggers: "publish the release", "push to nuget", "create github release",
  "tag the release", "close the milestone", "annotate release notes",
  "testing passed what's next", "finalize 3.119.2", "release is ready".
---

# Release Publish Skill

Publish packages to NuGet.org and finalize releases.

⚠️ **NO UNDO:** This is step 3 of 3. See [releasing.md](../../../documentation/releasing.md) for full workflow.

## ⚠️ 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` | 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 than `3.119.2-preview.3` — it's the stable/final release
- Always verify you are publishing from the correct branch
- If both `release/3.119.2` and `release/3.119.2-preview.3` exist, 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:
```bash
# 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](https://dev.azure.com/devdiv/DevDiv/_build?definitionId=25298) to push packages to NuGet.org.

### Pipeline Steps

1. Open the [NuGet.org publish pipeline](https://dev.azure.com/devdiv/DevDiv/_build?definitionId=25298)
2. Click **"Run pipeline"**
3. Select **"SkiaSharp"** from the radio buttons
4. Check **"Confirm push to NuGet.org"** checkbox
5. **For stable releases ONLY:** Check **"Push stable packages"** checkbox
   - ⚠️ Do NOT check this for preview releases
6. Click **"Next: Resources"**
7. In **"Pipeline artifacts"**, click the **SkiaSharp** artifact selector
8. From the **branch dropdown**, select `release/{version}` (the release branch)
9. From the **pipeline runs list**, select the correct build by checking the build number
10. Click **"Use selected run"**
11. Click **"Run"**

### Verification During Pipeline Run

⚠️ **Before approving the push step, verify BOTH:**

1. **Run name** — The pipeline run will rename itself to the version being released. Confirm this matches your expected version.
2. **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):

```bash
# 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):

```bash
# 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`)

```bash
git fetch origin
git checkout release/{branch-version}
git pull
git tag {tag}
```

**Confirm with `ask_user`** before pushing tag (cannot be undone):
```bash
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.

```bash
# 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

```bash
# 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
```

- `--title` sets the release title (use format above)
- `--generate-notes` auto-generates release notes from PRs/commits
- `--notes-start-tag` specifies the previous release to diff from (required)
- `--prerelease` marks as prerelease (preview only)
- `--verify-tag` ensures 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](references/release-notes.md)** for:
- Complete emoji reference (platform + contributor)
- Label-to-platform mapping
- Title keyword detection
- Full annotation process and examples

**Quick summary:**
1. Get release body: `gh release view {tag} --json body -q '.body' > /tmp/skiasharp/release/release-body.md`
2. For each PR: determine platform emoji, add ❤️ for non-mattleibow contributors
3. Build sections: Breaking Changes (if any), New Features (if any), What's Changed (all)
4. Update release: `gh release edit {tag} --notes-file /tmp/skiasharp/release/release-body.md`

---

## Step 7: Close Milestone (Stable only)

**Skip for preview releases.**

```bash
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:
1. Check what's done: `gh release view {tag}` (release exists?), `git ls-remote --tags origin` (tag exists?)
2. Skip completed steps
3. Continue from where you left off

---

## Resources

- [releasing.md](../../../documentation/releasing.md) — Version patterns, tag formats, workflow diagrams
- [references/release-notes.md](references/release-notes.md) — Emoji annotation details