From 0a3efe20c1edc118240cd554f9b5066ef2609834 Mon Sep 17 00:00:00 2001 From: Corina <14900841+corinagum@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:05:30 -0700 Subject: [PATCH 1/3] docs: document hotfix backports and preview release versioning RELEASE.md covered cutting a new release from main, but had nothing for backporting a fix onto an existing release branch, and nothing about how preview versions are numbered. Both gaps came up shipping 2.0.16 and 2.1.0-alpha.2 for the same fix. - Branch strategy now shows both live trains and their PyPI dist-tags - New section for cherry-picking a fix onto a release branch - New section on preview versioning: the height rules, the versionHeightOffset formula, and a worked example - Call out that preview backports must be merged with a merge commit, since squashing lowers the height and republishes an existing version - Add a pre-publish nbgv verification step and a post-publish check - Correct the claim that release PRs are squash-merged - Refresh stale version examples Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36f4a7c0-71cb-4b9d-8a5c-d6db706ff8a7 --- RELEASE.md | 120 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 113 insertions(+), 7 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 7f46decd..4e3d0be9 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -17,14 +17,17 @@ dotnet tool install -g nbgv ## Branch Strategy -| Branch | Versions | Published | -|--------|----------|-----------| -| `main` | `2.1.0.dev1`, `2.1.0.dev2`, ... | No | -| `release/v2.0` | `2.0.x` | Yes | +| Branch | Versions | PyPI tag | Published | +|--------|----------|----------|-----------| +| `main` | `2.1.0.dev1`, `2.1.0.dev2`, ... | n/a | No | +| `release/v2.0` | `2.0.x` (stable) | `latest` | Yes | +| `release/v2.1` | `2.1.0-alpha.N` (preview) | `next` | Yes | Release branches are **long-lived per minor line** and use the `release/v.` naming convention. Create a new release branch from `main` when that minor line enters its release phase; never reset an existing release branch. +**Several release branches are live at once.** At the time of writing, `release/v2.0` carries the stable line and `release/v2.1` carries the preview line. A fix that affects both must be backported to each one separately, and each gets its own release. See [Backporting a fix to a release branch](#backporting-a-fix-to-a-release-branch). + ## Workflow Development happens on `main`. When ready to release: @@ -62,6 +65,34 @@ release branch as merged without pulling in any of its content. gh pr create --base "release/v." --title "Release : merge main into release/v." ``` +## Backporting a fix to a release branch + +The workflow above cuts a release by bringing all of `main` into a release branch. That is not what you want for a hotfix. When a fix has already merged to `main` and needs to ship on an existing release line, cherry-pick just that commit instead. + +1. Start a branch from the release branch you are patching (not from `main`): + ```bash + git fetch origin + git checkout -b /backport--to-. "origin/release/v." + ``` + +2. Cherry-pick the fix. Use `-x` so the commit records where it came from: + ```bash + git cherry-pick -x + ``` + + Most PRs are squash-merged into `main`, so the merge commit has a single parent and a plain `cherry-pick` works. If you are picking a true merge commit, you need `-m 1`. Check with `git rev-list --parents -n 1 `: three entries means it is a merge commit, two means it is a squash. + +3. Set the version for the release. For a stable line, edit `version.json` to the next patch version. For a preview line, adjust `versionHeightOffset` instead. See [Preview releases](#preview-releases). + +4. Push and open a PR against the release branch. + +5. Verify the version **before** publishing, then follow [Publishing](#publishing) as usual. + +Keep backport PRs to the fix itself. Sweeping in unrelated commits from `main` turns a hotfix into an untested release. + +> [!IMPORTANT] +> Merge backport PRs with a **merge commit**, not a squash. On a preview line the version number is derived from commit height, so squashing changes the resulting version. See [Preview releases](#preview-releases). + ## Versioning Versions are managed by **Nerdbank.GitVersioning** via [version.json](version.json). @@ -83,7 +114,8 @@ version core resets Nerdbank.GitVersioning's height for the new development line | Branch | Package Name | |--------|--------------| | `main` | `microsoft_teams_apps-2.1.0.dev2.tar.gz` | -| `release/v2.0` | `microsoft_teams_apps-2.0.14.tar.gz` | +| `release/v2.0` | `microsoft_teams_apps-2.0.16.tar.gz` | +| `release/v2.1` | `microsoft_teams_apps-2.1.0a2.tar.gz` | > **Note:** Running the pipeline on a branch not in `publicReleaseRefSpec` (e.g., a feature branch) produces versions with the commit hash appended, like `2.1.0.dev5+g1a2b3c4`. This is expected and useful for testing. @@ -101,6 +133,60 @@ the [Workflow](#workflow) section above already handles this — just edit `vers After the PR merges, run the publish pipeline with **Public** to release to PyPI. +### Preview releases + +A preview line keeps `{height}` in the version string and lets Nerdbank.GitVersioning number each release, for example `release/v2.1`: + +```json +{ + "version": "2.1.0-alpha.{height}", + "versionHeightOffset": -14 +} +``` + +The published version is `height + versionHeightOffset`. Three rules govern the height: + +- Height **resets** when the `version` string itself changes. +- Editing `versionHeightOffset` does **not** reset it. +- Height is `1 + max(height of each parent)`, so every commit you add, **including the merge commit**, increases it. + +That last point is the one that bites. A backport adds the cherry-pick, plus a commit editing `version.json`, plus the merge commit itself. To land on a chosen alpha number: + +``` +versionHeightOffset = target_alpha - height_of_the_final_merge_commit +``` + +Worked example, backporting one fix onto `release/v2.1` to produce `2.1.0-alpha.2`: + +| Step | Height | +|------|--------| +| release branch tip before the backport | 13 | +| + cherry-picked fix | 14 | +| + commit editing `versionHeightOffset` | 15 | +| + merge commit | **16** | + +So the offset is `2 - 16 = -14`. + +This is why preview backports must be merged with a **merge commit**. Squashing removes commits from the chain, the final height is lower than planned, and the pipeline republishes a version number that is already on PyPI. + +Because the offset depends on the merge commit that does not exist yet, verify after merging and before publishing: + +```bash +git checkout "release/v." && git pull +nbgv get-version -v SemVer2 +``` + +If the number is wrong, correct `versionHeightOffset` and re-check. A corrective commit adds height of its own, so recompute rather than assuming the difference. + +> [!TIP] +> You can confirm the number before merging by simulating the merge locally. `publicReleaseRefSpec` matches on branch name, so the local branch has to be named like the real one: +> ```bash +> git checkout -b "release/v." "origin/release/v." +> git merge --no-ff +> nbgv get-version -v SemVer2 # should print the version you intend to publish +> ``` +> Delete the local branch afterwards so it cannot be pushed by accident. + ## Publishing The [publish pipeline](https://dev.azure.com/DomoreexpGithub/Github_Pipelines/_build?definitionId=51&_a=summary) (`.azdo/publish.yml`) is manually triggered and requires selecting a **Publish Type**: `Internal` or `Public`. @@ -115,6 +201,25 @@ The [publish pipeline](https://dev.azure.com/DomoreexpGithub/Github_Pipelines/_b > **Note:** The pipeline filters out packages matching the `ExcludePackageFolders` variable. Prerelease versions are tagged `next` on PyPI; stable versions are tagged `latest`. +Before triggering a **Public** run, confirm the version the branch will actually produce: + +```bash +git checkout "release/v." && git pull +nbgv get-version -v SemVer2 +``` + +PyPI rejects re-uploading a version that already exists, so a wrong number here means a failed publish, or worse, a silently skipped version. This matters most on preview lines, where the number is computed rather than written down. + +### After publishing + +Confirm the packages landed as intended. Every package in the workspace is published together, so check more than one: + +```bash +pip index versions microsoft-teams-apps --pre +``` + +A preview release should leave the `latest` tag alone. An unpinned `pip install microsoft-teams-apps` should still resolve to the newest **stable** version, while `pip install microsoft-teams-apps==` gets the preview. + ## Tagging and GitHub Release After the publish pipeline finishes and packages land on PyPI, tag the release and create a GitHub Release page: @@ -126,8 +231,9 @@ gh release create v -R microsoft/teams.py \ --generate-notes --notes-start-tag v ``` -**Note:** GitHub's auto-generated notes walk back from the release branch tip. Because the release PR is -squash-merged, the auto-list shows only the merge PR. To get the real PR delta from `main`, query by date: +Add `--prerelease` for preview releases so GitHub does not present them as the latest stable version. + +**Note:** GitHub's auto-generated notes walk back from the release branch tip, so they tend to list only the release or backport PR rather than the changes a reader cares about. Rewrite the body to describe the actual change, and credit the reporter when a fix came from an outside bug report. For a release cut from `main`, you can pull the real PR delta by date: ```bash # Note: macOS has no `tac` — the awk one-liner below works everywhere From c1671b6e7433a28e7adb71b2db488f451546ad2a Mon Sep 17 00:00:00 2001 From: Corina <14900841+corinagum@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:18:52 -0700 Subject: [PATCH 2/3] docs: correct squash-vs-merge-commit wording in backport steps The cherry-pick step called the source commit a merge commit, but PRs into main are squash-merged, which produces an ordinary single-parent commit. Rename the placeholder, explain how to find the commit by PR number, and make the -m 1 guidance conditional on an actual merge commit. Also name the asymmetry with release-branch PRs, which do require merge commits. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36f4a7c0-71cb-4b9d-8a5c-d6db706ff8a7 --- RELEASE.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 4e3d0be9..0e0426d1 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -77,10 +77,15 @@ The workflow above cuts a release by bringing all of `main` into a release branc 2. Cherry-pick the fix. Use `-x` so the commit records where it came from: ```bash - git cherry-pick -x + git cherry-pick -x ``` - Most PRs are squash-merged into `main`, so the merge commit has a single parent and a plain `cherry-pick` works. If you are picking a true merge commit, you need `-m 1`. Check with `git rev-list --parents -n 1 `: three entries means it is a merge commit, two means it is a squash. + PRs are squash-merged into `main`, so each merged PR is a single ordinary commit and a plain `cherry-pick` works. Find it by PR number: + ```bash + git log origin/main --oneline --grep "(#)" + ``` + + If you are ever picking an actual merge commit, add `-m 1` to pick its change relative to the first parent. To tell the two apart, `git rev-list --parents -n 1 ` prints the commit followed by its parents: two entries is an ordinary commit, three or more is a merge. 3. Set the version for the release. For a stable line, edit `version.json` to the next patch version. For a preview line, adjust `versionHeightOffset` instead. See [Preview releases](#preview-releases). @@ -91,7 +96,7 @@ The workflow above cuts a release by bringing all of `main` into a release branc Keep backport PRs to the fix itself. Sweeping in unrelated commits from `main` turns a hotfix into an untested release. > [!IMPORTANT] -> Merge backport PRs with a **merge commit**, not a squash. On a preview line the version number is derived from commit height, so squashing changes the resulting version. See [Preview releases](#preview-releases). +> Unlike PRs into `main`, backport PRs must be merged with a **merge commit**, not a squash. On a preview line the version number is derived from commit height, so squashing changes the resulting version. See [Preview releases](#preview-releases). ## Versioning From 41577238609bb2251a81067e7bf880376ba098c3 Mon Sep 17 00:00:00 2001 From: Corina <14900841+corinagum@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:23:18 -0700 Subject: [PATCH 3/3] docs: note that version.json stays put after a release Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 36f4a7c0-71cb-4b9d-8a5c-d6db706ff8a7 --- RELEASE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index 0e0426d1..32241d81 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -192,6 +192,12 @@ If the number is wrong, correct `versionHeightOffset` and re-check. A corrective > ``` > Delete the local branch afterwards so it cannot be pushed by accident. +### After a release + +Leave `version.json` alone once a release ships. A stable branch keeps its literal version, so the next release PR has to bump it; a preview branch increments `{height}` on its own and needs nothing. + +Sitting on an already-published stable version is deliberate: PyPI rejecting the re-upload is the only guard against an accidental publish, and a `-dev` version would publish cleanly instead. + ## Publishing The [publish pipeline](https://dev.azure.com/DomoreexpGithub/Github_Pipelines/_build?definitionId=51&_a=summary) (`.azdo/publish.yml`) is manually triggered and requires selecting a **Publish Type**: `Internal` or `Public`.