From 95a761122f18602736c1a0e9e9fb7b9764b0c7da Mon Sep 17 00:00:00 2001 From: wangbill Date: Mon, 3 Aug 2026 08:21:00 -0700 Subject: [PATCH] fix(release): safeguard durable-functions preview channel Use ESRP Release v12 for the durable-functions job, preserve preview-v across prerelease preparation, remove it for GA, and document and test the Functions-only release flow. --- .github/workflows/prepare-release.yaml | 21 +++++++++++++-- doc/release_process.md | 27 +++++++++++++------ eng/ci/release.yml | 2 +- packages/azure-functions-durable/CHANGELOG.md | 2 +- packages/azure-functions-durable/README.md | 10 +++++++ .../test/unit/release-metadata.spec.ts | 26 ++++++++++++++++++ 6 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 packages/azure-functions-durable/test/unit/release-metadata.spec.ts diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 2df2802..52a9e44 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -197,6 +197,17 @@ jobs: const packagePath = `${process.env.PKG_DIR}/package.json`; const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8")); pkg.version = process.env.NEW_VERSION; + if (process.env.PKG_DIR === "packages/azure-functions-durable") { + if (process.env.NEW_VERSION.includes("-")) { + const majorVersion = process.env.NEW_VERSION.split(".")[0]; + pkg.publishConfig = { ...pkg.publishConfig, tag: `preview-v${majorVersion}` }; + } else if (pkg.publishConfig) { + delete pkg.publishConfig.tag; + if (Object.keys(pkg.publishConfig).length === 0) { + delete pkg.publishConfig; + } + } + } fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + "\n"); const lockPath = "package-lock.json"; @@ -292,7 +303,13 @@ 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 --registry https://registry.npmjs.org/ --tag preview" + if [[ "$NPM_NAME" == "durable-functions" ]]; then + MAJOR_VERSION="${NEW_VERSION%%.*}" + PUBLISH_TAG="preview-v${MAJOR_VERSION}" + else + PUBLISH_TAG="preview" + fi + PUBLISH_CMD="npm publish --registry https://registry.npmjs.org/ --tag ${PUBLISH_TAG}" else PUBLISH_CMD="npm publish --registry https://registry.npmjs.org/" fi @@ -319,5 +336,5 @@ jobs: 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 + echo "> :warning: This is a prerelease. Omitting \`--tag ${PUBLISH_TAG}\` would move the \`latest\` dist-tag to a prerelease version." >> $GITHUB_STEP_SUMMARY fi diff --git a/doc/release_process.md b/doc/release_process.md index 4bccad1..cb7d869 100644 --- a/doc/release_process.md +++ b/doc/release_process.md @@ -36,7 +36,7 @@ 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` | | Stable | `0.1.0` | -The scheme above describes the version **string**. Mapping a version to an **npm dist-tag** is a separate concern and does **not** follow a per-stage `beta`/`next` convention: the **Prepare Release** workflow's emitted **manual fallback** `npm publish` command tags every prerelease `preview` and omits `--tag` for a stable GA (so only a GA moves `latest`). Publishing a prerelease through the sanctioned ESRP path is **blocked pending B11** until owners confirm whether the ESRP task can set that tag. See *Quick Reference: npm Dist Tags* below for the authoritative rule. +The scheme above describes the version **string**. Mapping a version to an **npm dist-tag** is a separate concern and does **not** follow a per-stage `beta`/`next` convention. For `durable-functions`, each prerelease uses a major-specific channel such as `preview-v4`; the package's `publishConfig.tag` carries that channel through the signed tarball to ESRP. A stable `durable-functions` release removes the preview tag so the publish moves `latest`. The other packages retain their existing `preview` manual-fallback convention. See *Quick Reference: npm Dist Tags* below for the authoritative rules. ## Automated Release Preparation (Recommended) @@ -107,7 +107,7 @@ Never publish from the release branch — it exists only to carry the version/ch ### Step 3: Run the Release Pipeline -Trigger the release pipeline to publish one signed package to npm via ESRP. **This is the sanctioned publish path.** `eng/ci/release.yml` consumes the `durabletask-js.official` build artifact from **`main`** (its pipeline resource is pinned to `branch: main`), so its source is the `main` official build selected in Step 2 — not the release branch. The pipeline's `package` runtime parameter controls which one of these release stages is inserted into the compiled 1ES plan. It defaults to `durabletask-js`, so verify or change it before queueing: a wrong or unchanged default compiles the wrong package stage. ESRP approval remains the final safety gate before publishing. +Trigger the release pipeline to publish one signed package to npm via ESRP. **This is the sanctioned publish path.** `eng/ci/release.yml` consumes the `durabletask-js.official` build artifact from **`main`** (its pipeline resource is pinned to `branch: main`), so its source is the `main` official build selected in Step 2 — not the release branch. The pipeline's `package` runtime parameter controls which one of these release stages is inserted into the compiled 1ES plan. It defaults to `durabletask-js`, so verify or change it before queueing: a wrong or unchanged default compiles the wrong package stage. The `durable-functions` stage uses ESRP Release v12, which is auto-approved and publishes without a manual ESRP prompt, so complete every artifact and parameter check before queueing. - `durabletask-js` inserts `release_durabletask_js` for core `@microsoft/durabletask-js`. - `durabletask-js-azuremanaged` inserts `release_durabletask_js_azuremanaged` for `@microsoft/durabletask-js-azuremanaged`. @@ -118,13 +118,13 @@ Trigger the release pipeline to publish one signed package to npm via ESRP. **Th 1. Click **Run pipeline** 2. Select the **`main` official build from Step 2** (the one containing the release commit) as the source pipeline artifact 3. Verify or change the **`package`** parameter to the one package to publish. Exactly one release stage is compiled and exactly one package is published; do not use queue-time stage selection to choose packages. -4. Approve the ESRP release when prompted. +4. For `durable-functions`, inspect the selected `.tgz` before starting the run. Confirm its `package/package.json` contains the intended version and `publishConfig.tag`; then queue the pipeline. Do not expect a later ESRP approval prompt. This compile-time selection eliminates the failure demonstrated by run **294746**: de-selecting the core stage at queue time did not expose its result to the Azure Managed dependency condition as the expected literal `Skipped`, so the selected package's stage was skipped too. The selected package now has no cross-stage dependency because the other package stages do not exist in that run's compiled plan. **Two runs for core + compat.** The compat **Prepare Release** run guards on core already being published to **public npm** — it runs `npm view @microsoft/durabletask-js@ --registry https://registry.npmjs.org/` and fails if that exact version is absent (`durable-functions` exact-pins core). Publish and verify core first; only then prepare `durable-functions` and publish it in a later release-pipeline run. (`azuremanaged` depends on core only through a peer floor that is already satisfied and has no ordering constraint.) -> **Open question (B11) — hard blocker for ESRP prereleases:** 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`). Publishing a prerelease such as `durable-functions@4.0.0-beta.1` through ESRP is **blocked** until the ESRP / 1ES pipeline owners confirm how to apply the `preview` tag — do **not** assume ESRP applies a dist-tag. The `--tag` guidance in *Quick Reference: npm Dist Tags* applies to a manual `npm publish`. +> **`durable-functions` prereleases:** the package manifest sets `publishConfig.tag` to `preview-v` (for example, `preview-v4`). ESRP Release v12 infers the npm dist-tag from that metadata inside the signed `.tgz`. Before queueing, inspect the tarball's `package/package.json` and confirm the expected version and `publishConfig.tag`; after publishing, verify that `preview-v4` moved and `latest` did not. This rule applies only to `durable-functions`. ESRP prerelease tagging for the two `@microsoft` packages remains unconfirmed; do not publish their prereleases through ESRP until their tag behavior is defined. ### Step 4: Verify npm Publish @@ -164,6 +164,17 @@ Bump the `"version"` field in **only the package you are releasing**: # compat: packages/azure-functions-durable/package.json → "version": "X.Y.Z" ``` +For `durable-functions`, keep the npm channel aligned with the version: + +- For a prerelease, set `publishConfig.tag` to `preview-v` (for example, `preview-v4` for `4.0.0-beta.1`). +- For a stable GA release, remove `publishConfig.tag`; if `publishConfig` is then empty, remove the object. + +```jsonc +"publishConfig": { + "tag": "preview-v4" +} +``` + Then update the affected cross-package dependency **for the package you are releasing**: - **`@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: @@ -235,11 +246,11 @@ Then follow the **Publishing** steps above (Steps 1-5). ## Quick Reference: npm Dist Tags -npm **dist-tags** are separate from the git tags this repo pushes (`v...`, `azuremanaged-v...`, `durable-functions-v...`); a git tag only names a release commit and never moves an npm dist-tag. This repo uses one simple rule, not the per-stage `alpha`/`beta`/`next` convention: +npm **dist-tags** are separate from the git tags this repo pushes (`v...`, `azuremanaged-v...`, `durable-functions-v...`); a git tag only names a release commit and never moves an npm dist-tag. This repo does not use the per-stage `alpha`/`beta`/`next` convention: -- **Prerelease** — any version containing `-` (e.g. `0.4.0-beta.1`, `4.0.0-beta.1`). The **Prepare Release** workflow emits `npm publish --registry https://registry.npmjs.org/ --tag preview` in its run summary as a **manual fallback** to run after the release PR merges. When using this manual fallback, publish every prerelease under the single `preview` dist-tag, so it never moves `latest`. `durable-functions` 4.x previews install with `npm install durable-functions@preview` (see `packages/azure-functions-durable/README.md`). -- **Stable GA** — no `-`. The emitted command omits `--tag`, so the publish moves `latest`. -- **ESRP path (sanctioned, Step 3):** whether the ESRP release task can set an npm dist-tag is an **open question (B11)** and a **hard blocker** for publishing a prerelease such as `durable-functions@4.0.0-beta.1` through ESRP — confirm with the ESRP / 1ES owners how to apply the `preview` tag before publishing any prerelease via ESRP. Do **not** assume ESRP applies a dist-tag; the `--tag preview` guidance above is for a manual `npm publish`. +- **`durable-functions` prerelease** — any `durable-functions` version containing `-` uses `preview-v` (for example, `4.0.0-beta.1` uses `preview-v4`). The Prepare Release workflow updates `publishConfig.tag`, and ESRP infers the tag from the signed package metadata. The manual fallback emits the matching `--tag preview-v4`. Install with `npm install durable-functions@preview-v4`. +- **Other-package prerelease** — the manual fallback uses the generic `preview` tag. Their ESRP prerelease behavior remains undefined and blocked as described above. +- **Stable GA** — no `-`. For `durable-functions`, Prepare Release removes the major-specific preview tag. The emitted manual command omits `--tag`, so the publish moves `latest`. ## Rolling Back a Release diff --git a/eng/ci/release.yml b/eng/ci/release.yml index 206d4c4..1b7c21a 100644 --- a/eng/ci/release.yml +++ b/eng/ci/release.yml @@ -117,7 +117,7 @@ extends: targetPath: $(System.DefaultWorkingDirectory)/drop steps: - - task: SFP.release-tasks.custom-build-release-task.EsrpRelease@9 + - task: SFP.release-tasks.custom-build-release-task.EsrpRelease@12 displayName: 'ESRP Release durable-functions' inputs: connectedservicename: 'dtfx-internal-esrp-prod' diff --git a/packages/azure-functions-durable/CHANGELOG.md b/packages/azure-functions-durable/CHANGELOG.md index f2fc94e..ce83bb0 100644 --- a/packages/azure-functions-durable/CHANGELOG.md +++ b/packages/azure-functions-durable/CHANGELOG.md @@ -4,11 +4,11 @@ ### Fixes - ## v4.0.0-beta.1 (2026-07-31) ### Changes +- Publish v4 prereleases through the major-specific `preview-v4` npm channel without moving `latest` from v3. - docs(release): changelog, README, and copilot-instructions updates ([#339](https://github.com/microsoft/durabletask-js/pull/339)) - feat(durable-functions): restore worker-side callHttp ([#318](https://github.com/microsoft/durabletask-js/issues/318)) ([#333](https://github.com/microsoft/durabletask-js/pull/333)) - fix(durable-functions): route sync single-arg orchestrators to core-native ([#321](https://github.com/microsoft/durabletask-js/pull/321)) (#323) diff --git a/packages/azure-functions-durable/README.md b/packages/azure-functions-durable/README.md index 801cf01..d0a18fd 100644 --- a/packages/azure-functions-durable/README.md +++ b/packages/azure-functions-durable/README.md @@ -8,6 +8,16 @@ Write [Azure Durable Functions](https://learn.microsoft.com/azure/azure-function This package supersedes the legacy [`durable-functions`](https://github.com/Azure/azure-functions-durable-js) package. New and existing (classic v3) orchestration, activity, and entity code all run on the same gRPC engine. +## Install the v4 preview + +Install the latest v4 prerelease from its major-specific npm channel: + +```bash +npm install durable-functions@preview-v4 +``` + +The `preview-v4` tag advances across v4 beta and release-candidate builds. Pin an exact version such as `durable-functions@4.0.0-beta.1` for reproducible installs. Until v4 reaches GA, an unqualified `npm install durable-functions` continues to install the stable v3 release from npm's `latest` tag. + ## Why it is needed - **One gRPC protocol.** Durable work items flow between the Functions host and your app over a single gRPC channel instead of the legacy out-of-proc HTTP protocol, keeping the JavaScript provider aligned with the .NET, Python, and Java Durable providers. diff --git a/packages/azure-functions-durable/test/unit/release-metadata.spec.ts b/packages/azure-functions-durable/test/unit/release-metadata.spec.ts new file mode 100644 index 0000000..7e34eb3 --- /dev/null +++ b/packages/azure-functions-durable/test/unit/release-metadata.spec.ts @@ -0,0 +1,26 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +interface PackageMetadata { + version: string; + publishConfig?: { + tag?: string; + }; +} + +describe("npm release metadata", () => { + const packageMetadata = JSON.parse( + readFileSync(join(__dirname, "..", "..", "package.json"), "utf8"), + ) as PackageMetadata; + + it("uses a major-specific preview channel only for prerelease versions", () => { + const versionMatch = /^(\d+)\.\d+\.\d+(-.+)?$/.exec(packageMetadata.version); + + expect(versionMatch).not.toBeNull(); + if (versionMatch?.[2]) { + expect(packageMetadata.publishConfig?.tag).toBe(`preview-v${versionMatch[1]}`); + } else { + expect(packageMetadata.publishConfig?.tag).toBeUndefined(); + } + }); +}); \ No newline at end of file