npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@microsoft/esrp-npm-release

v0.1.6

Published

ESRP npm release helper

Readme

@microsoft/esrp-npm-release

Tool for teams within Microsoft who would like to use ESRP to release npm packages in dependency-topological order: always publishing a package's internal dependencies before publishing the package itself. Especially in larger repos with many package consumers, this ordering is critical to ensure that if publishing fails partway through, there are never published packages with dangling references to nonexistent versions (which would cause installation failures in consuming repos).

When not to use this tool: This tool requires extra setup (below) and isn't owned by the ESRP team. For single packages or smaller monorepos where your team hasn't hit issues with publish ordering, you should use the official EsrpRelease task for simplicity. Or if you care about ordering but only have a few packages, it might be reasonable to manually group them and run the EsrpRelease task multiple times.

Note that the pipeline setup (including internal feed setup) described below is also applicable to repos using the EsrpRelease task; just omit any "staging" resources or steps, and you don't need to use a special folder layout for packed packages (though the special layout should work with the task).

Contents

Overview

The tool is a bundled CLI (dist/index.mjs) that runs during a release job and publishes npm packages through the ESRP release API in the proper order. Since release jobs can't directly download sources or install dependencies, the CLI bundle must be uploaded as a pipeline artifact from the build stage, then consumed and run by the release stage.

The tool itself doesn't calculate dependency order. Instead, it relies on receiving the packages already grouped into dependency tree layers: numbered subfolders where the packages in each layer depend only on packages in earlier layers. (beachball publish --pack-to-path produces this layout, but you could extend other change/release management tools to do the same.) The tool releases the layers in numeric order, so every internal dependency a package references is already published before that package's own layer.

For each layer, the tool:

  1. Zips the layer's package tarballs.
  2. Temporarily uploads the zip to a "staging" blob storage account, since the ESRP release API only accepts blob storage URLs, not files uploaded as request bodies. (See compliance notes below.)
  3. Calls the ESRP release API to publish the layer, waiting for it to complete before moving on to the next layer.
  4. Deletes the staged zip.

The tool retries transient initialization failures and failed layer releases up to three times. Permanent or unrecognized errors, including the ESRP status failDoNotRetry, fail immediately. It's also safe to manually re-run the whole stage.

The tool relies on the following inputs and resources:

  • Packed packages: Output folder from beachball publish --pack-to-path <path> or in the same format.
    • ⚠️ If using beachball, you should upgrade to a v3 prerelease (beachball@next) to take advantage of certain new features, including better registry config handling. See migration guide.
  • ESRP resources following standard onboarding procedures:
    • ESRP-onboarded identity in a production tenant (need client ID and tenant ID)
    • Production tenant key vault storing certificates
    • Production tenant managed identity with access to the key vault, and a corresponding ADO service connection
  • Staging resources specific to this tool (see below for setup details):
    • Azure Blob Storage account in your team's subscription (usually in the corp tenant) to temporarily host zips of packages
    • Managed identity with the right RBAC roles on the storage account (used to create user delegation keys for blob access)
    • ADO Azure Resource Manager service connection using the managed identity

Credit for the original staging and API integration approach goes to the VS Code team.

Tool inputs

The tool is configured using environment variables. All variables are required unless noted otherwise.

See setup steps in later sections and sample pipelines at the bottom of this document for details of setup and usage.

Packages and release info

⚠️ If you invoke the tool multiple times in the same pipeline to publish different sets of packages, each invocation must use a different ESRP_PRODUCT_NAME or ESRP_NPM_TAG. Otherwise the invocations share the same retry state and later ones will incorrectly assume their packages were already published.

| Variable | Description | | -------- | ----------- | | PACKED_PACKAGES_PATH | Path to the packed packages directory. A flat directory of .tgz files is also accepted and will be released as a single layer. | | ESRP_PRODUCT_NAME | Friendly product name for the release. This is not used as a published package name, just in the ESRP Release UI (and by the tool as a state key). | | ESRP_NPM_TAG | Optional. npm dist-tag for the published packages. Defaults to latest or uses each package's publishConfig. |

Contact info

All fields accepting email(s) can be comma-, semicolon-, newline-, or space-separated.

