diff --git a/doc/release_process.md b/doc/release_process.md index 81535285..33030087 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 @@ -23,40 +29,58 @@ We follow [semver](https://semver.org/) with optional pre-release tags: X.Y.Z-alpha.N → X.Y.Z-beta.N → X.Y.Z-rc.N → X.Y.Z (stable) ``` -| Version Type | Example | npm Tag | -|---|---|---| -| Alpha | `0.1.0-alpha.1` | `--tag alpha` | -| Beta | `0.1.0-beta.1` | `--tag beta` | -| Release Candidate | `0.1.0-rc.1` | `--tag next` | -| Stable | `0.1.0` | *(no tag, becomes `latest`)* | +| Version Type | Example | +|---|---| +| Alpha | `0.1.0-alpha.1` | +| Beta | `0.1.0-beta.1` | +| 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. ## Automated Release Preparation (Recommended) Use the **Prepare Release** GitHub Action to automate the release preparation process. +### One-Time Preflight: Seed the `azuremanaged` Release Tag + +The changelog step lists commits since the released package's **last package-scoped tag** (`git log ..HEAD -- `); if no tag with that package's prefix exists, it falls back to the repo's initial commit and would dump the entire history into the changelog. `@microsoft/durabletask-js-azuremanaged` uses the `azuremanaged-v` prefix and has no such tag yet — the last lockstep release is the unprefixed `v0.3.0`. **Before the first independent `azuremanaged` release**, seed its tag so changelog generation starts at `v0.3.0` instead of the repo root: + +```bash +git tag azuremanaged-v0.3.0 v0.3.0 +git push origin azuremanaged-v0.3.0 +``` + +Do this once (not as part of a normal release run). + ### Running the Workflow 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 only **prepares** a release — it bumps the version, generates the changelog, and creates the release branch and package-scoped tag. It does **not** publish to npm. Its run summary includes a **Create PR** link and an `npm publish` command; that command is a **manual fallback** for maintainers, and the sanctioned publish path is the ADO official build + ESRP release pipeline (see **Publishing** below). -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 +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 +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. @@ -75,42 +99,58 @@ Run it on the `main` branch (which now contains the release commit). ### Step 2: Run the Official Build Pipeline -Trigger the official build pipeline on the release tag/commit/branch to produce signed `.tgz` artifacts: +Trigger the official build pipeline to produce signed `.tgz` artifacts. The official build (`eng/ci/official-build.yml`) triggers on and builds **`main`** — it has no branch/tag selector and never builds the short-lived release branch. Use (or pick the automatic run of) the official build whose source commit **includes the merged release commit(s)**: **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`) -3. Wait for it to complete -4. Verify the `drop` artifact contains correctly versioned `.tgz` files: +1. Click **Run pipeline** on `main` (or locate the `main` run that already contains the release commit) +2. Wait for it to complete +3. A single `main` build packs **all three** packages into one `drop` artifact; which package(s) actually publish is decided later by stage selection in the release pipeline (Step 3). Verify `drop` contains the correctly versioned `.tgz` files: - `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` + +Never publish from the release branch — it exists only to carry the version/changelog PR into `main`. ### 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` 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. It 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: + +- `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 (link to update soon) +**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) +2. Select the **`main` official build from Step 2** (the one containing the release commit) as the source pipeline artifact +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. + +**Two waves 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). So core and compat cannot go out in a single pass: publish core to public npm first, then run compat **Prepare Release** and its release stage. Treat core and `durable-functions` as two separate release waves. (`azuremanaged` depends on core only through a peer floor and has no such 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`. ### 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,43 +160,50 @@ 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: -### 3. Update CHANGELOG.md + ```jsonc + "peerDependencies": { + "@microsoft/durabletask-js": ">=X.Y.Z" + } + ``` -Move items from the `## Upcoming` section into a new versioned section. Follow the existing format: +- **`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): -```markdown -## Upcoming + ```jsonc + "dependencies": { + "@microsoft/durabletask-js": "X.Y.Z" // exact — no range; must be published on npm first + } + ``` -### New - +### 3. Update the Changelog + +Move items from the `## Upcoming` section of the package's changelog into a new versioned section. The changelog is a **generated** list of the released package's commit messages / PR links — not a place for hand-authored breaking-change narrative; detailed preview and migration guidance lives in `packages/azure-functions-durable/README.md` (for `durable-functions`), not the changelog. To reproduce the tooling manually, promote any curated `## Upcoming` notes into the new section and add one `### Changes` entry per commit, mirroring `git log ..HEAD -- ` (each commit subject with its `(#NN)` linked). 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`: -## vX.Y.Z +```markdown +## Upcoming ### New -- Feature A ([#NN](https://github.com/microsoft/durabletask-js/pull/NN)) ### Fixes -- Fix B ([#NN](https://github.com/microsoft/durabletask-js/pull/NN)) -### Breaking Changes -- Breaking change C ([#NN](https://github.com/microsoft/durabletask-js/pull/NN)) +## vX.Y.Z (YYYY-MM-DD) + +### Changes +- Some change ([#NN](https://github.com/microsoft/durabletask-js/pull/NN)) +- Another change ([#NN](https://github.com/microsoft/durabletask-js/pull/NN)) ``` ### 4. Verify Build & Tests Pass Locally @@ -170,29 +217,29 @@ 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 -| Tag | When to use | -|---|---| -| `--tag alpha` | For `X.Y.Z-alpha.N` versions | -| `--tag beta` | For `X.Y.Z-beta.N` versions | -| `--tag next` | For release candidates (`X.Y.Z-rc.N`) | -| *(no tag / `latest`)* | For stable GA releases only | +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: + +- **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`. ## Rolling Back a Release