Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 263 additions & 0 deletions .github/workflows/prepare-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# Prepare Release Workflow
# This workflow automates the release preparation process:
# 1. Updates package versions in all package.json files
# 2. Generates changelog from git diff between main and last release tag
# 3. Creates a release branch and tag
# 4. Opens a pull request for review

name: Prepare Release

on:
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., 0.2.0-beta.1). Leave empty to auto-increment patch version.'
required: false
type: string

permissions:
contents: write
pull-requests: 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: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Get latest release tag
id: get-latest-tag
run: |
# Get the latest tag that looks like a version (v*)
LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | 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: Get current version
id: get-current-version
run: |
CURRENT_VERSION=$(node -p "require('./packages/durabletask-js/package.json').version")
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
# Use the specified version
NEW_VERSION="$INPUT_VERSION"
else
# Auto-increment: parse current version and bump appropriately
# Handle pre-release versions like 0.1.0-alpha.2 -> 0.1.0-alpha.3
# Handle stable versions like 0.1.0 -> 0.1.1
NEW_VERSION=$(node -e "
const v = '$CURRENT_VERSION';
const match = v.match(/^(\d+)\.(\d+)\.(\d+)(?:-([a-z]+)\.(\d+))?$/);
if (!match) {
console.log(v);
process.exit(0);
}
const [, major, minor, patch, preType, preNum] = match;
if (preType && preNum) {
// Increment pre-release number
console.log(\`\${major}.\${minor}.\${patch}-\${preType}.\${parseInt(preNum) + 1}\`);
} else {
// Increment patch version
console.log(\`\${major}.\${minor}.\${parseInt(patch) + 1}\`);
Comment on lines +72 to +81
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the current version doesn’t match the regex, the script prints the current version and exits successfully, which will cause the workflow to try creating a branch/tag that likely already exists (and then fail later). It would be safer to exit 1 with a clear error when parsing fails, or fall back to npm version patch --no-git-tag-version for semver-aware bumping.

Suggested change
console.log(v);
process.exit(0);
}
const [, major, minor, patch, preType, preNum] = match;
if (preType && preNum) {
// Increment pre-release number
console.log(\`\${major}.\${minor}.\${patch}-\${preType}.\${parseInt(preNum) + 1}\`);
} else {
// Increment patch version
console.log(\`\${major}.\${minor}.\${parseInt(patch) + 1}\`);
console.error(`Unable to parse current version '${v}'. Expected format 'MAJOR.MINOR.PATCH' or 'MAJOR.MINOR.PATCH-prerelease.NUM'.`);
process.exit(1);
}
const [, major, minor, patch, preType, preNum] = match;
if (preType && preNum) {
// Increment pre-release number
console.log(`${major}.${minor}.${patch}-${preType}.${parseInt(preNum) + 1}`);
} else {
// Increment patch version
console.log(`${major}.${minor}.${parseInt(patch) + 1}`);

Copilot uses AI. Check for mistakes.
}
")
fi

echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "New version: $NEW_VERSION"

- 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 merge commits (PRs) between last tag and HEAD
CHANGELOG_CONTENT=$(git log "$LATEST_TAG"..HEAD --merges --pretty=format:"- %s" | \
sed 's/Merge pull request #\([0-9]*\) from [^:]*:/\[#\1\](https:\/\/github.com\/microsoft\/durabletask-js\/pull\/\1):/' | \
Comment on lines +98 to +99
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git log ... --merges --pretty=format:"- %s" will typically produce subjects like "Merge pull request #123 from ..." (no PR title). After the sed rewrite this becomes a changelog entry that only contains a PR link/colon with no description. Consider extracting the PR title (e.g., from the merge commit body) or using a different format/API so each changelog line has meaningful text.

Suggested change
CHANGELOG_CONTENT=$(git log "$LATEST_TAG"..HEAD --merges --pretty=format:"- %s" | \
sed 's/Merge pull request #\([0-9]*\) from [^:]*:/\[#\1\](https:\/\/github.com\/microsoft\/durabletask-js\/pull\/\1):/' | \
CHANGELOG_CONTENT=$(git log "$LATEST_TAG"..HEAD --merges --pretty=format:"- %s %b" | \
sed 's/Merge pull request #\([0-9]*\) from [^ ]* \(.*\)/\[#\1\](https:\/\/github.com\/microsoft\/durabletask-js\/pull\/\1): \2/' | \

Copilot uses AI. Check for mistakes.
sed 's/Merge branch .*//' | \
grep -v '^$' || echo "")
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sed 's/Merge branch .*//' can turn a line like - Merge branch 'x' into - (not an empty line), so grep -v '^$' won't remove it and the changelog can contain blank bullet points. Consider filtering these merges before prefixing with - , or drop lines that become -/- after transformations.

Suggested change
grep -v '^$' || echo "")
grep -Ev '^- *$' || echo "")

Copilot uses AI. Check for mistakes.

# If no merge commits, get regular commits
if [ -z "$CHANGELOG_CONTENT" ]; then
CHANGELOG_CONTENT=$(git log "$LATEST_TAG"..HEAD --pretty=format:"- %s" --no-merges | head -20)
fi

# 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 package versions
run: |
NEW_VERSION="${{ steps.calc-version.outputs.new_version }}"

echo "Updating packages to version $NEW_VERSION..."

# Update durabletask-js package.json
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('packages/durabletask-js/package.json', 'utf8'));
pkg.version = '$NEW_VERSION';
fs.writeFileSync('packages/durabletask-js/package.json', JSON.stringify(pkg, null, 2) + '\n');
"

# Update durabletask-js-azuremanaged package.json
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('packages/durabletask-js-azuremanaged/package.json', 'utf8'));
pkg.version = '$NEW_VERSION';
// Also update peer dependency to the new version
if (pkg.peerDependencies && pkg.peerDependencies['@microsoft/durabletask-js']) {
pkg.peerDependencies['@microsoft/durabletask-js'] = '>=$NEW_VERSION';
}
fs.writeFileSync('packages/durabletask-js-azuremanaged/package.json', JSON.stringify(pkg, null, 2) + '\n');
"

echo "Updated package.json files:"
grep '"version"' packages/durabletask-js/package.json
grep '"version"' packages/durabletask-js-azuremanaged/package.json

- name: Update CHANGELOG.md
run: |
NEW_VERSION="${{ steps.calc-version.outputs.new_version }}"
CHANGELOG_FILE="${{ steps.changelog-diff.outputs.changelog_file }}"
RELEASE_DATE=$(date +%Y-%m-%d)

# Read the changelog content
CHANGELOG_CONTENT=$(cat "$CHANGELOG_FILE")

# Create new changelog section
NEW_SECTION="## v${NEW_VERSION} (${RELEASE_DATE})

### Changes

${CHANGELOG_CONTENT}
Comment on lines +157 to +159
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow adds changelog sections as ### Changes, but the existing CHANGELOG.md structure uses ### New / ### Fixes headings. Consider keeping the same headings (and optionally splitting entries) so the changelog remains consistent across releases and any downstream tooling/expectations don’t break.

Suggested change
### Changes
${CHANGELOG_CONTENT}
### New
${CHANGELOG_CONTENT}
### Fixes

Copilot uses AI. Check for mistakes.
"

# Insert new section after "## Upcoming" section
node -e "
const fs = require('fs');
let content = fs.readFileSync('CHANGELOG.md', 'utf8');

const newSection = \`$NEW_SECTION\`;
Comment on lines +163 to +167
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const newSection = $NEW_SECTION; in a node -e "..." block is fragile because $NEW_SECTION includes commit messages; backticks, ${...}, or other special characters in commit subjects can break the JS template literal and/or the shell quoting. Consider writing NEW_SECTION to a temp file and reading it from Node, or pass it via base64/JSON-escaped string to avoid injection/quoting failures.

Suggested change
node -e "
const fs = require('fs');
let content = fs.readFileSync('CHANGELOG.md', 'utf8');
const newSection = \`$NEW_SECTION\`;
NEW_SECTION_FILE=$(mktemp)
printf '%s\n' "$NEW_SECTION" > "$NEW_SECTION_FILE"
NEW_SECTION_FILE="$NEW_SECTION_FILE" node -e "
const fs = require('fs');
let content = fs.readFileSync('CHANGELOG.md', 'utf8');
const newSection = fs.readFileSync(process.env.NEW_SECTION_FILE, 'utf8');

Copilot uses AI. Check for mistakes.

// Find the Upcoming section and insert after it
const upcomingMatch = content.match(/## Upcoming[\s\S]*?(?=\n## v|$)/);
if (upcomingMatch) {
const upcomingEnd = content.indexOf(upcomingMatch[0]) + upcomingMatch[0].length;
content = content.slice(0, upcomingEnd) + '\n' + newSection + content.slice(upcomingEnd);
} else {
// No Upcoming section, prepend
content = '## Upcoming\n\n' + newSection + content;
}

// Reset the Upcoming section to empty
content = content.replace(/## Upcoming[\s\S]*?(?=\n## v)/, '## Upcoming\n\n### New\n\n### Fixes\n\n');

fs.writeFileSync('CHANGELOG.md', content);
"

echo "Updated CHANGELOG.md"

- 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
git checkout -b "$BRANCH_NAME"

# Stage and commit changes
git add packages/durabletask-js/package.json
git add packages/durabletask-js-azuremanaged/package.json
git add CHANGELOG.md
git commit -m "Release v${NEW_VERSION}"

# Create release tag
git tag "v${NEW_VERSION}"

# Push branch and tag
git push origin "$BRANCH_NAME"
git push origin "v${NEW_VERSION}"
Comment on lines +205 to +210
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow creates and pushes the release tag before the release PR is reviewed/merged, so the tag will point to a release-branch commit rather than the eventual merge commit on main. That can make vX.Y.Z not correspond to the released commit on the default branch and can confuse consumers/tooling. Consider deferring tag creation until after the PR is merged (e.g., a separate workflow triggered on merge), or tagging the merge commit.

Copilot uses AI. Check for mistakes.

echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "Created branch $BRANCH_NAME and tag v${NEW_VERSION}"

- name: Create Pull Request
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
NEW_VERSION="${{ steps.calc-version.outputs.new_version }}"
BRANCH_NAME="${{ steps.create-branch.outputs.branch_name }}"
CHANGELOG_FILE="${{ steps.changelog-diff.outputs.changelog_file }}"

CHANGELOG_CONTENT=$(cat "$CHANGELOG_FILE")

PR_BODY="## Release v${NEW_VERSION}

This PR prepares the release of version **${NEW_VERSION}** for all packages.

### Packages Updated
- \`@microsoft/durabletask-js@${NEW_VERSION}\`
- \`@microsoft/durabletask-js-azuremanaged@${NEW_VERSION}\`

### Changes Since Last Release
${CHANGELOG_CONTENT}

### Release Checklist
- [ ] Review version bumps in package.json files
- [ ] Review CHANGELOG.md updates
- [ ] Verify CI passes
- [ ] Merge this PR
- [ ] After merge, the official build pipeline will produce signed artifacts
- [ ] Download artifacts and publish to npm with appropriate tag
"

gh pr create \
--title "Release v${NEW_VERSION}" \
--body "$PR_BODY" \
--base main \
--head "$BRANCH_NAME" \
--label "release"

- 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 "A pull request has been created. Review and merge to complete the release." >> $GITHUB_STEP_SUMMARY
6 changes: 3 additions & 3 deletions eng/templates/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ jobs:

# Build azure-managed extension package
- script: |
cd extensions/durabletask-js-azuremanaged
cd packages/durabletask-js-azuremanaged
npm ci
npm run build
npm run test
npm prune --production
displayName: "Build azure-managed extension"
- script: |
cd extensions/durabletask-js-azuremanaged
cd packages/durabletask-js-azuremanaged
npm pack
displayName: "pack azure-managed extension"

Expand All @@ -50,6 +50,6 @@ jobs:
- task: CopyFiles@2
displayName: "Copy azure-managed extension to staging"
inputs:
SourceFolder: $(System.DefaultWorkingDirectory)/extensions/durabletask-js-azuremanaged
SourceFolder: $(System.DefaultWorkingDirectory)/packages/durabletask-js-azuremanaged
Contents: "*.tgz"
TargetFolder: $(Build.ArtifactStagingDirectory)/buildoutputs
Loading