| Variable | Description | | -------- | ----------- | | ESRP_OWNERS | Individual owner email(s). DL/SG not supported. If ESRP_CREATED_BY is not specified, it defaults to the first email. | | ESRP_APPROVERS | Individual approver email(s) (all auto-approved). DL/SG not supported. | | ESRP_CREATED_BY | Optional. Email of the user creating the release. Defaults to the first ESRP_OWNERS value. DL/SG not supported. | | ESRP_DRI_EMAIL | Optional. DRI email(s) for the team creating the release. This does support DL/SG. |

ESRP resources

ESRP resources are set up per their guides. Correspondence with official EsrpRelease task input names is given to assist with cross-referencing their docs.

| Variable | Description | | -------- | ----------- | | ESRP_TENANT_ID | Production tenant ID for your ESRP app registration or managed identity. (EsrpRelease task: domaintenantid) | | ESRP_CLIENT_ID | Client ID for your ESRP app registration or ESRP-allowlisted managed identity. (EsrpRelease task: clientid) | | ESRP_AUTH_CERT | Base64-encoded PFX certificate for authenticating to ESRP AAD. Set exactly one of this or ESRP_ID_TOKEN. | | ESRP_ID_TOKEN | Federated ID token for authenticating as an ESRP-allowlisted managed identity. Set exactly one of this or ESRP_AUTH_CERT. | | ESRP_REQUEST_SIGNING_CERT | Base64-encoded PFX certificate for signing JWS release requests. Required for both authentication methods. |

The request signing certificate and optional auth certificate are typically retrieved by a prior AzureKeyVault task step (as shown in the example pipeline):

- task: AzureKeyVault@2
  displayName: Get ESRP certificates from Key Vault
  inputs:
    # Production tenant service connection name (EsrpRelease task: "connectedservicename")
    azureSubscription: <ESRP service connection name>
    # Key vault name (EsrpRelease task: "keyvaultname")
    KeyVaultName: <key vault name>
    # Cert content is loaded into secret variables (EsrpRelease task: "authcertname" and "signcertname").
    # Omit the auth cert when using managed identity authentication.
    SecretsFilter: <auth cert name>,<request signing cert name>

For managed identity authentication, obtain ESRP_ID_TOKEN from the ESRP service connection:

- task: AzureCLI@2
  displayName: Get credentials for ESRP
  inputs:
    azureSubscription: <ESRP service connection name>
    scriptType: bash
    scriptLocation: inlineScript
    addSpnToEnvironment: true
    inlineScript: |
      echo "##vso[task.setvariable variable=ESRP_ID_TOKEN;issecret=true]$idToken"

Staging resources

See staging resource setup.

| Variable | Description | | -------- | ----------- | | STAGING_STORAGE_ACCOUNT_NAME | Name of the staging storage account. | | STAGING_CLIENT_ID | Client ID used for storage account access. | | STAGING_TENANT_ID | Tenant ID used for storage account access. | | STAGING_ID_TOKEN | Federated ID token used for storage account access. |

Aside from STAGING_STORAGE_ACCOUNT_NAME, these are typically retrieved by a prior AzureCLI task step (as shown in the example pipeline):

- task: AzureCLI@2
  displayName: Get credentials for staging blob storage
  inputs:
    azureSubscription: <staging service connection name>
    scriptType: bash
    scriptLocation: inlineScript
    addSpnToEnvironment: true
    inlineScript: |
      echo "##vso[task.setvariable variable=STAGING_TENANT_ID]$tenantId"
      echo "##vso[task.setvariable variable=STAGING_CLIENT_ID]$servicePrincipalId"
      echo "##vso[task.setvariable variable=STAGING_ID_TOKEN;issecret=true]$idToken"

Packed packages format

Running beachball publish --pack-to-path <path> produces a directory with this shape. If there are 10+ layers, the directories must be zero-padded (e.g. 01, 02, ..., 10, etc.) to ensure correct lexical ordering.

<path>/
├── 01/                    # Layer 1: packages with no internal dependencies
│   ├── pkg-a-1.0.0.tgz
│   └── pkg-b-2.0.0.tgz
├── 02/                    # Layer 2: packages that depend only on layer 1
│   └── pkg-c-3.0.0.tgz
├── 03/
│   └── ...
...

