@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
EsrpReleasetask for simplicity. Or if you care about ordering but only have a few packages, it might be reasonable to manually group them and run theEsrpReleasetask multiple times.Note that the pipeline setup (including internal feed setup) described below is also applicable to repos using the
EsrpReleasetask; 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:
- Zips the layer's package tarballs.
- 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.)
- Calls the ESRP release API to publish the layer, waiting for it to complete before moving on to the next layer.
- 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.
- ⚠️ If using
- 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_NAMEorESRP_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
| 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
stagingandrelease-statecontainers, 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)/.npmrcThere 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
registrysetting (--registryarg, or conditionally set inbeachball.config.js), but auth will be picked up automatically. - v3 (prerelease): respects npm's configured
registryand auth, so no extra steps needed.
If using yarn berry (v2+)
Since yarn berry (only) doesn't respect .npmrc, it requires some extra configuration:
- Add and commit the plugin
yarn-plugin-npmrcso thatyarnwill pick up credentials from.npmrc(but don't setnpmrcAuthEnabled). - 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.
In your pipeline, after
npmAuthenticatebut 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 settingsnpm+ Beachball only, if pushing changes back to git: Beachball must updatepackage-lock.jsonafter bumping, so you'll needhooks.precommitinbeachball.config.jsto revert the URL changes. SeerevertLockFileRegistry.mtsfor 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:
- 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).
- 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: releaseJobandisProduction: true. - (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 dependenciesCommon 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 directoryPublish 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-releasescript 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)