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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions .github/workflows/prepare-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand All @@ -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
27 changes: 19 additions & 8 deletions doc/release_process.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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`.
Expand All @@ -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@<pinned-version> --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<major>` (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

Expand Down Expand Up @@ -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<major>` (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:
Expand Down Expand Up @@ -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<major>` (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

Expand Down
2 changes: 1 addition & 1 deletion eng/ci/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion packages/azure-functions-durable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions packages/azure-functions-durable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
Comment on lines +1 to +2

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();
}
});
});
Loading