Each numbered directory is a dependency-topological layer: the packages in layer 1 have no internal dependencies, or none within the set of packages being published. Packages in layer N may only depend on packages in layers 1..N-1. The tool releases layers in numeric order, so that by the time a layer is published, every internal dependency version it references is already on the registry.

More detailed example of the packed packages format

Staging resource setup

Follow the steps below to create the Azure resources used by this tool: resource group, storage account, user-assigned managed identity, RBAC assignments, and lifecycle management policy. The Azure subscription can be in either the corp tenant or a production tenant.

The commands below reuse the same subscription, resource group, storage account, and managed identity names, so set them once in your shell to keep the snippets copy-pasteable:

SUBSCRIPTION="<sub>"
RESOURCE_GROUP="<rg>"
STORAGE_ACCOUNT="<storage>"
MANAGED_IDENTITY="<uami-name>"

1. Create the resource group

Either use an existing group, or run the command below to create a new one. (To see available locations: az account list-locations --output tsv --query "[].name")

az group create \
  --subscription "$SUBSCRIPTION" \
  --name "$RESOURCE_GROUP" \
  --location <location>

2. Deploy the Bicep template

Use the Bicep template at bicep/stagingResources.bicep to create the storage account, blob lifecycle policy, user-assigned managed identity, and required RBAC assignments (see notes below about how each piece is used).

Preview the changes first. If a storage account with the given name already exists in the resource group, its properties are reconciled to match the template, and this command will show the diff.

# either the path within node_modules, or the local path if you downloaded the file
BICEP_PATH="node_modules/esrp-npm-release/bicep/stagingResources.bicep"

az deployment group what-if \
  --subscription "$SUBSCRIPTION" \
  --resource-group "$RESOURCE_GROUP" \
  --template-file "$BICEP_PATH" \
  --parameters \
    stagingStorageName="$STORAGE_ACCOUNT" \
    managedIdentityName="$MANAGED_IDENTITY"

Apply changes:

az deployment group create \
  --subscription "$SUBSCRIPTION" \
  --resource-group "$RESOURCE_GROUP" \
  --template-file "$BICEP_PATH" \
  --parameters \
    stagingStorageName="$STORAGE_ACCOUNT" \
    managedIdentityName="$MANAGED_IDENTITY"

The deployment is idempotent: re-running it reconciles drift in the storage account properties, role assignments, and lifecycle policy. The storage account name will be passed to the tool as STAGING_STORAGE_ACCOUNT_NAME.

Storage account and lifecycle policy

Within the storage account, the tool uses two blob storage containers (automatically created) as part of the release process:

  • staging: Temporarily hosts the zipped layers so ESRP can download them via user delegation SAS URL.
  • release-state: Persist retry state so ADO stage retries can resume from where a previous attempt left off. State is keyed by repo name, build source version, product name, and npm tag if set.

The template also configures an Azure Storage lifecycle management policy on the staging storage account. It cleans up release-state blobs automatically after (as of writing) 90 days, and staging blobs after 3 days. (The release tool attempts to delete the staging blobs immediately after the release, but the policy provides a fallback.)

Managed identity and roles

All storage access uses Entra ID auth: the tool authenticates with a managed identity, and ESRP downloads each artifact via a short-lived, read-only user delegation SAS URL. Storage account keys and public blob access are disabled, so this setup is compliant with Microsoft's Safe Secrets Standard for storage accounts (SFI-ID4.2.1).

The template assigns two data-plane roles to the managed identity at the storage account scope. Control-plane roles like Contributor or Owner are not sufficient; without the roles below, you'd see a 403 error like "This request is not authorized to perform this operation using this permission" the first time the tool tried to list or write a blob.

  • Storage Blob Data Contributor: List, read, write, and delete blobs in the staging and release-state containers, and create the containers on first use.
  • Storage Blob Delegator: Mint short-lived user-delegation SAS tokens that ESRP uses to download staged zips.

RBAC propagation usually takes less than five minutes. If a freshly-assigned role still produces 403s, wait a few minutes and retry.

3. Create the service connection

In your ADO project, create an Azure Resource Manager service connection:

  • Identity type: Managed identity (automatically configures Workload Identity Federation)
  • Managed identity details: Choose your subscription, resource group, and identity
  • Azure scope: Choose the same subscription and resource group
  • Service Connection Name: Choose any name and make note of it for later

