From 6f958bbe90adc1ca77b9d1d780773625cfbf23d2 Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 24 Mar 2026 12:45:37 -0700 Subject: [PATCH 1/2] Add Prepare Release GitHub Action for automated release kickoff --- .github/workflows/prepare-release.yaml | 229 +++++++++++++++++++++++++ doc/release_process.md | 77 +++++++-- 2 files changed, 296 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/prepare-release.yaml diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml new file mode 100644 index 000000000..8b99502c6 --- /dev/null +++ b/.github/workflows/prepare-release.yaml @@ -0,0 +1,229 @@ +# Prepare Release Workflow +# This workflow automates the release preparation process: +# 1. Updates the version in eng/targets/Release.props +# 2. Generates changelog from git diff between main and last release tag +# 3. Updates CHANGELOG.md with the new version section +# 4. Creates a release branch and tag +# After the workflow completes, manually create a PR from the release branch to main. + +name: Prepare Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g., 1.24.0 or 1.24.0-preview.1). Leave empty to auto-increment patch version.' + required: false + type: string + +permissions: + contents: write + +jobs: + prepare-release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history needed for git diff and tags + + - name: Get current version + id: get-current-version + run: | + # Extract VersionPrefix and VersionSuffix from Release.props + VERSION_PREFIX=$(grep -oP '(?<=)[^<]+' eng/targets/Release.props) + VERSION_SUFFIX=$(grep -oP '(?<=)[^<]*' eng/targets/Release.props) + + if [ -n "$VERSION_SUFFIX" ]; then + CURRENT_VERSION="${VERSION_PREFIX}-${VERSION_SUFFIX}" + else + CURRENT_VERSION="$VERSION_PREFIX" + fi + + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "Current version: $CURRENT_VERSION" + + - name: Calculate next version + id: calc-version + run: | + INPUT_VERSION="${{ github.event.inputs.version }}" + CURRENT_VERSION="${{ steps.get-current-version.outputs.current_version }}" + + if [ -n "$INPUT_VERSION" ]; then + NEW_VERSION="$INPUT_VERSION" + else + # Auto-increment: parse current version and bump appropriately + # Handle pre-release versions like 1.23.0-preview.1 -> 1.23.0-preview.2 + # Handle stable versions like 1.23.0 -> 1.23.1 + NEW_VERSION=$(python3 -c " + import re, sys + v = '$CURRENT_VERSION' + m = re.match(r'^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z]+)\.(\d+))?$', v) + if not m: + print(v) + sys.exit(0) + major, minor, patch, pre_type, pre_num = m.groups() + if pre_type and pre_num: + print(f'{major}.{minor}.{patch}-{pre_type}.{int(pre_num) + 1}') + else: + print(f'{major}.{minor}.{int(patch) + 1}') + ") + fi + + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "New version: $NEW_VERSION" + + - name: Get latest release tag + id: get-latest-tag + run: | + NEW_VERSION="${{ steps.calc-version.outputs.new_version }}" + # Get the latest tag that looks like a version (v*), excluding the + # tag being created so that re-runs don't use it as the baseline. + LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | \ + grep -v "^v${NEW_VERSION}$" | head -n 1) + if [ -z "$LATEST_TAG" ]; then + echo "No previous release tag found, using initial commit" + LATEST_TAG=$(git rev-list --max-parents=0 HEAD) + fi + echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT + echo "Latest tag: $LATEST_TAG" + + - name: Generate changelog diff + id: changelog-diff + run: | + LATEST_TAG="${{ steps.get-latest-tag.outputs.latest_tag }}" + NEW_VERSION="${{ steps.calc-version.outputs.new_version }}" + + echo "Generating changelog for changes between $LATEST_TAG and HEAD..." + + # Get commits between last tag and HEAD. + # GitHub squash-merges put the PR number in parentheses, e.g. "Some change (#123)". + # We convert "(#N)" to a markdown link. + CHANGELOG_CONTENT=$(git log "$LATEST_TAG"..HEAD --pretty=format:"%s" --no-merges | \ + sed 's/(#\([0-9]*\))/([#\1](https:\/\/github.com\/microsoft\/durabletask-dotnet\/pull\/\1))/' | \ + sed 's/^/- /' | \ + grep -v '^- $' || echo "") + + # Save to file for multi-line output + echo "$CHANGELOG_CONTENT" > /tmp/changelog_content.txt + echo "changelog_file=/tmp/changelog_content.txt" >> $GITHUB_OUTPUT + + echo "Generated changelog:" + cat /tmp/changelog_content.txt + + - name: Update Release.props version + run: | + NEW_VERSION="${{ steps.calc-version.outputs.new_version }}" + + # Parse version into prefix and suffix + if [[ "$NEW_VERSION" == *-* ]]; then + VERSION_PREFIX="${NEW_VERSION%%-*}" + VERSION_SUFFIX="${NEW_VERSION#*-}" + else + VERSION_PREFIX="$NEW_VERSION" + VERSION_SUFFIX="" + fi + + echo "Updating Release.props: VersionPrefix=$VERSION_PREFIX, VersionSuffix=$VERSION_SUFFIX" + + # Update VersionPrefix + sed -i "s|[^<]*|$VERSION_PREFIX|" eng/targets/Release.props + + # Update VersionSuffix + sed -i "s|[^<]*|$VERSION_SUFFIX|" eng/targets/Release.props + + echo "Updated eng/targets/Release.props:" + cat eng/targets/Release.props + + - name: Update CHANGELOG.md + run: | + NEW_VERSION="${{ steps.calc-version.outputs.new_version }}" + CHANGELOG_FILE="/tmp/changelog_content.txt" + RELEASE_DATE=$(date +%Y-%m-%d) + + python3 -c " + import sys + + version = '${NEW_VERSION}' + release_date = '${RELEASE_DATE}' + changelog_file = '${CHANGELOG_FILE}' + + with open(changelog_file, 'r') as f: + changes = f.read().strip() + + with open('CHANGELOG.md', 'r') as f: + content = f.read() + + new_section = f'\n## v{version}\n{changes}\n' + + # Insert the new version section after '## Unreleased' + marker = '## Unreleased' + idx = content.find(marker) + if idx != -1: + insert_pos = idx + len(marker) + # Skip any whitespace/newlines after the marker + while insert_pos < len(content) and content[insert_pos] in (' ', '\t', '\n', '\r'): + # Stop if we hit another section header + if content[insert_pos] == '\n' and insert_pos + 1 < len(content) and content[insert_pos + 1] == '#': + break + insert_pos += 1 + content = content[:insert_pos] + '\n' + new_section + content[insert_pos:] + else: + # No Unreleased section found, insert at top after title + title_end = content.find('\n', content.find('# Changelog')) + if title_end != -1: + content = content[:title_end + 1] + '\n## Unreleased\n' + new_section + content[title_end + 1:] + + with open('CHANGELOG.md', 'w') as f: + f.write(content) + + print(f'Updated CHANGELOG.md with v{version}') + " + + - name: Create release branch and commit + id: create-branch + run: | + NEW_VERSION="${{ steps.calc-version.outputs.new_version }}" + BRANCH_NAME="release/v${NEW_VERSION}" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Create and checkout release branch (idempotent) + git checkout -B "$BRANCH_NAME" + + # Stage and commit changes + git add eng/targets/Release.props + git add CHANGELOG.md + git commit -m "Release v${NEW_VERSION}" + + # Create release tag (idempotent) + git tag -f "v${NEW_VERSION}" + + # Push branch and tag (force to handle re-runs) + git push -f origin "$BRANCH_NAME" + git push -f origin "v${NEW_VERSION}" + + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + echo "Created branch $BRANCH_NAME and tag v${NEW_VERSION}" + + - name: Summary + run: | + NEW_VERSION="${{ steps.calc-version.outputs.new_version }}" + BRANCH_NAME="${{ steps.create-branch.outputs.branch_name }}" + + echo "## Release Preparation Complete! :rocket:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: v${NEW_VERSION}" >> $GITHUB_STEP_SUMMARY + echo "- **Branch**: ${BRANCH_NAME}" >> $GITHUB_STEP_SUMMARY + echo "- **Tag**: v${NEW_VERSION}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Next Steps" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "1. **Create a Pull Request** from **${BRANCH_NAME}** → **main** with title: **Release v${NEW_VERSION}**" >> $GITHUB_STEP_SUMMARY + echo "2. Review the version bump in \`eng/targets/Release.props\` and changelog updates" >> $GITHUB_STEP_SUMMARY + echo "3. Update per-package \`RELEASENOTES.md\` files if needed" >> $GITHUB_STEP_SUMMARY + echo "4. Merge the PR after CI passes" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "[Create PR](https://github.com/microsoft/durabletask-dotnet/compare/main...${BRANCH_NAME}?expand=1&title=Release+v${NEW_VERSION})" >> $GITHUB_STEP_SUMMARY diff --git a/doc/release_process.md b/doc/release_process.md index 22ff7db3b..10840b194 100644 --- a/doc/release_process.md +++ b/doc/release_process.md @@ -1,9 +1,73 @@ # Release Process +## Overview + +| Package prefix | Registry | +|---|---| +| `Microsoft.DurableTask.*` | [NuGet](https://www.nuget.org/profiles/durabletask) | + +This repo publishes multiple NuGet packages. Most share a single version defined in `eng/targets/Release.props`. Individual packages can version independently by adding `` and `` properties directly in their `.csproj`. + +We follow an approach of releasing everything together, even if a package has no changes — unless we intentionally hold a package back. + +### Versioning Scheme + +We follow [semver](https://semver.org/) with optional pre-release tags: + +``` +X.Y.Z-preview.N → X.Y.Z-rc.N → X.Y.Z (stable) +``` + +## Automated Release Preparation (Recommended) + +Use the **Prepare Release** GitHub Action to automate the release preparation process. + +### Running the Workflow + +1. Go to **Actions** → **Prepare Release** in GitHub +2. Click **Run workflow** +3. Optionally specify a version (e.g., `1.24.0` or `1.24.0-preview.1`). Leave empty to auto-increment the patch version. +4. Click **Run workflow** + +### What the Workflow Does + +1. **Determines the next version**: If not specified, auto-increments the current version from `Release.props` +2. **Generates changelog**: Uses `git log` to find changes between `main` and the last release tag +3. **Updates `Release.props`**: Bumps `VersionPrefix` and `VersionSuffix` to the new version +4. **Updates `CHANGELOG.md`**: Adds a new version section with the discovered changes +5. **Creates a release branch**: `release/vX.Y.Z` +6. **Creates a release tag**: `vX.Y.Z` + +### After the Workflow Completes + +The workflow summary will include a link to create a PR. You must **manually create a pull request** from the release branch (`release/vX.Y.Z`) to `main`: + +1. Go to the workflow run summary and click the **Create PR** link, or navigate to: `https://github.com/microsoft/durabletask-dotnet/compare/main...release/vX.Y.Z` +2. Set the PR title to `Release vX.Y.Z` +3. Review the version bump in `eng/targets/Release.props` and changelog updates +4. Update per-package `RELEASENOTES.md` files if needed +5. Merge the PR after CI passes + +After the PR is merged, follow the **Publishing** steps below. + +## Publishing (After Release PR is Merged) + +1. Kick off [ADO release build](https://dev.azure.com/durabletaskframework/Durable%20Task%20Framework%20CI/_build?definitionId=29) (use the tag as the build target, enter `refs/tags/`) +2. Validate signing, package contents (if build changes were made.) +3. Create ADO release. + - From successful ADO release build, click 3 dots in top right → 'Release'. +4. Release to ADO feed. +5. Release to NuGet once validated (and dependencies are also released to NuGet.) +6. Publish GitHub draft release. +7. Delete contents of all `RELEASENOTES.md` files. + +## Manual Release Process (Alternative) + +If you prefer to prepare the release manually, follow these steps: + 1. Rev versions as appropriate following semver. - Repo-wide versions can be found in `eng/targets/Release.props` - Individual packages can version independently by adding the `` and `` properties directly in their `.csproj`. - - We follow an approach of just releasing everything, even if the package has no changes. _Unless_ we intentionally do not want to release a package. 2. Ensure appropriate `RELEASENOTES.md` are updated in the repository. - These are per-package and contain only that packages changes. - The contents will be included in the `.nupkg` and show up in nuget.org's release notes tab. @@ -11,12 +75,5 @@ 3. Ensure `CHANGELOG.md` in repo root is updated. 4. Tag the release. - `git tag v`, `git push -u ` -5. Draft github release for new tag. -6. Kick off [ADO release build](https://dev.azure.com/durabletaskframework/Durable%20Task%20Framework%20CI/_build?definitionId=29) (use the tag as the build target, enter `refs/tags/`) -7. Validate signing, package contents (if build changes were made.) -8. Create ADO release. - - From successful ADO release build, click 3 dots in top right -> 'Release'. -9. Release to ADO feed. -10. Release to nuget once validated (and dependencies are also released to nuget.) -11. Publish github draft release. -12. Delete contents of all `RELEASENOTES.md` files. +5. Draft GitHub release for new tag. +6. Follow the **Publishing** steps above. From 80536c2a85045bcf08f57a3c6152c39d4a58b5ae Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 24 Mar 2026 13:17:30 -0700 Subject: [PATCH 2/2] Address PR review: fix re-run safety, remove dead code, fix doc wording --- .github/workflows/prepare-release.yaml | 10 ++++++---- doc/release_process.md | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 8b99502c6..54923eab5 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -12,7 +12,7 @@ on: workflow_dispatch: inputs: version: - description: 'Release version (e.g., 1.24.0 or 1.24.0-preview.1). Leave empty to auto-increment patch version.' + description: 'Release version (e.g., 1.24.0 or 1.24.0-preview.1). Leave empty to auto-increment (patch for stable, pre-release number for pre-release).' required: false type: string @@ -140,13 +140,11 @@ jobs: run: | NEW_VERSION="${{ steps.calc-version.outputs.new_version }}" CHANGELOG_FILE="/tmp/changelog_content.txt" - RELEASE_DATE=$(date +%Y-%m-%d) python3 -c " import sys version = '${NEW_VERSION}' - release_date = '${RELEASE_DATE}' changelog_file = '${CHANGELOG_FILE}' with open(changelog_file, 'r') as f: @@ -196,7 +194,11 @@ jobs: # Stage and commit changes git add eng/targets/Release.props git add CHANGELOG.md - git commit -m "Release v${NEW_VERSION}" + if ! git diff --cached --quiet; then + git commit -m "Release v${NEW_VERSION}" + else + echo "No changes to commit (re-run with same version)" + fi # Create release tag (idempotent) git tag -f "v${NEW_VERSION}" diff --git a/doc/release_process.md b/doc/release_process.md index 10840b194..319a2cd49 100644 --- a/doc/release_process.md +++ b/doc/release_process.md @@ -26,7 +26,7 @@ Use the **Prepare Release** GitHub Action to automate the release preparation pr 1. Go to **Actions** → **Prepare Release** in GitHub 2. Click **Run workflow** -3. Optionally specify a version (e.g., `1.24.0` or `1.24.0-preview.1`). Leave empty to auto-increment the patch version. +3. Optionally specify a version (e.g., `1.24.0` or `1.24.0-preview.1`). Leave empty to auto-increment (patch for stable, pre-release number for pre-release). 4. Click **Run workflow** ### What the Workflow Does