Publishing Npm to Azure Devops With Github
Why is this so difficult again?⌗
What should have been a fairly straightforward task, turned out to be anything but. The task? Test, build, and publish a JavaScript package using NPM to an Azure DevOps package artifact registry using a GitHub action. Sounds straightforward enough, right? There are plenty of Stack Overflow Questions with answers close to this. All with some variation on how to handle the credentials and tokens too. None were quite right though. Other solutions included pulling in some random GitHub action task and passing the details into that. But who wants to keep an audit yet another dependency?
Hopefully if you stumble upon this, what worked for me will also work for you.
Pre-Requisites⌗
Things you’re going to need for this:
- A GitGub account that can run actions / store secrets
- An Azure DevOps account with an organization, project, and artifact feed, and a personal access token for accessing it (The token needs Packaging read/write permissions).
- The token also converted to base64. I’ll cover this below.
- Your package
Making things work⌗
First, we will need the Azure DevOps Personal Access Token. If you already have it skip down to the next paragraph. Otherwise, go to your Azure DevOps User Settings, the URL will be something like: ‘https://dev.azure.com/ORGANIZATION/_usersSettings/tokens' Click to create a “New Token” Name it something sane like “ORGANIZATION_DOPS_PACKAGES_TOKEN” I recommend setting expiration to no more than 90 days, ideally sooner. Under scopes, make sure that at least Read & Write is checked under “Packaging” Take the value that is produced, store in your password / token manager of choice.
You will also be needing to convert that PAT to a Base64 string. The instructions that Microsoft provides for this is to run the following (Note, don’t run arbitrary code you find on the Internet without proofing it first, that’s bad. These same instructions can be found by going to your artifact feed, clicking “connect to feed”, “NPM”, under “Project Setup” go to “Other” the code block is under Step 3 as of writing.)
node -e "require('readline') .createInterface({input:process.stdin,output:process.stdout,historySize:0}) .question('PAT> ',p => { b64=Buffer.from(p.trim()).toString('base64');console.log(b64);process.exit(); })"
It will prompt you now for the PAT, paste your PAT in and the output (to stdout) will be the base64 encoded string. You will need this in a moment. I suggest also adding it to your token manager with the same expiration. If your manager supports notes, throw it in there for when it expires.
Next, go to your GitHub repository. Then in “Settings”, Under “Security” -> “Actions” or if working on an GitHub organization under the Organization “Settings” also under “Security” -> “Actions” create a new secret. Name it something that makes sense like “ORGANIZATION_DOPS_NPM_ARTIFACT_TOKEN” for the value, you’re going to need that Base64 encoded value of your Azure DevOps PAT. Make sure it has access to the repository you’re working against.
Now back to our code repository for setting up the action, and making sure ‘.npmrc’ and ‘package.json’ files have the right values. Starting with .npmrc it will need the following:
registry=https://pkgs.dev.azure.com/ORGANIZATION/TEAM/_packaging/FEED/npm/registry/
; begin auth token
//pkgs.dev.azure.com/ORGANIZATION/TEAM/_packaging/FEED/npm/registry/:username=ORGANIZATION
//pkgs.dev.azure.com/ORGANIZATION/TEAM/_packaging/FEED/npm/registry/:_password=${NODE_AUTH_TOKEN}
//pkgs.dev.azure.com/ORGANIZATION/TEAM/_packaging/FEED/npm/registry/:email=npm requires email to be set but doesn't use the value
//pkgs.dev.azure.com/ORGANIZATION/TEAM/_packaging/FEED/npm/:username=ORGANIZATION
//pkgs.dev.azure.com/ORGANIZATION/TEAM/_packaging/FEED/npm/:_password=${NODE_AUTH_TOKEN}
//pkgs.dev.azure.com/ORGANIZATION/TEAM/_packaging/FEED/npm/:email=npm requires email to be set but doesn't use the value
; end auth token
always-auth=true
A sample of this is also provided in the “Connect to Feed” instructions above in your Azure DevOps. It’s a combination of ‘Step 1’ and and the “Add a .npmrc to your project, in the same directory as your packages.json” information. You don’t have to worry about the apostraphe either, it does get ignored properly in the email section. Though it does seem in bad taste on Microsoft’s side…
Now in your ‘package.json’ file you’re going to add a “publishConfig” section in the main JSON. "publishConfig":{"registry":"https://pkgs.dev.azure.com/ORGANIZATION/TEAM/_packaging/FEED/npm/registry/"}
An example is provided below.
{
"name": "Foo Package",
"version": "0.0.1",
"description": "Foo Package",
"main": "dist/index.js",
"type": "module",
"private": false,
"publishConfig":{"registry":"https://pkgs.dev.azure.com/Organization/Team/_packaging/feed/npm/registry/"},
"scripts": {
"test": "NODE_OPTIONS=--experimental-vm-modules npx jest",
"build": "webpack --config ./config/webpack.prod.config.cjs",
"start": "webpack serve --open --config ./config/webpack.dev.config.cjs"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ORGANIZATION/REPO_NAME.git"
},
"author": "John Doe",
"license": "MIT",
"bugs": {
"url": "https://github.com/ORGANIZATION/REPO_NAME/issues"
},
"homepage": "https://github.com/ORGANIZATION/REPO_NAME#readme",
"devDependencies": {
"html-loader": "^3.1.2",
"html-webpack-plugin": "^5.5.0",
"jest": "^28.1.3",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
}
}
Next up, the action file. This will be in the repo’s base under .github/workflows
we’ll create our workflow file. I like to name them something that makes sense based on when it runs and what it does. So something like dev-test-and-publish.yml
And here’s the meat and potatoes. We DO NOT want the registry in this file. It will absolutely not work based on testing if we pass it to the actions/setup-node tasks. All in it’s a pretty straightforward action that checks out the code, makes sure Node.js is setup, makes sure deps are there, runs the tests, and then publishes. We need to make sure that the env setting of NODE_AUTH_TOKEN has the appropriate secret of the base64 token. It cannot be the non-b64 encoded token or it won’t work. If you changed the name above when adding the token to your GitHub account, make sure it’s changed here as well. Also, if you change the environment variable here, make sure to update the name in the curly brackets in your .npmrc file.
name: Test and Publish
env:
NODE_AUTH_TOKEN: ${{ secrets.ORGANIZATION_DOPS_NPM_ARTIFACT_TOKEN }}
on:
push:
branches: [ dev ]
jobs:
Test-and-Publish:
name: Test and Publish to Azure DevOps registry
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js environment
uses: actions/setup-node@v3
with:
node-version: lts/*
- run: npm ci
- name: Run Tests
run: npm test
- name: Publish to Azure DevOps artifact registry
run: npm publish --tag dev
For the actions we are using, their references can be found here:
Everything else is just standard CLI commands to the Ubuntu Image. You can run it on whatever runner you’d like. I prefer to opt for the LTS releases when possible. One thing to note, is under the publish we are passing the tag name. By default it goes to “latest”. To make it easier to identify in your registry rely on the the tagging to keep the branch up to date. Because the registry is immutable, you cannot re-use a package version when publishing. As a result, good adherance to symantic versioning is required. You can also change that tag value to whatever you wish.
You can make this action more complicated to change up the tag based on branch, but that’s for a different day. For simple setups, I’d recommend just creating a second task called main-test-and-publish.yml and change the tag to “stable” or “latest” As best I can tell, you cannot do multiple tags at publishing. So if you want to add another just add another task with a name like “Add X tag” and run npm dist-tag add package@version stable
or whatever you want the tag to be. Also, see the dist-tagging documents.
Anyway, at this point you have done all the required work, and when you push this to the branch name is covered in the action, it will run and publish appropriately. Make sure to bump your version every time you make a change so that the package can be updated.
References Using private packages in a CI/CD workflow Configuring NPM, Package-JSON How to authenticate against npm registries in Azure DevOps Checkout Setup Node.js Environment