The release stage (later) passes the connection's name to AzureCLI@2 as azureSubscription. It uses the option addSpnToEnvironment: true to obtain a federated idToken that this tool exchanges for an AAD access token at runtime, so there are no long-lived secrets to manage.

Pipeline setup

This tool is designed to run in an Azure DevOps pipeline, presumably using 1ES Pipeline Templates. The typical setup has one stage or pipeline which builds the code and outputs an artifact with the packed packages and release API, and a second stage or pipeline which consumes those artifacts and runs the release tool.

ESRP onboarding

See ESRP OSS publishing docs for onboarding info. For purposes of this tool, you'll need:

  • ESRP-onboarded identity in a production tenant (need client ID and tenant ID): either an app registration with an authentication certificate, or an ESRP-allowlisted managed identity authenticated using workload identity federation.
  • Production tenant key vault storing the request signing certificate and, when using certificate authentication, the ESRP auth certificate (PFX format, base64-encoded)
  • Production tenant managed identity with access to the key vault
  • ADO Azure Resource Manager service connection using the managed identity to access the key vault

Due to the complexity of setting up and maintaining an ESRP registration, it's recommended to see if your engineering systems team or a sibling team has already set one up that you can reuse.

Service connections

The pipeline typically uses two Azure Resource Manager service connections:

  • ESRP (production tenant): access to the key vault containing the request signing certificate and, for certificate authentication, the ESRP auth certificate. For managed identity authentication, this service connection must use the ESRP-allowlisted identity so its federated token can be passed to the tool. (See ESRP onboarding and ESRP resource inputs.)
  • Staging: access to staging blob storage (see staging service connection and staging resource inputs)

Internal feed setup

If your repo normally installs packages from registry.npmjs.org or registry.yarnpkg.com, you'll need to set up an internal feed for the publish build only, since 1ES PT official templates restrict access to public npm.

Start by choosing a feed in your project (or creating a new one) that has the public npm registry as an upstream source. Exact steps may vary by project, but a common setup is to add .npmrc to .gitignore, then use and authenticate with the feed in the release pipeline as follows:

variables:
  REGISTRY_URL: '<yourRegistryUrl>/'
  # ONLY for yarn berry (v2+): it doesn't respect .npmrc, so set config with YARN_* env
  YARN_NPM_REGISTRY_SERVER: $(REGISTRY_URL)
  YARN_NPM_ALWAYS_AUTH: 1
  YARN_NPMRC_AUTH_ENABLED: 1 # enables yarn-plugin-npmrc (see below)

# Later in the pipeline (within a job):
steps:
  # ...

  # WARNING: assumes .npmrc is in .gitignore
  - script: echo 'registry=$(REGISTRY_URL)' >> .npmrc
    displayName: Configure npm registry

  - task: npmAuthenticate@0
    inputs:
      workingFile: $(Build.SourcesDirectory)/.npmrc

There may also be some extra steps depending on your setup:

If using Beachball

When publishing with ESRP, Beachball still uses npm to fetch existing versions for validation. Whether this requires extra settings depends on your Beachball version:

  • v2: must provide the registry setting (--registry arg, or conditionally set in beachball.config.js), but auth will be picked up automatically.
  • v3 (prerelease): respects npm's configured registry and auth, so no extra steps needed.

If using yarn berry (v2+)

