From 2bac8ffc0d64fb14079188f1287b19e90a011048 Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 28 Jul 2026 16:41:12 -0700 Subject: [PATCH 1/6] Make prepare-release workflow per-package The prepare-release workflow hardcoded two package paths and a single version namespace, so it could not release the new durable-functions compat package without destructively bumping unrelated packages to the wrong version and namespace. Running it with 4.0.0-beta.1 would catapult @microsoft/durabletask-js from 0.4.x to 4.0.0-beta.1, bump azuremanaged to 4.0.0-beta.1, leave azure-functions-durable untouched, and tag a mislabeled v4.0.0-beta.1. An npm name+version pair can never be reused (even after unpublish), so reaching npm that way is irreversible. Rework the workflow to release exactly one selected package at a time: - Add a required `package` choice input (durabletask-js, durabletask-js-azuremanaged, azure-functions-durable) and an early "Resolve package metadata" step mapping it to pkg_dir, npm_name, tag_prefix (v / azuremanaged-v / functions-v) and changelog_path. - Thread those outputs through every step: read the selected package's version, scope the latest-tag glob to the package's tag prefix, bump only the selected package.json, write the package's changelog, and create release/ plus a tag. - Delete the azuremanaged peerDependencies['@microsoft/durabletask-js'] rewrite: packages now release independently, so bumping a >=0.3.0 floor on every core release no longer makes sense. - Before releasing azure-functions-durable, verify its pinned @microsoft/durabletask-js dependency actually exists on the public npm registry, failing the run otherwise (core is 0.4.0 in-repo but npm's latest is 0.3.0, which would ship an uninstallable hard dependency). - Print the exact publish command in the run summary, deriving the dist-tag from the version: `npm publish --tag preview` for prereleases so they do not move `latest`, plain `npm publish` otherwise. scripts/update-changelog.js gains an optional 4th positional argument for the target changelog path (default CHANGELOG.md), so the compat package's packages/azure-functions-durable/CHANGELOG.md can be updated without changing existing 3-argument behavior or the `## Upcoming` logic. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d86f5f69-4e1a-4c1d-b017-5e290c85cc05 --- .github/workflows/prepare-release.yaml | 177 ++++++++++++++++++------- scripts/update-changelog.js | 16 ++- 2 files changed, 141 insertions(+), 52 deletions(-) diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index a71a205c..3bf8f1d0 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -1,15 +1,26 @@ # 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 -# After the workflow completes, manually create a PR from the release branch to main. +# This workflow automates per-package release preparation. Choose which package to release +# with the required `package` input; the workflow then, for THAT package only: +# 1. Bumps the selected package's version in its own package.json +# 2. Generates a changelog from git history since the package's last release tag +# 3. Updates the package's changelog file and creates a release branch + package-scoped tag +# Packages are versioned and tagged independently (tag prefixes: v, azuremanaged-v, functions-v). +# After the workflow completes, manually open a PR from the release branch to main, then publish +# from the package directory using the npm command shown in the run summary (mind the dist-tag). name: Prepare Release on: workflow_dispatch: inputs: + package: + description: 'Which package to release' + required: true + type: choice + options: + - durabletask-js + - durabletask-js-azuremanaged + - azure-functions-durable version: description: 'Release version (e.g., 0.2.0-beta.1). Leave empty to auto-increment patch version.' required: false @@ -32,10 +43,45 @@ jobs: with: node-version: '22' + - name: Resolve package metadata + id: resolve-package + run: | + PACKAGE="${{ github.event.inputs.package }}" + case "$PACKAGE" in + durabletask-js) + PKG_DIR="packages/durabletask-js" + NPM_NAME="@microsoft/durabletask-js" + TAG_PREFIX="v" + CHANGELOG_PATH="CHANGELOG.md" + ;; + durabletask-js-azuremanaged) + PKG_DIR="packages/durabletask-js-azuremanaged" + NPM_NAME="@microsoft/durabletask-js-azuremanaged" + TAG_PREFIX="azuremanaged-v" + CHANGELOG_PATH="CHANGELOG.md" + ;; + azure-functions-durable) + PKG_DIR="packages/azure-functions-durable" + NPM_NAME="durable-functions" + TAG_PREFIX="functions-v" + CHANGELOG_PATH="packages/azure-functions-durable/CHANGELOG.md" + ;; + *) + echo "::error::Unknown package '$PACKAGE'. Expected one of: durabletask-js, durabletask-js-azuremanaged, azure-functions-durable." + exit 1 + ;; + esac + echo "pkg_dir=$PKG_DIR" >> $GITHUB_OUTPUT + echo "npm_name=$NPM_NAME" >> $GITHUB_OUTPUT + echo "tag_prefix=$TAG_PREFIX" >> $GITHUB_OUTPUT + echo "changelog_path=$CHANGELOG_PATH" >> $GITHUB_OUTPUT + echo "Releasing '$PACKAGE': dir=$PKG_DIR npm=$NPM_NAME tag_prefix=$TAG_PREFIX changelog=$CHANGELOG_PATH" + - name: Get current version id: get-current-version run: | - CURRENT_VERSION=$(node -p "require('./packages/durabletask-js/package.json').version") + PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}" + CURRENT_VERSION=$(node -p "require('./${PKG_DIR}/package.json').version") echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT echo "Current version: $CURRENT_VERSION" @@ -77,10 +123,13 @@ jobs: 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) + TAG_PREFIX="${{ steps.resolve-package.outputs.tag_prefix }}" + # Get the latest tag for THIS package (its tag prefix), excluding the tag being + # created so that re-runs don't use it as the baseline. The legacy core prefix "v" + # still matches historical tags like v0.3.0 (intended, backward compatible) and never + # matches the "azuremanaged-v"/"functions-v" namespaces (they start with a/f, not v). + LATEST_TAG=$(git tag -l "${TAG_PREFIX}*" --sort=-v:refname | \ + grep -v "^${TAG_PREFIX}${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) @@ -111,48 +160,62 @@ jobs: echo "Generated changelog:" cat /tmp/changelog_content.txt - - name: Update package versions + - name: Update package version run: | NEW_VERSION="${{ steps.calc-version.outputs.new_version }}" + PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}" - echo "Updating packages to version $NEW_VERSION..." + echo "Updating ${PKG_DIR} 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')); + const path = '${PKG_DIR}/package.json'; + const pkg = JSON.parse(fs.readFileSync(path, 'utf8')); pkg.version = '$NEW_VERSION'; - fs.writeFileSync('packages/durabletask-js/package.json', JSON.stringify(pkg, null, 2) + '\n'); + fs.writeFileSync(path, 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 + echo "Updated ${PKG_DIR}/package.json:" + grep '"version"' "${PKG_DIR}/package.json" - - name: Update CHANGELOG.md + - name: Update changelog run: | NEW_VERSION="${{ steps.calc-version.outputs.new_version }}" CHANGELOG_FILE="${{ steps.changelog-diff.outputs.changelog_file }}" + CHANGELOG_PATH="${{ steps.resolve-package.outputs.changelog_path }}" RELEASE_DATE=$(date +%Y-%m-%d) - node scripts/update-changelog.js "$NEW_VERSION" "$RELEASE_DATE" "$CHANGELOG_FILE" + node scripts/update-changelog.js "$NEW_VERSION" "$RELEASE_DATE" "$CHANGELOG_FILE" "$CHANGELOG_PATH" + + - name: Verify pinned core dependency is published on npm + if: ${{ github.event.inputs.package == 'azure-functions-durable' }} + run: | + PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}" + PINNED=$(node -p "require('./${PKG_DIR}/package.json').dependencies['@microsoft/durabletask-js'] || ''") + if [ -z "$PINNED" ]; then + echo "::error::Could not read the @microsoft/durabletask-js pin from ${PKG_DIR}/package.json." + exit 1 + fi + echo "durable-functions pins @microsoft/durabletask-js@${PINNED}; verifying it is published on public npm..." + # Query the public registry explicitly: durable-functions is installed from npmjs.org, + # and the repo's committed .npmrc points npm at an auth-gated feed that would 401 here. + PUBLISHED=$(npm view "@microsoft/durabletask-js@${PINNED}" version --registry https://registry.npmjs.org/ 2>/dev/null || true) + if [ "$PUBLISHED" = "$PINNED" ]; then + echo "OK: @microsoft/durabletask-js@${PINNED} is published on npm." + else + 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." + exit 1 + fi - name: Create release branch and commit id: create-branch run: | NEW_VERSION="${{ steps.calc-version.outputs.new_version }}" - BRANCH_NAME="release/v${NEW_VERSION}" + PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}" + NPM_NAME="${{ steps.resolve-package.outputs.npm_name }}" + TAG_PREFIX="${{ steps.resolve-package.outputs.tag_prefix }}" + CHANGELOG_PATH="${{ steps.resolve-package.outputs.changelog_path }}" + TAG_NAME="${TAG_PREFIX}${NEW_VERSION}" + BRANCH_NAME="release/${TAG_NAME}" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -160,35 +223,59 @@ jobs: # Create and checkout release branch (idempotent) 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}" + # Stage and commit changes (only the released package and its changelog) + git add "${PKG_DIR}/package.json" + git add "${CHANGELOG_PATH}" + git commit -m "Release ${NPM_NAME}@${NEW_VERSION}" # Create release tag (idempotent) - git tag -f "v${NEW_VERSION}" + git tag -f "$TAG_NAME" # Push branch and tag (force to handle re-runs) git push -f origin "$BRANCH_NAME" - git push -f origin "v${NEW_VERSION}" + git push -f origin "$TAG_NAME" echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - echo "Created branch $BRANCH_NAME and tag v${NEW_VERSION}" + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "Created branch $BRANCH_NAME and tag $TAG_NAME" - name: Summary run: | NEW_VERSION="${{ steps.calc-version.outputs.new_version }}" + PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}" + NPM_NAME="${{ steps.resolve-package.outputs.npm_name }}" BRANCH_NAME="${{ steps.create-branch.outputs.branch_name }}" + TAG_NAME="${{ steps.create-branch.outputs.tag_name }}" + + # Derive the npm dist-tag from the version string. A prerelease (contains "-") must + # publish under a non-default tag so it does NOT move the "latest" dist-tag. + if [[ "$NEW_VERSION" == *-* ]]; then + PUBLISH_CMD="npm publish --tag preview" + else + PUBLISH_CMD="npm publish" + fi echo "## Release Preparation Complete! :rocket:" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Version**: v${NEW_VERSION}" >> $GITHUB_STEP_SUMMARY + echo "- **Package**: ${NPM_NAME}" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: ${NEW_VERSION}" >> $GITHUB_STEP_SUMMARY echo "- **Branch**: ${BRANCH_NAME}" >> $GITHUB_STEP_SUMMARY - echo "- **Tag**: v${NEW_VERSION}" >> $GITHUB_STEP_SUMMARY + echo "- **Tag**: ${TAG_NAME}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Next Step: Create a Pull Request" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "Manually create a PR from **${BRANCH_NAME}** → **main** with title: **Release v${NEW_VERSION}**" >> $GITHUB_STEP_SUMMARY + echo "Manually create a PR from **${BRANCH_NAME}** → **main** with title: **Release ${NPM_NAME}@${NEW_VERSION}**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "[Create PR](https://github.com/microsoft/durabletask-js/compare/main...${BRANCH_NAME}?expand=1&title=Release+${NPM_NAME}@${NEW_VERSION})" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "[Create PR](https://github.com/microsoft/durabletask-js/compare/main...${BRANCH_NAME}?expand=1&title=Release+v${NEW_VERSION})" >> $GITHUB_STEP_SUMMARY + echo "### Then Publish" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "After the release PR is merged, publish from the package directory:" >> $GITHUB_STEP_SUMMARY + echo '```bash' >> $GITHUB_STEP_SUMMARY + echo "cd ${PKG_DIR}" >> $GITHUB_STEP_SUMMARY + echo "${PUBLISH_CMD}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + if [[ "$NEW_VERSION" == *-* ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "> :warning: This is a prerelease. Omitting \`--tag preview\` would move the \`latest\` dist-tag to a prerelease version." >> $GITHUB_STEP_SUMMARY + fi diff --git a/scripts/update-changelog.js b/scripts/update-changelog.js index 9ba5f4a3..fafbe50f 100644 --- a/scripts/update-changelog.js +++ b/scripts/update-changelog.js @@ -1,18 +1,20 @@ #!/usr/bin/env node -// This script updates CHANGELOG.md with a new release section -// Usage: node update-changelog.js +// This script updates a changelog file with a new release section. +// Usage: node update-changelog.js [target-changelog] +// file containing the generated changelog entries to insert +// [target-changelog] changelog to update in place (default: CHANGELOG.md) const fs = require('fs'); -const [,, version, releaseDate, changelogFile] = process.argv; +const [,, version, releaseDate, changelogFile, targetChangelog = 'CHANGELOG.md'] = process.argv; if (!version || !releaseDate || !changelogFile) { - console.error('Usage: node update-changelog.js '); + console.error('Usage: node update-changelog.js [target-changelog]'); process.exit(1); } const changelogContent = fs.readFileSync(changelogFile, 'utf8').trim(); -let content = fs.readFileSync('CHANGELOG.md', 'utf8'); +let content = fs.readFileSync(targetChangelog, 'utf8'); const newSection = `## v${version} (${releaseDate}) @@ -33,5 +35,5 @@ if (upcomingMatch) { // 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); -console.log('Updated CHANGELOG.md'); +fs.writeFileSync(targetChangelog, content); +console.log(`Updated ${targetChangelog}`); From 8fa1cdd2f0a5a0c78242d5a825c77ad2749bb60f Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 28 Jul 2026 17:39:19 -0700 Subject: [PATCH 2/6] Target public npm registry on publish and guard core with prepublishOnly Add `publishConfig.registry = https://registry.npmjs.org/` to all three publishable packages (@microsoft/durabletask-js, @microsoft/durabletask-js-azuremanaged, durable-functions). The repo has no committed .npmrc; root package.json's `preinstall` hook (scripts/preinstallNpmrc.js) generates a gitignored .npmrc pointing at the azfunc Azure DevOps feed for @microsoft.com users and azfunc CI, so `npm publish` from a Microsoft machine would otherwise target that feed instead of public npm. publishConfig travels with the package and pins the registry regardless of the publishing machine's .npmrc. prepare-release.yaml: - Correct the false comment in the "Verify pinned core dependency" step: the repo does not commit an .npmrc; the preinstall hook generates a gitignored one. Keep the explicit --registry flag (still needed). - Print the publish command with an explicit --registry https://registry.npmjs.org/ (belt-and-braces so a copied command is safe even if publishConfig is ever dropped). durabletask-js: add `prepublishOnly: npm run build && npm run test` (the only publishable package without a build/test publish guard), matching the string used by the other two packages verbatim. Core's test suite is backend-independent (67 suites / 1176 tests pass with no sidecar). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d86f5f69-4e1a-4c1d-b017-5e290c85cc05 --- .github/workflows/prepare-release.yaml | 12 ++++++++---- packages/azure-functions-durable/package.json | 3 +++ packages/durabletask-js-azuremanaged/package.json | 3 +++ packages/durabletask-js/package.json | 6 +++++- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 3bf8f1d0..154b33b1 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -196,8 +196,12 @@ jobs: exit 1 fi echo "durable-functions pins @microsoft/durabletask-js@${PINNED}; verifying it is published on public npm..." - # Query the public registry explicitly: durable-functions is installed from npmjs.org, - # and the repo's committed .npmrc points npm at an auth-gated feed that would 401 here. + # Query the public registry explicitly: durable-functions is installed from npmjs.org. + # The repo has NO committed .npmrc; instead root package.json's "preinstall" hook + # (scripts/preinstallNpmrc.js) generates a gitignored .npmrc via scripts/setupNpmrc.js + # that points npm at the azfunc Azure DevOps feed for @microsoft.com users and azfunc CI + # builds. Forcing --registry keeps this check on public npm regardless of any such + # generated (or machine-level) npm config, which would otherwise 401 on this query. PUBLISHED=$(npm view "@microsoft/durabletask-js@${PINNED}" version --registry https://registry.npmjs.org/ 2>/dev/null || true) if [ "$PUBLISHED" = "$PINNED" ]; then echo "OK: @microsoft/durabletask-js@${PINNED} is published on npm." @@ -250,9 +254,9 @@ jobs: # Derive the npm dist-tag from the version string. A prerelease (contains "-") must # publish under a non-default tag so it does NOT move the "latest" dist-tag. if [[ "$NEW_VERSION" == *-* ]]; then - PUBLISH_CMD="npm publish --tag preview" + PUBLISH_CMD="npm publish --registry https://registry.npmjs.org/ --tag preview" else - PUBLISH_CMD="npm publish" + PUBLISH_CMD="npm publish --registry https://registry.npmjs.org/" fi echo "## Release Preparation Complete! :rocket:" >> $GITHUB_STEP_SUMMARY diff --git a/packages/azure-functions-durable/package.json b/packages/azure-functions-durable/package.json index 5a4206a5..f333afa5 100644 --- a/packages/azure-functions-durable/package.json +++ b/packages/azure-functions-durable/package.json @@ -44,6 +44,9 @@ "url": "https://github.com/microsoft/durabletask-js/issues" }, "homepage": "https://github.com/microsoft/durabletask-js/tree/main/packages/azure-functions-durable#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, "engines": { "node": ">=22.0.0" }, diff --git a/packages/durabletask-js-azuremanaged/package.json b/packages/durabletask-js-azuremanaged/package.json index c7f7f6b4..1c6c11f7 100644 --- a/packages/durabletask-js-azuremanaged/package.json +++ b/packages/durabletask-js-azuremanaged/package.json @@ -43,6 +43,9 @@ "url": "https://github.com/microsoft/durabletask-js/issues" }, "homepage": "https://github.com/microsoft/durabletask-js/tree/main/extensions/durabletask-js-azuremanaged#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, "engines": { "node": ">=22.0.0" }, diff --git a/packages/durabletask-js/package.json b/packages/durabletask-js/package.json index 598a96c9..90375433 100644 --- a/packages/durabletask-js/package.json +++ b/packages/durabletask-js/package.json @@ -15,7 +15,8 @@ "prebuild": "node -p \"'// Auto-generated by prebuild\\nexport const SDK_VERSION = ' + JSON.stringify(require('./package.json').version) + ';\\nexport const SDK_PACKAGE_NAME = ' + JSON.stringify(require('./package.json').name) + ';'\" > src/version.ts", "build": "npm run prebuild && npm run clean && tsc -p tsconfig.build.json && npm run copy-proto", "test": "jest --runInBand --detectOpenHandles", - "test:unit": "jest test --runInBand --detectOpenHandles" + "test:unit": "jest test --runInBand --detectOpenHandles", + "prepublishOnly": "npm run build && npm run test" }, "engines": { "node": ">=22.0.0" @@ -37,6 +38,9 @@ "url": "https://github.com/microsoft/durabletask-js/issues" }, "homepage": "https://github.com/microsoft/durabletask-js#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, "dependencies": { "@grpc/grpc-js": "^1.14.4", "google-protobuf": "^3.21.2" From 5fd89b74e7afa83ac67fd7666f8dfc64aae1a6df Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 28 Jul 2026 17:59:33 -0700 Subject: [PATCH 3/6] docs+release: promote curated Upcoming notes, document bundle floor, correct WhenAll/#317/publishConfig update-changelog.js: a release now PROMOTES the curated `## Upcoming` body into the new `## v` section instead of discarding it (fixes silent data loss where curated notes were overwritten by the generated commit list). Empty scaffold subsections (`### New` / `### Fixes` with no body) are dropped, so cutting a release with an untouched Upcoming section is byte-for-byte identical to before. The no-Upcoming fallback, the final Upcoming reset, the 4th-arg target path, and `## v` boundary detection are unchanged. Verified with scratch runs across four cases (empty scaffold diff-identical to the pre-change script; curated Breaking-changes block promoted; no-Upcoming fallback; 3-arg default target). CHANGELOG.md: fill `## Upcoming` with a `### Breaking changes` section for the two core 0.4.0 changes -- whenAll no longer fails fast (throws an AggregateError only after every child is terminal; fixes #301) and default sub-orchestration instance IDs derive from the parent execution ID (with a legacy fallback when executionId is empty). New/Fixes left as empty scaffold for the workflow to append. publishConfig: add `access: public` to the two scoped packages (@microsoft/durabletask-js, @microsoft/durabletask-js-azuremanaged) so a first publish cannot fail `restricted`; omitted for the unscoped durable-functions where access is meaningless. Docs: - azure-functions-durable/README.md: the v3 df.lock / isLocked surface is not supported and not planned (#317 is closed not_planned; the doc PR was closed unmerged) -- drop the dead tracking-issue pointer. Add a Requirements section documenting the Functions extension-bundle floor (GA [4.36.0, 5.0.0) or Preview [4.29.0, 5.0.0)); earlier GA v4 bundles lack the durable gRPC endpoint and cause a silent ~60s starter hang. - copilot-instructions.md: correct the WhenAll description in the task hierarchy tree and the renamed "WhenAll Wait-All Behavior" section -- it completes only when every child is terminal and aggregates failures into an AggregateError, not fail-fast. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d86f5f69-4e1a-4c1d-b017-5e290c85cc05 --- .github/copilot-instructions.md | 14 +++++++---- CHANGELOG.md | 25 +++++++++++++++++++ packages/azure-functions-durable/README.md | 18 +++++++++++-- .../durabletask-js-azuremanaged/package.json | 3 ++- packages/durabletask-js/package.json | 3 ++- scripts/update-changelog.js | 22 +++++++++++++++- 6 files changed, 75 insertions(+), 10 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c8c15017..2e177551 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -113,7 +113,7 @@ Task — Base: result, exception, isComplete, isFailed, parent r │ ├── RetryableTask — Policy-driven retries (exponential backoff) │ └── RetryHandlerTask — Custom retry handler function └── CompositeTask — Holds children with parent backlinks - ├── WhenAllTask — Completes when ALL children done; fails fast on first failure + ├── WhenAllTask — Completes when ALL children are terminal (waits for every child) └── WhenAnyTask — Completes when ANY child done ``` @@ -123,11 +123,15 @@ Task — Base: result, exception, isComplete, isFailed, parent r **Important:** `CompositeTask` checks already-complete children in its constructor. A composite task can be **immediately complete upon construction** — callers must handle this. -### WhenAll Fail-Fast Behavior +### WhenAll Wait-All Behavior -`WhenAllTask` marks itself complete on the **first** failed child. Other children may still -be in flight. The `isComplete` guard prevents double-completion, but the mental model is: -one failure = whole task fails immediately, remaining results ignored. +`WhenAllTask` completes only when **every** child is terminal (`_completedTasks == _tasks.length`) — +a failing child does **not** complete it early. This prevents a later failing sibling's `TaskFailed` +from being dropped against an already-terminal instance (issue #301). `_exception` is set only at +completion, so `isFailed` and `isComplete` flip together — otherwise `resume()` (which checks +`isFailed` before `isComplete`) would throw into the generator before the other siblings finish, +re-introducing fail-fast. On failure, all child exceptions are aggregated into an `AggregateError` +whose message inlines each child message. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index a725396a..5faa07b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ ## Upcoming +### Breaking changes + +- **`whenAll` no longer fails fast.** A `whenAll` task — and therefore the Durable Functions compat + `context.df.Task.all()`, which forwards straight to it — now completes only after **every** child + is terminal, instead of the moment the first child fails. Two user-visible effects: (1) **timing** — + the orchestrator resumes past the `whenAll` only once all siblings finish, not at the first failure; + (2) **error type** — on failure it throws a JS-native `AggregateError` (`.errors` populated, message + of the form `" of tasks in the whenAll failed: "`) instead of + re-throwing the first child's own exception. This fixes a lost-failure bug where a later failing + sibling was dropped against an already-terminal `whenAll` + ([#301](https://github.com/microsoft/durabletask-js/issues/301)). Fan-out/fan-in is the canonical + Durable Functions pattern, so review any `Task.all()` error handling that relied on early completion + or on catching a single child error. +- **Default sub-orchestration instance IDs now derive from the parent's execution ID.** When a + sub-orchestration is scheduled **without** an explicit `instanceId`, the auto-generated ID changed + from `${parentInstanceId}:${suffix}` to `${parentExecutionId}:${suffix}` (`suffix` is a 4-hex-digit + sequence number). Explicit `instanceId`s are untouched, and there is a **fallback**: backends that + do not populate an execution ID still get the legacy `${parentInstanceId}:${suffix}` form, so this + is not an unconditional format change. It fixes a continue-as-new collision — the sequence number + resets each work item and the parent instance ID is stable across generations, so a default-ID + sub-orchestration scheduled after continue-as-new (e.g. `callHttp`) re-derived the previous + generation's ID and failed with "instance already exists" — and keeps the ID constant-length + (~37 chars) at any nesting depth instead of concatenating every ancestor's ID, which would breach + the Durable Task Scheduler 100-character instance-ID limit at ~2 levels of nesting. + ### New ### Fixes diff --git a/packages/azure-functions-durable/README.md b/packages/azure-functions-durable/README.md index 3d642a08..3d2265f3 100644 --- a/packages/azure-functions-durable/README.md +++ b/packages/azure-functions-durable/README.md @@ -42,8 +42,8 @@ changed: `context.df.isLocked()` and the `DurableLock` / `LockState` / `LockingRulesViolationError` exports are removed. Acquire locks with the core `context.entities.lockEntities(...entityIds)` (returns a `LockHandle` — call `release()`, ideally in a `finally`) and query with - `context.entities.isInCriticalSection()`. Restoring the v3 `df.lock` / `isLocked` surface is tracked - in [#317](https://github.com/microsoft/durabletask-js/issues/317). + `context.entities.isInCriticalSection()`. The v3 `df.lock` / `isLocked` surface is **not supported + and not planned** — there is no tracking issue. - **`context.df.callHttp(...)` is restored** as a worker-side durable HTTP call ([#318](https://github.com/microsoft/durabletask-js/issues/318)) — though **not** as a drop-in, fully v3-equivalent replacement: the known incompatibilities and behavior differences listed below are @@ -106,6 +106,20 @@ changed: sync **generators** (`function*`) using `context.df.*` — are unaffected; convert any non-generator classic orchestrator to generator form, or to the core-native `ctx.*` API. +## Requirements + +This provider reaches the Durable Task backend over the Functions host's **gRPC** channel, which +exists only in newer durable-extension builds. Your app's `host.json` must reference one of: + +- GA bundle — `Microsoft.Azure.Functions.ExtensionBundle` at **`[4.36.0, 5.0.0)`**, or +- Preview bundle — `Microsoft.Azure.Functions.ExtensionBundle.Preview` at **`[4.29.0, 5.0.0)`**. + +Earlier GA v4 bundles (**<= 4.32.0**) predate that gRPC endpoint, so orchestration starters **hang +for ~60 seconds and time out with no error**. A fresh app on the default GA range (`[4.*, 5.0.0)`) +resolves to the latest GA (>= 4.36.0) and works — the trap is an **explicit** pin at or below +4.32.0. GA and Preview are independent feeds whose version numbers are **not comparable** (a higher +Preview number does not imply the GA bundle contains the same code). + ## Getting started ```typescript diff --git a/packages/durabletask-js-azuremanaged/package.json b/packages/durabletask-js-azuremanaged/package.json index 1c6c11f7..6b04d399 100644 --- a/packages/durabletask-js-azuremanaged/package.json +++ b/packages/durabletask-js-azuremanaged/package.json @@ -44,7 +44,8 @@ }, "homepage": "https://github.com/microsoft/durabletask-js/tree/main/extensions/durabletask-js-azuremanaged#readme", "publishConfig": { - "registry": "https://registry.npmjs.org/" + "registry": "https://registry.npmjs.org/", + "access": "public" }, "engines": { "node": ">=22.0.0" diff --git a/packages/durabletask-js/package.json b/packages/durabletask-js/package.json index 90375433..44805cf8 100644 --- a/packages/durabletask-js/package.json +++ b/packages/durabletask-js/package.json @@ -39,7 +39,8 @@ }, "homepage": "https://github.com/microsoft/durabletask-js#readme", "publishConfig": { - "registry": "https://registry.npmjs.org/" + "registry": "https://registry.npmjs.org/", + "access": "public" }, "dependencies": { "@grpc/grpc-js": "^1.14.4", diff --git a/scripts/update-changelog.js b/scripts/update-changelog.js index fafbe50f..be902201 100644 --- a/scripts/update-changelog.js +++ b/scripts/update-changelog.js @@ -16,9 +16,29 @@ if (!version || !releaseDate || !changelogFile) { const changelogContent = fs.readFileSync(changelogFile, 'utf8').trim(); let content = fs.readFileSync(targetChangelog, 'utf8'); +// Promote any curated notes from the current "## Upcoming" section into the release section. +// A subsection that is just an empty scaffold heading (e.g. "### New" with nothing under it) is +// dropped, so cutting a release with an untouched Upcoming section promotes nothing and produces +// byte-for-byte the same output as before. +let promoted = ''; +const currentUpcoming = content.match(/## Upcoming[\s\S]*?(?=\n## v|$)/); +if (currentUpcoming) { + const body = currentUpcoming[0].replace(/^## Upcoming[^\n]*\n?/, ''); + const kept = []; + for (const part of body.split(/(?=^### )/m)) { + const heading = part.match(/^### [^\n]*/); + if (!heading) continue; // whitespace before the first subsection heading + const subContent = part.slice(heading[0].length).trim(); + if (subContent) { + kept.push(`${heading[0].trim()}\n\n${subContent}`); + } + } + promoted = kept.join('\n\n'); +} + const newSection = `## v${version} (${releaseDate}) -### Changes +${promoted ? promoted + '\n\n' : ''}### Changes ${changelogContent} `; From 908d37115f7f668eb163329dce332f42f98ef892 Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 28 Jul 2026 18:14:43 -0700 Subject: [PATCH 4/6] ci(release): scope changelog commit list to package; promote lead-in; seed compat changelog 5A: The "Generate changelog diff" step in prepare-release.yaml ran `git log "$LATEST_TAG"..HEAD` with no path filter, so a release listed commits from every package. For a package with no release tag yet (compat, azuremanaged) LATEST_TAG falls back to the repo's initial commit, so the list became the entire repo history (193 commits for compat). Add a `-- "$PKG_DIR"` path filter (after the `--` separator) so a release only lists commits that touched that package. Effect: compat 193->5, azuremanaged 193->20, core (v0.3.0..HEAD) 93->66. The existing `--no-merges`, the `(#N)`->markdown-link sed, the `- ` prefix, and the empty-line grep are unchanged. 5B: update-changelog.js promotion (added in the prior release-hygiene commit) skipped any lead-in body text written directly under `## Upcoming` before the first `### ` subsection, silently dropping it. Promote that lead-in too, placed ahead of the promoted subsections; whitespace-only lead-in still promotes nothing, so the empty-scaffold case stays byte-identical to the pre-promotion script. 5C: Replace the compat package's placeholder CHANGELOG body with a curated `## Upcoming` section for 4.0.0-beta.1 (a preview lead-in + install fence, then Why-this-is-a-new-major-version / Requirements / Underlying packages / Breaking changes / Added / Known limitations). The release workflow promotes this and appends the scoped commit list as `### Changes`; it does not yet carry a `## v` heading. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d86f5f69-4e1a-4c1d-b017-5e290c85cc05 --- .github/workflows/prepare-release.yaml | 11 +- packages/azure-functions-durable/CHANGELOG.md | 100 +++++++++++++++++- scripts/update-changelog.js | 8 +- 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 154b33b1..569547a4 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -143,12 +143,17 @@ jobs: 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..." + PKG_DIR="${{ steps.resolve-package.outputs.pkg_dir }}" + echo "Generating changelog for changes between $LATEST_TAG and HEAD (scoped to $PKG_DIR)..." - # Get commits between last tag and HEAD. + # Get commits between last tag and HEAD, scoped to the released package's directory via the + # "-- $PKG_DIR" path filter so a release only lists commits that touched that package. + # Without it every package's commits are listed -- and since a package with no release tag + # yet falls back to the repo's initial commit, the first release would dump the ENTIRE repo + # history into that package's changelog. # 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 | \ + CHANGELOG_CONTENT=$(git log "$LATEST_TAG"..HEAD --pretty=format:"%s" --no-merges -- "$PKG_DIR" | \ sed 's/(#\([0-9]*\))/([#\1](https:\/\/github.com\/microsoft\/durabletask-js\/pull\/\1))/' | \ sed 's/^/- /' | \ grep -v '^- $' || echo "") diff --git a/packages/azure-functions-durable/CHANGELOG.md b/packages/azure-functions-durable/CHANGELOG.md index 00858aea..60caa077 100644 --- a/packages/azure-functions-durable/CHANGELOG.md +++ b/packages/azure-functions-durable/CHANGELOG.md @@ -1,5 +1,99 @@ -# Changelog +## Upcoming -## TBD +First preview of the rewritten `durable-functions` provider. This is a **preview release**: it is +published under the `preview` npm dist-tag, and APIs may change before the stable `4.0.0`. -- Details to be finalized at release time. +Install with: + +```bash +npm install durable-functions@preview +``` + +### Why this is a new major version + +v4 is a rewrite on top of `@microsoft/durabletask-js`: replay, activities, entities, retries, and +instance-management all come from that shared core, so behavior matches the .NET, Python, and Java +Durable providers. Durable work items flow over a single gRPC channel instead of the legacy +out-of-process HTTP protocol -- the same consolidation `durabletask-python` shipped as +`azure-functions-durable` 2.0.0b1. It supersedes the legacy `durable-functions` v3 +([Azure/azure-functions-durable-js](https://github.com/Azure/azure-functions-durable-js)). Classic +`context.df.*` orchestrators and entities keep working through a compatibility layer, but several +surfaces changed (see below). + +### Requirements + +- **Node.js >= 22** (v3 supported Node 18 and 20; those are no longer supported). +- **Functions extension bundle** -- GA `Microsoft.Azure.Functions.ExtensionBundle` `[4.36.0, 5.0.0)` + or Preview `Microsoft.Azure.Functions.ExtensionBundle.Preview` `[4.29.0, 5.0.0)`. The provider needs + a durable extension that starts a gRPC server for Node + ([Azure/azure-functions-durable-extension#3260](https://github.com/Azure/azure-functions-durable-extension/pull/3260)); + on an earlier bundle the app starts normally but every orchestration starter hangs and times out + after ~60 s with no error. A fresh app on the default GA range works -- the trap is an explicit pin + at or below 4.32.0. GA and Preview version numbers are NOT comparable across feeds. + +### Underlying packages + +- `@microsoft/durabletask-js` `0.4.0` (exact pin). +- `@azure/functions` `^4.16.1`. +- `@azure/identity` `^4.0.0` as an **optional** peer dependency -- needed only for a `callHttp` + `tokenSource`, and not installed automatically. + +### Breaking changes from durable-functions v3 + +- **Node.js >= 22** is required. +- **Classic contexts no longer extend `InvocationContext`** -- only `df` plus replay-safe log helpers + are available (the classic entity context is just `{ df }`). +- **Task result shape follows the core SDK** -- use `isComplete` / `isFailed` / `getResult()` instead + of v3's `isCompleted` / `isFaulted` / `result`. `context.df.createTimer(...)` still returns a + cancelable `TimerTask`. +- **Entity locking moved to the core context.** `context.df.lock` / `context.df.isLocked` and the + `DurableLock` / `LockState` / `LockingRulesViolationError` exports are removed and will **not** be + restored. Acquire locks with `context.entities.lockEntities(...)` (returns a `LockHandle` with + `release()`) and query with `context.entities.isInCriticalSection()`. +- **A plain non-generator classic orchestrator is no longer supported.** A synchronous, + single-argument, non-generator function is now treated as core-native and receives the core + `OrchestrationContext` (which has no `.df`); sync generators (`function*`) using `context.df.*` are + unaffected. +- **Some v3 top-level exports were removed** -- `DummyOrchestrationContext`, `DummyEntityContext`, and + the entity-lock types above. `TaskFailedError` is re-exported from the core SDK, and aggregate + failures now surface as JS-native `AggregateError`; use the core `TestOrchestrationWorker` / + `TestOrchestrationClient` for orchestration unit tests. + +### Added + +- **`context.df.callHttp(...)` is restored** as a worker-side durable HTTP call. It accepts the v3 + `CallHttpOptions` (`method`, `url`, `body`, `headers`, `tokenSource`, `enablePolling`) and returns a + `Task` (`{ statusCode, headers, content }`), including automatic `202 Accepted` + polling that honors `Retry-After` via durable timers. Caveats: + - **Trust-boundary change** -- in v3 the Functions **host** extension ran the request; here it runs + as a durable **activity inside your worker** (via `fetch`), so egress path, source identity, and + firewall/VNet rules follow the worker process -- re-verify IP allow-lists. + - **Cross-origin `202` poll credentials are stripped** -- when the callee-controlled `Location` + points to a different origin, the poll drops `Authorization`, `Cookie`, and the `tokenSource`; + `x-functions-key` is **always** dropped; same-origin polls still forward headers and the + `tokenSource`. Mirrors the .NET extension + ([Azure/azure-functions-durable-extension#3443](https://github.com/Azure/azure-functions-durable-extension/pull/3443)). + - **The initial request follows redirects with `fetch` defaults**, which drop `Authorization` / + `Cookie` across origins but **not** custom credential headers like `x-functions-key`. + - **Default poll interval is 30 s** when a `202` carries no usable `Retry-After`. + - **Known incompatibility** -- a body on a `GET`/`HEAD` request throws (the Fetch Standard forbids + it, though v3, the .NET extension, and Python all send it). + - **Known incompatibility** -- `DurableHttpResponse` is a plain object, not a class, so + `response.getHeader(name)` is unavailable and existing calls **fail at runtime**; index + `response.headers[...]` by lower-cased key instead. + - A managed-identity `tokenSource` needs the optional `@azure/identity` package or it throws a clear + error. +- **`app.client.*` starter helpers** (`http`, `timer`, `storageBlob`, `storageQueue`, + `serviceBusQueue`, `serviceBusTopic`, `eventHub`, `eventGrid`, `cosmosDB`, `generic`) register a + normal trigger and inject the client as the handler's second argument, so + `(trigger, client, context)` works without manually wiring `df.input.durableClient()` + + `df.getClient(context)`. +- **`client.startNew()` supports the `version` option.** +- **`client.getStatus()` keeps the v3 shape** -- a non-optional `DurableOrchestrationStatus` that + throws when the instance is missing; `showInput` suppresses only the top-level input, `showHistory` + populates `history`, and `showHistoryOutput` toggles the per-entry input/result payloads; `history` + entries are core `HistoryEvent`s (v3 typed it `Array`). + +### Known limitations + +- This is a preview release; the API surface may change before `4.0.0`. diff --git a/scripts/update-changelog.js b/scripts/update-changelog.js index be902201..111e8559 100644 --- a/scripts/update-changelog.js +++ b/scripts/update-changelog.js @@ -27,7 +27,13 @@ if (currentUpcoming) { const kept = []; for (const part of body.split(/(?=^### )/m)) { const heading = part.match(/^### [^\n]*/); - if (!heading) continue; // whitespace before the first subsection heading + if (!heading) { + // Lead-in body text before the first "### " subsection (e.g. a preview notice). Promote it + // too, ahead of the subsections; a whitespace-only scaffold gap promotes nothing. + const lead = part.trim(); + if (lead) kept.push(lead); + continue; + } const subContent = part.slice(heading[0].length).trim(); if (subContent) { kept.push(`${heading[0].trim()}\n\n${subContent}`); From 8501bc0720daeacc9a21659921cf81860efb70d1 Mon Sep 17 00:00:00 2001 From: wangbill Date: Wed, 29 Jul 2026 15:10:13 -0700 Subject: [PATCH 5/6] ci(release): rename compat tag prefix to durable-functions-v; fix prerelease sort 7A: Rename the azure-functions-durable release tag prefix from `functions-v` to `durable-functions-v`. A git tag should name the package a reader can look up on the registry; our npm package is `durable-functions` (`azure-functions-durable` is only the directory name, which drove the original choice). This mirrors how `azuremanaged-v` matches the npm-name suffix of `@microsoft/durabletask-js-azuremanaged`; core `v` and azuremanaged `azuremanaged-v` already match durabletask-python, only compat diverged. The resulting tag is `durable-functions-v4.0.0-beta.1` and branch `release/durable-functions-v4.0.0-beta.1` (valid ref). No `functions-v*` tag has ever existed, so nothing to retag. 7B: Add `-c versionsort.suffix=-` to the latest-tag lookup so git orders a prerelease (X-beta.1) below its own GA (X). With git's default config, `--sort=-v:refname` ranks X-beta.1 ABOVE X, so the first post-GA release (e.g. X.0.1) would resolve LATEST_TAG to the prerelease baseline and re-list every commit already shipped in GA. This is a pre-existing flaw independent of 7A. The flag is scoped to that single tag-listing command only; not set globally. Only .github/workflows/prepare-release.yaml changes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d86f5f69-4e1a-4c1d-b017-5e290c85cc05 --- .github/workflows/prepare-release.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 569547a4..769cf926 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -4,7 +4,7 @@ # 1. Bumps the selected package's version in its own package.json # 2. Generates a changelog from git history since the package's last release tag # 3. Updates the package's changelog file and creates a release branch + package-scoped tag -# Packages are versioned and tagged independently (tag prefixes: v, azuremanaged-v, functions-v). +# Packages are versioned and tagged independently (tag prefixes: v, azuremanaged-v, durable-functions-v). # After the workflow completes, manually open a PR from the release branch to main, then publish # from the package directory using the npm command shown in the run summary (mind the dist-tag). @@ -63,7 +63,7 @@ jobs: azure-functions-durable) PKG_DIR="packages/azure-functions-durable" NPM_NAME="durable-functions" - TAG_PREFIX="functions-v" + TAG_PREFIX="durable-functions-v" CHANGELOG_PATH="packages/azure-functions-durable/CHANGELOG.md" ;; *) @@ -127,8 +127,12 @@ jobs: # Get the latest tag for THIS package (its tag prefix), excluding the tag being # created so that re-runs don't use it as the baseline. The legacy core prefix "v" # still matches historical tags like v0.3.0 (intended, backward compatible) and never - # matches the "azuremanaged-v"/"functions-v" namespaces (they start with a/f, not v). - LATEST_TAG=$(git tag -l "${TAG_PREFIX}*" --sort=-v:refname | \ + # matches the "azuremanaged-v"/"durable-functions-v" namespaces (they start with a/d, not v). + # Without versionsort.suffix=-, git ranks a prerelease (e.g. X-beta.1) ABOVE its own GA + # (X), so the release AFTER GA (e.g. X.0.1) would resolve LATEST_TAG to the prerelease and + # re-list every commit already shipped in GA. Treating "-" as the prerelease separator + # orders X-beta.1 below X. + LATEST_TAG=$(git -c versionsort.suffix=- tag -l "${TAG_PREFIX}*" --sort=-v:refname | \ grep -v "^${TAG_PREFIX}${NEW_VERSION}$" | head -n 1) if [ -z "$LATEST_TAG" ]; then echo "No previous release tag found, using initial commit" From c60230253fe453169c14bb278271852f98b9fd7c Mon Sep 17 00:00:00 2001 From: wangbill Date: Wed, 29 Jul 2026 15:34:26 -0700 Subject: [PATCH 6/6] docs(release): rewrite release_process.md for the per-package model doc/release_process.md is the runbook Bill will follow for this release, but it still described the old "all packages share one version, publish manually" model and, followed as written, would produce a wrong release. Update it to match the per-package release tooling (PR #335 prepare-release workflow + PR #336 ADO per-package stages). - Scope: cover all three published packages -- @microsoft/durabletask-js (core), @microsoft/durabletask-js-azuremanaged, and durable-functions (source dir packages/azure-functions-durable) -- in the intro, Overview table, the Step 2 .tgz list, and the Step 4 npm-view list. - Versioning Policy: replace "all packages same version" with per-package independent versioning/changelog/tags; document the one hard ordering constraint (compat exact-pins core, so core must publish first or the published compat package is uninstallable) and that azuremanaged is unordered (peer floor >=0.3.0 already satisfied by published core). - Resolve the self-contradiction: ESRP via eng/ci/release.yml is the sanctioned publish path; a manual npm publish is the alternative. - Prepare Release section: document the required `package` input and that the workflow bumps only the selected package and writes only that package's own changelog file (root CHANGELOG.md for core/azuremanaged, packages/azure-functions-durable/CHANGELOG.md for durable-functions). - Branch/tag naming: prefix-derived (v / azuremanaged-v / durable-functions-v) with concrete examples, in the workflow section, Step 2, Step 5, and the manual steps. - Step 3: three ADO stages (not jobs), individually selectable at queue time, core-before-compat ordering, and the explicit Skipped-accepting condition (rejects Failed/Canceled) added in PR #336. - Manual section: bump only the released package; add the durable-functions exact-pin dependency step. - Dist tags: reconcile the general semver table with the tooling (every prerelease -> preview) and make durable-functions@preview explicit (cross-checked against packages/azure-functions-durable/CHANGELOG.md). - Flag B11 (whether ESRP can set an npm dist-tag) as an explicit open question, not fact. Docs only; no code or behavior change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d86f5f69-4e1a-4c1d-b017-5e290c85cc05 --- doc/release_process.md | 132 +++++++++++++++++++++++++++-------------- 1 file changed, 88 insertions(+), 44 deletions(-) diff --git a/doc/release_process.md b/doc/release_process.md index 81535285..b39d5798 100644 --- a/doc/release_process.md +++ b/doc/release_process.md @@ -1,19 +1,25 @@ # Release Process -This document describes how to cut a new release of the `@microsoft/durabletask-js` and `@microsoft/durabletask-js-azuremanaged` npm packages. +This document describes how to cut a new release of the three npm packages published from this monorepo: `@microsoft/durabletask-js` (core), `@microsoft/durabletask-js-azuremanaged`, and `durable-functions` (the Azure Functions v4 compatibility provider, whose source lives in `packages/azure-functions-durable`). ## Overview -| Package | npm | -|---|---| -| `@microsoft/durabletask-js` | [npmjs](https://www.npmjs.com/package/@microsoft/durabletask-js) | -| `@microsoft/durabletask-js-azuremanaged` | [npmjs](https://www.npmjs.com/package/@microsoft/durabletask-js-azuremanaged) | +| Package (npm name) | Directory | npm | +|---|---|---| +| `@microsoft/durabletask-js` (core) | `packages/durabletask-js` | [npmjs](https://www.npmjs.com/package/@microsoft/durabletask-js) | +| `@microsoft/durabletask-js-azuremanaged` | `packages/durabletask-js-azuremanaged` | [npmjs](https://www.npmjs.com/package/@microsoft/durabletask-js-azuremanaged) | +| `durable-functions` | `packages/azure-functions-durable` | [npmjs](https://www.npmjs.com/package/durable-functions) | -This is an **npm workspaces** monorepo. There is no automated publish pipeline — the official build (`eng/ci/official-build.yml`) produces signed `.tgz` artifacts which are then published to npm manually. +This is an **npm workspaces** monorepo. The official build (`eng/ci/official-build.yml`) produces signed `.tgz` artifacts, and the **sanctioned way to publish them to npm is the ESRP release pipeline** (`eng/ci/release.yml`, see Step 3 below). A manual `npm publish` from the package directory is documented as an alternative for maintainers who need it. ### Versioning Policy -**All packages are released with the same version number.** This simplifies dependency management and makes it clear which versions are compatible. When cutting a release, both packages are bumped to the same version regardless of whether both have changes. +**Each package is versioned, changelogged, and tagged independently.** A release covers exactly one package; its version number, its changelog, and its git tag advance on their own and do **not** have to match the other packages. + +There is one hard ordering constraint between packages: + +- **`@microsoft/durabletask-js` (core) must be published before `durable-functions`.** The compat package declares an **exact** dependency on core (currently `"@microsoft/durabletask-js": "0.4.0"`). If `durable-functions` is published while that exact core version is not yet on npm, the published compat package is **uninstallable**. +- **`@microsoft/durabletask-js-azuremanaged` is unordered relative to `durable-functions`.** It depends on core only through a peer **floor** (`"@microsoft/durabletask-js": ">=0.3.0"`), which is already satisfied by the published core, so it can be released before or after the other packages. ## Versioning Scheme @@ -30,6 +36,8 @@ X.Y.Z-alpha.N → X.Y.Z-beta.N → X.Y.Z-rc.N → X.Y.Z (stable) | Release Candidate | `0.1.0-rc.1` | `--tag next` | | Stable | `0.1.0` | *(no tag, becomes `latest`)* | +> Note: current tooling overrides the beta/rc rows above for prereleases. The **Prepare Release** workflow publishes every prerelease under the single `preview` dist-tag, and `durable-functions` 4.x previews ship under `preview` (`npm install durable-functions@preview`). See *Quick Reference: npm Dist Tags* below. + ## Automated Release Preparation (Recommended) Use the **Prepare Release** GitHub Action to automate the release preparation process. @@ -38,25 +46,28 @@ 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., `0.2.0-beta.1`). Leave empty to auto-increment. -4. Click **Run workflow** +3. **Select the `package` to release** (required) — one of `durabletask-js`, `durabletask-js-azuremanaged`, or `azure-functions-durable`. Each run releases exactly one package. +4. Optionally specify a version (e.g., `0.2.0-beta.1` for core, or `4.0.0-beta.1` for `durable-functions`). Leave empty to auto-increment the selected package's current version (bumps the prerelease number for a prerelease, otherwise the patch). +5. Click **Run workflow** ### What the Workflow Does -1. **Determines the next version**: If not specified, auto-increments the current version -2. **Generates changelog**: Uses `git diff` to find changes between `main` and the last release tag -3. **Updates package versions**: Bumps `version` in all `package.json` files to the same 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` +For the **one package you selected** (and only that package): + +1. **Determines the next version**: uses the `version` input, or auto-increments the selected package's current version +2. **Generates a changelog**: lists commits since that package's last release tag, scoped to the package's directory (`git log ..HEAD -- `), so only commits that touched that package are included +3. **Bumps the version**: updates `version` in that package's own `package.json` +4. **Updates that package's changelog**: core and azuremanaged write the repo-root `CHANGELOG.md`; `durable-functions` writes `packages/azure-functions-durable/CHANGELOG.md` +5. **Creates a release branch and a package-scoped tag**: tag `` and branch `release/`, where the prefix is `v` (core), `azuremanaged-v`, or `durable-functions-v` +6. **For `durable-functions` only**: verifies the exact-pinned `@microsoft/durabletask-js` version is already published on public npm, and fails the run if it is not (guards the uninstallable-dependency case) ### 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`: +The workflow summary includes a **Create PR** link and the exact `npm publish` command to run later. You must **manually create a pull request** from the release branch to `main`. The branch is `release/` where `` is the package-scoped tag — e.g. `release/durable-functions-v4.0.0-beta.1`, `release/azuremanaged-v0.3.0`, or `release/v0.4.0`: -1. Go to the workflow run summary and click the **Create PR** link, or navigate to: `https://github.com/microsoft/durabletask-js/compare/main...release/vX.Y.Z` -2. Set the PR title to `Release vX.Y.Z` -3. Review the version bumps and changelog updates +1. Go to the workflow run summary and click the **Create PR** link +2. Set the PR title to `Release @` (e.g. `Release durable-functions@4.0.0-beta.1`) +3. Review the version bump and changelog update — only the released package should change 4. Merge the PR after CI passes After the PR is merged, follow the **Publishing** steps below to build and publish. @@ -80,37 +91,50 @@ Trigger the official build pipeline on the release tag/commit/branch to produce **Pipeline**: [durabletask-js.official](https://dev.azure.com/azfunc/internal/_build?definitionId=1012&_a=summary) 1. Click **Run pipeline** -2. Select the release branch or tag (e.g., `release/vX.Y.Z` or tag `vX.Y.Z`) +2. Select the release branch or tag created by the release — the package-scoped `release/` branch or `` tag (e.g., `release/v0.4.0` or tag `v0.4.0`) 3. Wait for it to complete -4. Verify the `drop` artifact contains correctly versioned `.tgz` files: +4. Verify the `drop` artifact contains the correctly versioned `.tgz` files (the official build packs all three packages): - `buildoutputs/durabletask-js/microsoft-durabletask-js-X.Y.Z.tgz` - `buildoutputs/durabletask-js-azuremanaged/microsoft-durabletask-js-azuremanaged-X.Y.Z.tgz` + - `buildoutputs/azure-functions-durable/durable-functions-X.Y.Z.tgz` ### Step 3: Run the Release Pipeline -Trigger the release pipeline to publish the signed packages to npm via ESRP. The pipeline has separate release jobs for each package: +Trigger the release pipeline to publish the signed packages to npm via ESRP. **This is the sanctioned publish path.** `eng/ci/release.yml` is a 1ES multi-stage pipeline with **one stage per package**, each publishing that package's `.tgz` from its own `buildoutputs/` folder via the `EsrpRelease@9` task: -**Pipeline**: eng/ci/release.yml (link to update soon) +- `release_durabletask_js` — core `@microsoft/durabletask-js`; has no stage dependency. +- `release_durabletask_js_azuremanaged` — `@microsoft/durabletask-js-azuremanaged`; `dependsOn: release_durabletask_js`. +- `release_durable_functions` — `durable-functions`; `dependsOn: release_durabletask_js`. + +**Pipeline**: `eng/ci/release.yml` (ADO pipeline link: TBD) 1. Click **Run pipeline** 2. Select the build from Step 2 as the source pipeline artifact -3. Approve the ESRP release when prompted (one approval per package) +3. **Choose which stage(s) to run.** Stages are individually selectable at queue time, so you can release one package at a time. Both dependent stages `dependsOn` the core stage only — they are siblings, not chained to each other. +4. **Respect the ordering rule:** release the core stage before (or in the same run as) `durable-functions`, because compat exact-pins core. Each dependent stage carries an explicit condition — `in(dependencies.release_durabletask_js.result, 'Succeeded', 'SucceededWithIssues', 'Skipped')` — so it still runs when the core stage is **de-selected (Skipped)** at queue time (letting you release that package on its own once core is already on npm), but it will **not** run if the core stage actually **Failed** or was **Canceled**, which preserves the publish ordering. +5. Approve the ESRP release when prompted — each selected stage runs its own `EsrpRelease@9` task with its own approver, so expect one ESRP approval per package. + +> **Open question (B11):** it is not yet confirmed whether the ESRP release task can set the npm **dist-tag** (e.g. publish a prerelease under `preview` rather than moving `latest`). This must be confirmed with the ESRP / 1ES pipeline owners before publishing a prerelease through ESRP — do **not** assume ESRP applies a dist-tag. The `--tag` guidance in *Quick Reference: npm Dist Tags* applies to a manual `npm publish`. ### Step 4: Verify npm Publish ```bash npm view @microsoft/durabletask-js versions npm view @microsoft/durabletask-js-azuremanaged versions +npm view durable-functions versions + +# For a prerelease, also confirm which dist-tag it landed under (and that `latest` did not move): +npm view durable-functions dist-tags ``` ### Step 5: Create a GitHub Release Go to [GitHub Releases](https://github.com/microsoft/durabletask-js/releases) and create a new release: -- **Tag**: `vX.Y.Z` -- **Title**: `vX.Y.Z` -- **Description**: Copy the relevant section from `CHANGELOG.md` -- **Pre-release**: Check this box for alpha/beta/rc releases +- **Tag**: the package-scoped tag created by the release — e.g. `durable-functions-v4.0.0-beta.1`, `azuremanaged-v0.3.0`, or `v0.4.0` +- **Title**: the same tag, or `@` +- **Description**: copy the relevant section from that package's changelog (`CHANGELOG.md` for core / azuremanaged, `packages/azure-functions-durable/CHANGELOG.md` for `durable-functions`) +- **Pre-release**: check this box for alpha/beta/rc/preview releases ## Manual Release Process (Alternative) @@ -120,26 +144,37 @@ If you prefer to prepare the release manually, follow these steps. Review merged PRs since the last release tag to understand what's shipping. Choose an appropriate version number following semver. -### 2. Update Package Versions +### 2. Update the Package Version -Bump the `"version"` field in **both** package.json files to the **same version**: +Bump the `"version"` field in **only the package you are releasing**: ```bash -# Edit packages/durabletask-js/package.json → "version": "X.Y.Z" -# Edit packages/durabletask-js-azuremanaged/package.json → "version": "X.Y.Z" +# core: packages/durabletask-js/package.json → "version": "X.Y.Z" +# azuremanaged: packages/durabletask-js-azuremanaged/package.json → "version": "X.Y.Z" +# compat: packages/azure-functions-durable/package.json → "version": "X.Y.Z" ``` -Also update the peer dependency in `packages/durabletask-js-azuremanaged/package.json`: +Then update the affected cross-package dependency **for the package you are releasing**: -```jsonc -"peerDependencies": { - "@microsoft/durabletask-js": ">=X.Y.Z" // ← same version -} -``` +- **`@microsoft/durabletask-js-azuremanaged`** declares a peer **floor** on core. Only raise it if this release actually requires a newer core — it does not have to equal the release version: + + ```jsonc + "peerDependencies": { + "@microsoft/durabletask-js": ">=X.Y.Z" + } + ``` + +- **`durable-functions`** pins core to an **exact** version. This is the single most consequential dependency edit: if core is being bumped, set this pin to the exact core version you will publish, and publish that core version **first** (otherwise the published compat package is uninstallable): -### 3. Update CHANGELOG.md + ```jsonc + "dependencies": { + "@microsoft/durabletask-js": "X.Y.Z" // exact — no range; must be published on npm first + } + ``` -Move items from the `## Upcoming` section into a new versioned section. Follow the existing format: +### 3. Update the Changelog + +Move items from the `## Upcoming` section of the package's changelog into a new versioned section, following the existing format. Use the repo-root `CHANGELOG.md` for `@microsoft/durabletask-js` and `@microsoft/durabletask-js-azuremanaged`, or `packages/azure-functions-durable/CHANGELOG.md` for `durable-functions`: ```markdown ## Upcoming @@ -170,23 +205,26 @@ npm run lint ### 5. Create a Release PR -Create a branch (e.g., `release/vX.Y.Z`), commit the version bumps and changelog update, and open a PR against `main`. The PR title should follow: `Release vX.Y.Z`. +Create a release branch named `release/`, where `` is the package-scoped tag `` (prefix `v`, `azuremanaged-v`, or `durable-functions-v`) — e.g. `release/durable-functions-v4.0.0-beta.1`. Commit the version bump and changelog update for that one package, and open a PR against `main`. The PR title should follow: `Release @`. ### 6. Merge and Tag -After the PR is approved and merged to `main`: +After the PR is approved and merged to `main`, create the package-scoped tag (``): ```bash git checkout main git pull -git tag vX.Y.Z -git push origin vX.Y.Z +# e.g. durable-functions-v4.0.0-beta.1, azuremanaged-v0.3.0, or v0.4.0 +git tag +git push origin ``` Then follow the **Publishing** steps above (Steps 1-5). ## Quick Reference: npm Dist Tags +The general semver → dist-tag convention: + | Tag | When to use | |---|---| | `--tag alpha` | For `X.Y.Z-alpha.N` versions | @@ -194,6 +232,12 @@ Then follow the **Publishing** steps above (Steps 1-5). | `--tag next` | For release candidates (`X.Y.Z-rc.N`) | | *(no tag / `latest`)* | For stable GA releases only | +**Current tooling and package-specific rules take precedence where they differ from the table above:** + +- The **Prepare Release** workflow publishes *every* prerelease (any version containing `-`) under the single **`preview`** dist-tag — its run summary suggests `npm publish --registry https://registry.npmjs.org/ --tag preview` — so a prerelease never moves `latest`. +- **`durable-functions` 4.x previews ship under `preview`.** Its documented install line is `npm install durable-functions@preview` (see `packages/azure-functions-durable/CHANGELOG.md`); publish these with `npm publish --tag preview`. +- Whether the **ESRP** pipeline (Step 3) can set a dist-tag is an open question (B11); the `--tag` guidance here applies to a manual `npm publish`. + ## Rolling Back a Release If a release has critical issues: