11# Prepare Release Workflow
2- # This workflow automates the release preparation process:
3- # 1. Updates package versions in all package.json files
4- # 2. Generates changelog from git diff between main and last release tag
5- # 3. Creates a release branch and tag
6- # After the workflow completes, manually create a PR from the release branch to main.
2+ # This workflow automates per-package release preparation. Choose which package to release
3+ # with the required `package` input; the workflow then, for THAT package only:
4+ # 1. Bumps the selected package's version in its own package.json
5+ # 2. Generates a changelog from git history since the package's last release tag
6+ # 3. Updates the package's changelog file and creates a release branch + package-scoped tag
7+ # Packages are versioned and tagged independently (tag prefixes: v, azuremanaged-v, durable-functions-v).
8+ # After the workflow completes, manually open a PR from the release branch to main, then publish
9+ # from the package directory using the npm command shown in the run summary (mind the dist-tag).
710
811name : Prepare Release
912
1013on :
1114 workflow_dispatch :
1215 inputs :
16+ package :
17+ description : ' Which package to release'
18+ required : true
19+ type : choice
20+ options :
21+ - durabletask-js
22+ - durabletask-js-azuremanaged
23+ - azure-functions-durable
1324 version :
1425 description : ' Release version (e.g., 0.2.0-beta.1). Leave empty to auto-increment patch version.'
1526 required : false
@@ -32,10 +43,45 @@ jobs:
3243 with :
3344 node-version : ' 22'
3445
46+ - name : Resolve package metadata
47+ id : resolve-package
48+ run : |
49+ PACKAGE="${{ github.event.inputs.package }}"
50+ case "$PACKAGE" in
51+ durabletask-js)
52+ PKG_DIR="packages/durabletask-js"
53+ NPM_NAME="@microsoft/durabletask-js"
54+ TAG_PREFIX="v"
55+ CHANGELOG_PATH="CHANGELOG.md"
56+ ;;
57+ durabletask-js-azuremanaged)
58+ PKG_DIR="packages/durabletask-js-azuremanaged"
59+ NPM_NAME="@microsoft/durabletask-js-azuremanaged"
60+ TAG_PREFIX="azuremanaged-v"
61+ CHANGELOG_PATH="CHANGELOG.md"
62+ ;;
63+ azure-functions-durable)
64+ PKG_DIR="packages/azure-functions-durable"
65+ NPM_NAME="durable-functions"
66+ TAG_PREFIX="durable-functions-v"
67+ CHANGELOG_PATH="packages/azure-functions-durable/CHANGELOG.md"
68+ ;;
69+ *)
70+ echo "::error::Unknown package '$PACKAGE'. Expected one of: durabletask-js, durabletask-js-azuremanaged, azure-functions-durable."
71+ exit 1
72+ ;;
73+ esac
74+ echo "pkg_dir=$PKG_DIR" >> $GITHUB_OUTPUT
75+ echo "npm_name=$NPM_NAME" >> $GITHUB_OUTPUT
76+ echo "tag_prefix=$TAG_PREFIX" >> $GITHUB_OUTPUT
77+ echo "changelog_path=$CHANGELOG_PATH" >> $GITHUB_OUTPUT
78+ echo "Releasing '$PACKAGE': dir=$PKG_DIR npm=$NPM_NAME tag_prefix=$TAG_PREFIX changelog=$CHANGELOG_PATH"
79+
3580 - name : Get current version
3681 id : get-current-version
3782 run : |
38- CURRENT_VERSION=$(node -p "require('./packages/durabletask-js/package.json').version")
83+ PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}"
84+ CURRENT_VERSION=$(node -p "require('./${PKG_DIR}/package.json').version")
3985 echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
4086 echo "Current version: $CURRENT_VERSION"
4187
@@ -77,10 +123,17 @@ jobs:
77123 id : get-latest-tag
78124 run : |
79125 NEW_VERSION="${{ steps.calc-version.outputs.new_version }}"
80- # Get the latest tag that looks like a version (v*), excluding the
81- # tag being created so that re-runs don't use it as the baseline.
82- LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | \
83- grep -v "^v${NEW_VERSION}$" | head -n 1)
126+ TAG_PREFIX="${{ steps.resolve-package.outputs.tag_prefix }}"
127+ # Get the latest tag for THIS package (its tag prefix), excluding the tag being
128+ # created so that re-runs don't use it as the baseline. The legacy core prefix "v"
129+ # still matches historical tags like v0.3.0 (intended, backward compatible) and never
130+ # matches the "azuremanaged-v"/"durable-functions-v" namespaces (they start with a/d, not v).
131+ # Without versionsort.suffix=-, git ranks a prerelease (e.g. X-beta.1) ABOVE its own GA
132+ # (X), so the release AFTER GA (e.g. X.0.1) would resolve LATEST_TAG to the prerelease and
133+ # re-list every commit already shipped in GA. Treating "-" as the prerelease separator
134+ # orders X-beta.1 below X.
135+ LATEST_TAG=$(git -c versionsort.suffix=- tag -l "${TAG_PREFIX}*" --sort=-v:refname | \
136+ grep -v "^${TAG_PREFIX}${NEW_VERSION}$" | head -n 1)
84137 if [ -z "$LATEST_TAG" ]; then
85138 echo "No previous release tag found, using initial commit"
86139 LATEST_TAG=$(git rev-list --max-parents=0 HEAD)
@@ -94,12 +147,17 @@ jobs:
94147 LATEST_TAG="${{ steps.get-latest-tag.outputs.latest_tag }}"
95148 NEW_VERSION="${{ steps.calc-version.outputs.new_version }}"
96149
97- echo "Generating changelog for changes between $LATEST_TAG and HEAD..."
150+ PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}"
151+ echo "Generating changelog for changes between $LATEST_TAG and HEAD (scoped to $PKG_DIR)..."
98152
99- # Get commits between last tag and HEAD.
153+ # Get commits between last tag and HEAD, scoped to the released package's directory via the
154+ # "-- $PKG_DIR" path filter so a release only lists commits that touched that package.
155+ # Without it every package's commits are listed -- and since a package with no release tag
156+ # yet falls back to the repo's initial commit, the first release would dump the ENTIRE repo
157+ # history into that package's changelog.
100158 # GitHub squash-merges put the PR number in parentheses, e.g. "Some change (#123)".
101159 # We convert "(#N)" to a markdown link.
102- CHANGELOG_CONTENT=$(git log "$LATEST_TAG"..HEAD --pretty=format:"%s" --no-merges | \
160+ CHANGELOG_CONTENT=$(git log "$LATEST_TAG"..HEAD --pretty=format:"%s" --no-merges -- "$PKG_DIR" | \
103161 sed 's/(#\([0-9]*\))/([#\1](https:\/\/github.com\/microsoft\/durabletask-js\/pull\/\1))/' | \
104162 sed 's/^/- /' | \
105163 grep -v '^- $' || echo "")
@@ -111,84 +169,126 @@ jobs:
111169 echo "Generated changelog:"
112170 cat /tmp/changelog_content.txt
113171
114- - name : Update package versions
172+ - name : Update package version
115173 run : |
116174 NEW_VERSION="${{ steps.calc-version.outputs.new_version }}"
175+ PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}"
117176
118- echo "Updating packages to version $NEW_VERSION..."
177+ echo "Updating ${PKG_DIR} to version $NEW_VERSION..."
119178
120- # Update durabletask-js package.json
121179 node -e "
122180 const fs = require('fs');
123- const pkg = JSON.parse(fs.readFileSync('packages/durabletask-js/package.json', 'utf8'));
181+ const path = '${PKG_DIR}/package.json';
182+ const pkg = JSON.parse(fs.readFileSync(path, 'utf8'));
124183 pkg.version = '$NEW_VERSION';
125- fs.writeFileSync('packages/durabletask-js/package.json' , JSON.stringify(pkg, null, 2) + '\n');
184+ fs.writeFileSync(path , JSON.stringify(pkg, null, 2) + '\n');
126185 "
127186
128- # Update durabletask-js-azuremanaged package.json
129- node -e "
130- const fs = require('fs');
131- const pkg = JSON.parse(fs.readFileSync('packages/durabletask-js-azuremanaged/package.json', 'utf8'));
132- pkg.version = '$NEW_VERSION';
133- // Also update peer dependency to the new version
134- if (pkg.peerDependencies && pkg.peerDependencies['@microsoft/durabletask-js']) {
135- pkg.peerDependencies['@microsoft/durabletask-js'] = '>=$NEW_VERSION';
136- }
137- fs.writeFileSync('packages/durabletask-js-azuremanaged/package.json', JSON.stringify(pkg, null, 2) + '\n');
138- "
139-
140- echo "Updated package.json files:"
141- grep '"version"' packages/durabletask-js/package.json
142- grep '"version"' packages/durabletask-js-azuremanaged/package.json
187+ echo "Updated ${PKG_DIR}/package.json:"
188+ grep '"version"' "${PKG_DIR}/package.json"
143189
144- - name : Update CHANGELOG.md
190+ - name : Update changelog
145191 run : |
146192 NEW_VERSION="${{ steps.calc-version.outputs.new_version }}"
147193 CHANGELOG_FILE="${{ steps.changelog-diff.outputs.changelog_file }}"
194+ CHANGELOG_PATH="${{ steps.resolve-package.outputs.changelog_path }}"
148195 RELEASE_DATE=$(date +%Y-%m-%d)
149- node scripts/update-changelog.js "$NEW_VERSION" "$RELEASE_DATE" "$CHANGELOG_FILE"
196+ node scripts/update-changelog.js "$NEW_VERSION" "$RELEASE_DATE" "$CHANGELOG_FILE" "$CHANGELOG_PATH"
197+
198+ - name : Verify pinned core dependency is published on npm
199+ if : ${{ github.event.inputs.package == 'azure-functions-durable' }}
200+ run : |
201+ PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}"
202+ PINNED=$(node -p "require('./${PKG_DIR}/package.json').dependencies['@microsoft/durabletask-js'] || ''")
203+ if [ -z "$PINNED" ]; then
204+ echo "::error::Could not read the @microsoft/durabletask-js pin from ${PKG_DIR}/package.json."
205+ exit 1
206+ fi
207+ echo "durable-functions pins @microsoft/durabletask-js@${PINNED}; verifying it is published on public npm..."
208+ # Query the public registry explicitly: durable-functions is installed from npmjs.org.
209+ # The repo has NO committed .npmrc; instead root package.json's "preinstall" hook
210+ # (scripts/preinstallNpmrc.js) generates a gitignored .npmrc via scripts/setupNpmrc.js
211+ # that points npm at the azfunc Azure DevOps feed for @microsoft.com users and azfunc CI
212+ # builds. Forcing --registry keeps this check on public npm regardless of any such
213+ # generated (or machine-level) npm config, which would otherwise 401 on this query.
214+ PUBLISHED=$(npm view "@microsoft/durabletask-js@${PINNED}" version --registry https://registry.npmjs.org/ 2>/dev/null || true)
215+ if [ "$PUBLISHED" = "$PINNED" ]; then
216+ echo "OK: @microsoft/durabletask-js@${PINNED} is published on npm."
217+ else
218+ echo "::error::durable-functions depends on @microsoft/durabletask-js@${PINNED}, but that exact version is not published on the public npm registry. Release @microsoft/durabletask-js@${PINNED} first, otherwise the published durable-functions package will have an uninstallable dependency."
219+ exit 1
220+ fi
150221
151222 - name : Create release branch and commit
152223 id : create-branch
153224 run : |
154225 NEW_VERSION="${{ steps.calc-version.outputs.new_version }}"
155- BRANCH_NAME="release/v${NEW_VERSION}"
226+ PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}"
227+ NPM_NAME="${{ steps.resolve-package.outputs.npm_name }}"
228+ TAG_PREFIX="${{ steps.resolve-package.outputs.tag_prefix }}"
229+ CHANGELOG_PATH="${{ steps.resolve-package.outputs.changelog_path }}"
230+ TAG_NAME="${TAG_PREFIX}${NEW_VERSION}"
231+ BRANCH_NAME="release/${TAG_NAME}"
156232
157233 git config user.name "github-actions[bot]"
158234 git config user.email "github-actions[bot]@users.noreply.github.com"
159235
160236 # Create and checkout release branch (idempotent)
161237 git checkout -B "$BRANCH_NAME"
162238
163- # Stage and commit changes
164- git add packages/durabletask-js/package.json
165- git add packages/durabletask-js-azuremanaged/package.json
166- git add CHANGELOG.md
167- git commit -m "Release v${NEW_VERSION}"
239+ # Stage and commit changes (only the released package and its changelog)
240+ git add "${PKG_DIR}/package.json"
241+ git add "${CHANGELOG_PATH}"
242+ git commit -m "Release ${NPM_NAME}@${NEW_VERSION}"
168243
169244 # Create release tag (idempotent)
170- git tag -f "v${NEW_VERSION} "
245+ git tag -f "$TAG_NAME "
171246
172247 # Push branch and tag (force to handle re-runs)
173248 git push -f origin "$BRANCH_NAME"
174- git push -f origin "v${NEW_VERSION} "
249+ git push -f origin "$TAG_NAME "
175250
176251 echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
177- echo "Created branch $BRANCH_NAME and tag v${NEW_VERSION}"
252+ echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
253+ echo "Created branch $BRANCH_NAME and tag $TAG_NAME"
178254
179255 - name : Summary
180256 run : |
181257 NEW_VERSION="${{ steps.calc-version.outputs.new_version }}"
258+ PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}"
259+ NPM_NAME="${{ steps.resolve-package.outputs.npm_name }}"
182260 BRANCH_NAME="${{ steps.create-branch.outputs.branch_name }}"
261+ TAG_NAME="${{ steps.create-branch.outputs.tag_name }}"
262+
263+ # Derive the npm dist-tag from the version string. A prerelease (contains "-") must
264+ # publish under a non-default tag so it does NOT move the "latest" dist-tag.
265+ if [[ "$NEW_VERSION" == *-* ]]; then
266+ PUBLISH_CMD="npm publish --registry https://registry.npmjs.org/ --tag preview"
267+ else
268+ PUBLISH_CMD="npm publish --registry https://registry.npmjs.org/"
269+ fi
183270
184271 echo "## Release Preparation Complete! :rocket:" >> $GITHUB_STEP_SUMMARY
185272 echo "" >> $GITHUB_STEP_SUMMARY
186- echo "- **Version**: v${NEW_VERSION}" >> $GITHUB_STEP_SUMMARY
273+ echo "- **Package**: ${NPM_NAME}" >> $GITHUB_STEP_SUMMARY
274+ echo "- **Version**: ${NEW_VERSION}" >> $GITHUB_STEP_SUMMARY
187275 echo "- **Branch**: ${BRANCH_NAME}" >> $GITHUB_STEP_SUMMARY
188- echo "- **Tag**: v${NEW_VERSION }" >> $GITHUB_STEP_SUMMARY
276+ echo "- **Tag**: ${TAG_NAME }" >> $GITHUB_STEP_SUMMARY
189277 echo "" >> $GITHUB_STEP_SUMMARY
190278 echo "### Next Step: Create a Pull Request" >> $GITHUB_STEP_SUMMARY
191279 echo "" >> $GITHUB_STEP_SUMMARY
192- echo "Manually create a PR from **${BRANCH_NAME}** → **main** with title: **Release v${NEW_VERSION}**" >> $GITHUB_STEP_SUMMARY
280+ echo "Manually create a PR from **${BRANCH_NAME}** → **main** with title: **Release ${NPM_NAME}@${NEW_VERSION}**" >> $GITHUB_STEP_SUMMARY
281+ echo "" >> $GITHUB_STEP_SUMMARY
282+ echo "[Create PR](https://github.com/microsoft/durabletask-js/compare/main...${BRANCH_NAME}?expand=1&title=Release+${NPM_NAME}@${NEW_VERSION})" >> $GITHUB_STEP_SUMMARY
193283 echo "" >> $GITHUB_STEP_SUMMARY
194- echo "[Create PR](https://github.com/microsoft/durabletask-js/compare/main...${BRANCH_NAME}?expand=1&title=Release+v${NEW_VERSION})" >> $GITHUB_STEP_SUMMARY
284+ echo "### Then Publish" >> $GITHUB_STEP_SUMMARY
285+ echo "" >> $GITHUB_STEP_SUMMARY
286+ echo "After the release PR is merged, publish from the package directory:" >> $GITHUB_STEP_SUMMARY
287+ echo '```bash' >> $GITHUB_STEP_SUMMARY
288+ echo "cd ${PKG_DIR}" >> $GITHUB_STEP_SUMMARY
289+ echo "${PUBLISH_CMD}" >> $GITHUB_STEP_SUMMARY
290+ echo '```' >> $GITHUB_STEP_SUMMARY
291+ if [[ "$NEW_VERSION" == *-* ]]; then
292+ echo "" >> $GITHUB_STEP_SUMMARY
293+ echo "> :warning: This is a prerelease. Omitting \`--tag preview\` would move the \`latest\` dist-tag to a prerelease version." >> $GITHUB_STEP_SUMMARY
294+ fi
0 commit comments