Since yarn berry (only) doesn't respect .npmrc, it requires some extra configuration:

  1. Add and commit the plugin yarn-plugin-npmrc so that yarn will pick up credentials from .npmrc (but don't set npmrcAuthEnabled).
  2. In your pipeline, set the YARN_* variables as shown above.

If using npm or yarn v1

These package managers save resolved URLs in the lock file, which requires an extra step to manually overwrite them before installing.

  1. In your pipeline, after npmAuthenticate but before installing dependencies, add a step to update the lock file:

    - script: npx --package beachball@next beachball-auth-helper update-lock-registry --registry '$(REGISTRY_URL)'
      displayName: Prepare npm registry settings
  2. npm + Beachball only, if pushing changes back to git: Beachball must update package-lock.json after bumping, so you'll need hooks.precommit in beachball.config.js to revert the URL changes. See revertLockFileRegistry.mts for an example.

If using pnpm

There are no currently known extra steps if using pnpm.

(Notes: pnpm's pnpm-lock.yaml omits tarball URLs for the default registry, and reconstructs them from the .npmrc registry at install time. It only stores absolute URLs for non-default registries. pnpm install --frozen-lockfile respects the .npmrc registry without modifying the lock file. Post-bump, pnpm install --lockfile-only should skip registry access provided that workspace: deps are used internally.)

Linking YAML file to Azure DevOps

After creating the relevant resources and your pipeline YAML file, you'll need to link it to Azure DevOps. Typically this is done via Start Right (see internal docs). Notes:

  • You must be elevated to GitHub repo admin when creating the link between the ADO pipeline and the YAML file.
  • Be sure to classify the pipeline as "Production" since it releases code to npm.
  • After creating the pipeline, be sure to change it to "Use app connection" when prompted, so the pipeline doesn't authenticate as you.

Example pipeline YAML

The typical release process using this tool has multiple parts, which could be either stages or separate pipelines depending on your desired setup:

  1. Build stage or pipeline: builds the repo, packs the packages, and publishes a single pipeline artifact containing the packed packages and the release tool (since 1ES PT release jobs can't access source code or npm feeds).
  2. Publish/release stage or pipeline: consumes those artifacts and runs the release tool to publish via ESRP. The job should use 1ES PT template options type: releaseJob and isProduction: true.
  3. (optional) Push stage: commit and push version bumps and changelogs back to git.

In 1ES PT, the presence of a production release job applies stricter network isolation policies to the entire pipeline. If you're using a single pipeline, you'll need to re-enable the access the rest of the pipeline needs (GitHub, Azure) via the networkIsolationPolicy setting.

The examples below cover a single pipeline. Start with the shared setup and build stage, then append one of the publish stages depending on how you authenticate to ESRP. Be sure to fill in all the <placeholders>! See https://github.com/microsoft/beachball/blob/main/.ado/release.yml for a full example, which also pushes updates to GitHub. (For an example of separate pipelines, see this build pipeline and publish/release pipeline. Ignore the parts targeting non-Node platforms.)

Setup template

If your pipeline includes a push stage, it's helpful to put the setup steps between the build and push stages in a shared setup.yml template. (These steps can go inline with the build stage if you don't have a push stage.)

In the samples below, the filename for this template is .ado/templates/setup.yml.

parameters:
  # Node.js major version to install (e.g. "24").
  - name: nodeVersion
    type: string

steps:
  - checkout: self

  - task: UseNode@1
    displayName: Install Node.js ${{ parameters.nodeVersion }}
    inputs:
      version: ${{ parameters.nodeVersion }}.x

  - script: echo "registry=$REGISTRY_URL" >> .npmrc
    displayName: Configure npm registry

  - task: npmAuthenticate@0
    displayName: npm authenticate
    inputs:
      workingFile: $(Build.SourcesDirectory)/.npmrc

  # This isn't used, it's just a clear way to check that auth is working
  # (yarn install's auth errors can be very unclear)
  - script: yarn npm info beachball
    displayName: Get package to verify registry auth

  # ONLY for npm / yarn v1: rewrite lock file URLs to the private registry.
  # `npx` fetches the `beachball-auth-helper` binary from the `beachball` package in the private registry.
  - script: npx --package beachball@next beachball-auth-helper update-lock-registry --registry "$REGISTRY_URL"
    displayName: (npm or yarn v1) Update registry in lock file

  # Modify as appropriate
  - script: yarn --immutable
    displayName: Install dependencies

Common setup and build stage

# Build number/name - modify as desired
name: <name>-release-$(Date:yyyy-MM-dd).$(Rev:rr)

pr: none
trigger: none

resources:
  repositories:
    # fill in per 1ES PT instructions for your org
    - repository: <template repo>
      type: git
      name: <template repo name>
      ref: <release tag>

variables:
  tags: production
  nodeVersion: 24
  # internal feed with npmjs.org as upstream
  REGISTRY_URL: '<yourRegistryUrl>/'
  # ONLY for yarn berry (v2+): it doesn't respect .npmrc, so set config with YARN_* env
  # (using 1 instead of 'true' avoids any issues with weird ADO boolean handling)
  YARN_NPM_REGISTRY_SERVER: $(REGISTRY_URL)
  YARN_NPM_ALWAYS_AUTH: 1
  YARN_NPMRC_AUTH_ENABLED: 1
  # artifact names used below
  packagesArtifactName: packed-packages
  releaseToolArtifactName: release-api-tool

extends:
  template: <1ES PT template>
  parameters:
    pool:
      name: <pool name>
      vmImage: windows-latest
      os: windows # some scans require windows

    settings:
      # If any job has `type: releaseJob`, it applies strict network isolation to the whole pipeline.
      # Re-enable the access needed. (For some reason DefaultDeny actually enables contacting ADO...)
      networkIsolationPolicy: DefaultDeny,AzureActiveDirectory,AzureKeyVault,AzureStorage,GitHub

    stages:
      - stage: build
        displayName: Build artifacts
        jobs:
          - job: build
            displayName: Build, test, pack

            pool:
              name: <1ES PT pool name>
              # could also use windows
              image: ubuntu-latest
              os: linux

            workspace:
              clean: all

            variables:
              PACKAGES_ARTIFACT_PATH: $(Build.StagingDirectory)/${{ variables.packagesArtifactName }}
              RELEASE_TOOL_ARTIFACT_PATH: $(Build.StagingDirectory)/${{ variables.releaseToolArtifactName }}

            templateContext:
              outputParentDirectory: $(Build.StagingDirectory)
              outputs:
                - output: pipelineArtifact
                  artifactName: ${{ variables.packagesArtifactName }}
                  path: $(PACKAGES_ARTIFACT_PATH)
                - output: pipelineArtifact
                  artifactName: ${{ variables.releaseToolArtifactName }}
                  path: $(RELEASE_TOOL_ARTIFACT_PATH)

            steps:
              - template: /.ado/templates/setup.yml@self
                parameters:
                  nodeVersion: ${{ variables.nodeVersion }}

              # TODO: insert steps to build, test, etc

              # Bump and pack packages (update command as needed).
              # This could also run some other script that outputs packages in the same format.
              - script: |
                  mkdir -p "$PACKAGES_ARTIFACT_PATH"
                  yarn beachball publish --no-push --pack-to-path "$PACKAGES_ARTIFACT_PATH"
                displayName: Pack packages

              # Update as needed for your install layout
              - script: |
                  mkdir -p "$RELEASE_TOOL_ARTIFACT_PATH"
                  cp -r "$BUILD_SOURCESDIRECTORY/node_modules/@microsoft/esrp-npm-release/dist/index.mjs" "$RELEASE_TOOL_ARTIFACT_PATH"
                displayName: Copy release API tool to staging directory

Publish stage

Add one of the following publish stages to the pipeline (under extends.parameters.stages). The stage is tagged as type: releaseJob and isProduction: true, which means it can't check out code or install dependencies; it can only download the previously-created artifacts.

If this stage fails partway through, it should be safe to re-run unless the error from ESRP is failDoNotRetry (but see note below about 404 errors from npm). The tool's internal state tracking ensures successful layers are not re-submitted. If a layer fails partway through and is re-submitted, the ESRP service will silently skip the package versions that already exist in the registry.

Note that a 404 error from npm typically indicates a permission issue. For an existing package, the most likely cause is that the npm accounts @microsoft1es and @microsoft-oss-releases have not been added as organization or package owners.

- stage: publish
  displayName: Publish
  dependsOn: build
  jobs:
    - job: npm_publish
      displayName: Publish npm packages

      pool:
        name: <1ES PT pool name>
        image: ubuntu-latest
        os: linux

      variables:
        PACKAGES_ARTIFACT_PATH: $(Agent.BuildDirectory)/${{ variables.packagesArtifactName }}
        RELEASE_TOOL_ARTIFACT_PATH: $(Agent.BuildDirectory)/${{ variables.releaseToolArtifactName }}

      templateContext:
        type: releaseJob
        isProduction: true
        inputs:
          - input: pipelineArtifact
            artifactName: ${{ variables.packagesArtifactName }}
            targetPath: $(PACKAGES_ARTIFACT_PATH)
          - input: pipelineArtifact
            artifactName: ${{ variables.releaseToolArtifactName }}
            targetPath: $(RELEASE_TOOL_ARTIFACT_PATH)

      steps:
        - task: UseNode@1
          displayName: Install Node.js 24
          inputs:
            version: 24.x

        # Get credentials that will be used to temporarily upload zips to the staging storage account
        # in your team's Azure subscription
        - task: AzureCLI@2
          displayName: Get credentials for staging blob storage
          inputs:
            azureSubscription: <staging service connection name>
            scriptType: bash
            scriptLocation: inlineScript
            addSpnToEnvironment: true
            inlineScript: |
              echo "##vso[task.setvariable variable=STAGING_TENANT_ID]$tenantId"
              echo "##vso[task.setvariable variable=STAGING_CLIENT_ID]$servicePrincipalId"
              echo "##vso[task.setvariable variable=STAGING_ID_TOKEN;issecret=true]$idToken"

        # Fetch both ESRP certificates from the production tenant key vault
        - task: AzureKeyVault@2
          displayName: Get ESRP certificates from Key Vault
          inputs:
            azureSubscription: <ESRP service connection name>
            KeyVaultName: <key vault name>
            SecretsFilter: <auth cert name>,<request signing cert name>

        # Run the tool (see "Tool inputs" below for details on each variable)
        - script: node "$RELEASE_TOOL_ARTIFACT_PATH/index.mjs"
          displayName: Publish using ESRP Release API
          env:
            PACKED_PACKAGES_PATH: $(PACKAGES_ARTIFACT_PATH)

            # Staging storage credentials
            STAGING_STORAGE_ACCOUNT_NAME: <storage account name>
            # set above by AzureCLI@2 but must be mapped in
            STAGING_CLIENT_ID: $(STAGING_CLIENT_ID)
            STAGING_TENANT_ID: $(STAGING_TENANT_ID)
            STAGING_ID_TOKEN: $(STAGING_ID_TOKEN)

            # ESRP credentials using authentication certificate
            ESRP_AUTH_CERT: $(<auth cert name>)
            ESRP_REQUEST_SIGNING_CERT: $(<request signing cert name>)
            ESRP_TENANT_ID: <production tenant ID>
            ESRP_CLIENT_ID: <ESRP app registration client ID>

            # Release info (must be unique per invocation if publishing multiple times per build)
            ESRP_PRODUCT_NAME: <friendly product name>
            ESRP_NPM_TAG: <npm dist-tag> # optional
            ESRP_APPROVERS: <user email(s)>
            ESRP_OWNERS: <user email(s)>

Note: ID tokens have a lifetime on the order of 1 hour. Because the esrp-npm-release script has no way to refresh this token, if publishing packages takes more than 1 hour, this process may fail. Retrying the pipeline stage is safe and will start where the previous attempt left off.

- stage: publish
  displayName: Publish
  dependsOn: build
  jobs:
    - job: npm_publish
      displayName: Publish npm packages

      pool:
        name: <1ES PT pool name>
        image: ubuntu-latest
        os: linux

      variables:
        PACKAGES_ARTIFACT_PATH: $(Agent.BuildDirectory)/${{ variables.packagesArtifactName }}
        RELEASE_TOOL_ARTIFACT_PATH: $(Agent.BuildDirectory)/${{ variables.releaseToolArtifactName }}
        TOOL_ARTIFACT_BIN: $(RELEASE_TOOL_ARTIFACT_PATH)/index.mjs

      templateContext:
        type: releaseJob
        isProduction: true
        inputs:
          - input: pipelineArtifact
            artifactName: ${{ variables.packagesArtifactName }}
            targetPath: $(PACKAGES_ARTIFACT_PATH)
          - input: pipelineArtifact
            artifactName: ${{ variables.releaseToolArtifactName }}
            targetPath: $(RELEASE_TOOL_ARTIFACT_PATH)

      steps:
        - task: UseNode@1
          displayName: Install Node.js 24
          inputs:
            version: 24.x

        # Get credentials that will be used to temporarily upload zips to the staging storage account
        # in your team's Azure subscription
        - task: AzureCLI@2
          displayName: Get credentials for staging blob storage
          inputs:
            azureSubscription: <staging service connection name>
            scriptType: bash
            scriptLocation: inlineScript
            addSpnToEnvironment: true
            inlineScript: |
              echo "##vso[task.setvariable variable=STAGING_TENANT_ID]$tenantId"
              echo "##vso[task.setvariable variable=STAGING_CLIENT_ID]$servicePrincipalId"
              echo "##vso[task.setvariable variable=STAGING_ID_TOKEN;issecret=true]$idToken"

        # Only the request signing certificate is needed with managed identity authentication
        - task: AzureKeyVault@2
          displayName: Get ESRP request signing certificate from Key Vault
          inputs:
            azureSubscription: <ESRP service connection name>
            KeyVaultName: <key vault name>
            SecretsFilter: <request signing cert name>

        # Capture the federated token from the ESRP-allowlisted identity used by the service connection
        - task: AzureCLI@2
          displayName: Get credentials for ESRP
          inputs:
            azureSubscription: <ESRP service connection name>
            scriptType: bash
            scriptLocation: inlineScript
            addSpnToEnvironment: true
            inlineScript: |
              echo "##vso[task.setvariable variable=ESRP_ID_TOKEN;issecret=true]$idToken"

        # Run the tool (see "Tool inputs" below for details on each variable)
        - script: node '$(TOOL_ARTIFACT_BIN)'
          displayName: Publish using ESRP Release API
          env:
            PACKED_PACKAGES_PATH: $(PACKAGES_ARTIFACT_PATH)

            # Staging storage credentials
            STAGING_STORAGE_ACCOUNT_NAME: <storage account name>
            # set above by AzureCLI@2 but must be mapped in
            STAGING_CLIENT_ID: $(STAGING_CLIENT_ID)
            STAGING_TENANT_ID: $(STAGING_TENANT_ID)
            STAGING_ID_TOKEN: $(STAGING_ID_TOKEN)

            # ESRP credentials using managed identity
            ESRP_ID_TOKEN: $(ESRP_ID_TOKEN)
            ESRP_REQUEST_SIGNING_CERT: $(<request signing cert name>)
            ESRP_TENANT_ID: <production tenant ID>
            ESRP_CLIENT_ID: <ESRP-allowlisted managed identity client ID>

            # Release info (must be unique per invocation if publishing multiple times per build)
            ESRP_PRODUCT_NAME: <friendly product name>
            ESRP_NPM_TAG: <npm dist-tag> # optional
            ESRP_OWNERS: <user email(s)>
            ESRP_APPROVERS: <user email(s)>

Push stage (optional)

If your repo has traditionally pushed packages directly back to GitHub with a personal access token, you can instead use a GitHub app token and the beachball-auth-helper CLI.

- stage: push
  displayName: Push to git
  dependsOn: publish
  jobs:
    - job: push
      pool:
        name: <1ES PT pool name>
        image: ubuntu-latest
        os: linux
      steps:
        - template: /.ado/templates/setup.yml@self
          parameters:
            nodeVersion: ${{ variables.nodeVersion }}

        # TODO: add a step to build your packages

        - task: AzureCLI@2
          name: createToken
          displayName: Create GitHub App token
          inputs:
            azureSubscription: <GitHub App service connection name>
            scriptType: bash
            scriptLocation: inlineScript
            inlineScript: yarn beachball-auth-helper create-github-app-token
          env:
            APP_CLIENT_ID: <GitHub App client ID>
            KEY_ID: <key vault key URL>
            REPOSITORY: $(Build.Repository.Name)
            PERMISSIONS: contents:write
            CI_OUTPUT_NAME: GITHUB_APP_TOKEN

        # Run publish without publishing to npm. Beachball reads `BEACHBALL_GIT_TOKEN` to authenticate the git push.
        # Update the command as needed for your repo.
        - script: yarn beachball publish --no-publish
          displayName: Push package updates
          env:
            BEACHBALL_GIT_TOKEN: $(createToken.GITHUB_APP_TOKEN)

        - script: yarn beachball-auth-helper revoke-github-app-token
          displayName: Revoke GitHub App token
          condition: and(always(), ne(variables['createToken.GITHUB_APP_TOKEN'], ''))
          env:
            TOKEN: $(createToken.GITHUB_APP_TOKEN)