Publish an NPM Package to JFrog Artifactory
Here's how to publish an NPM package to JFrog Artifactory.
Create a JFrog Artifactory Token
You use JFrog Artifactory when you want to publish a package to a private registry. The normal, public one at npmjs.com is too public. You want privacy and/or control. Get Artifactory deployed, set up the repository, and get a login to it.
If you go to the Artifactory > Artifacts page for your repository, you'll see a "Set Me Up" button. Click it and under the Configure tab, you'll see a "Generate Token & Create Instructions" button. Click that, and you'll see "The token has been created successfully!"
This token is used for both JFrog (jf
) cli auth and npm auth.
Build NPM Package
There are probably lots of ways to do this. Some of this setup is a bit irrelevant to the JFrog integration. Bypass if you have your own build already. Also, this build feels a bit silly for a nodejs project because we're not going to use npm publish
later, like we might with npmjs.com.
Let's say that our package is called myproject
. It's in a TypeScript project. We'll write scripts/build.sh
script:
#!/bin/bash
mkdir -p @myteam/myproject
tsc
npm pack
mv myproject-*.tgz @myteam/myproject/
npm pack
will create a tarball of everything in the project that's not .npmignore
d. Modify that file to modify the contents of the tarball. And you'll likely want to modify the .gitignore
file too:
@myteam/myproject/
*.tgz
And we set up our package.json
with an npm-script:
{
"name": "@myteam/myproject",
"version": "0.0.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "chmod +x ./scripts/build.sh && ./scripts/build.sh"
},
"devDependencies": {
"typescript": "^5.6.2"
}
}
And we have a tsconfig.json
that builds the js files:
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"module": "commonjs",
"target": "es6",
"declaration": true
},
"include": ["src/**/*"]
}
Publish to JFrog Artifactory
We have a project in Github. We want to use Github Actions to build and publish our project. We need to set two set two variables. On the Github project, go to Settings > Secrets and Variables > Actions.
Set one variable:
JF_URL=https://myteam.jfrog.io/
And set one secret:
JF_ACCESS_TOKEN=the-token-you-just-generated
Now create a Github action for publishing in your project source tree:
mkdir -p .github/workflows/
touch .github/workflows/build_and_publish.yml
The build_and_publish.yml
file might look like this:
name: build-and-publish
on:
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
# Pulls in vars and secrets from GH project and makes available to commands here
env:
JF_URL: ${{ vars.JF_URL }}
JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }}
steps:
- name: Checkout Repo
uses: actions/checkout@v3
- name: Setup JFrog CLI
uses: jfrog/setup-jfrog-cli@v4
with:
version: latest
- name: Set CLI Config
run: |
jf npm-config --global=true
- uses: actions/setup-node@v4
with:
node-version-file: '.tool-versions'
cache: 'npm'
scope: '@myteam'
# runs the build script we made above
- name: Install build deps and build
run: |
npm ci
npm run build
# "myteam-npm-local" is the name of the local npm repository in Artifactory
- name: Publish
run: |
jf rt upload *.tgz myteam-npm-local/
- name: Publish Build info With JFrog CLI
run: |
# Collect environment variables for the build
jf rt build-collect-env
# Collect VCS details from git and add them to the build
jf rt build-add-git
# Publish build info
jf rt build-publish
In order for all of those jf
JFrog CLI commands to work against Artifactory, the JF_URL
and the JF_ACCESS_TOKEN
need set.
Because this action is run on workflow_dispatch
, run it by going to the Actions tab in your Github project and clicking the "Run workflow" button.
If it all works well, you'll have a new tarball in your Artifactory npm. Check out the Artifactory > Artifacts page. Browse to your equivalent of myteam-npm-local/@myteam/myproject/myproject-0.0.1.tgz
.
Manually Install a Package from JFrog Artifactory
Now you're on the consuming side in a different client project. You want this project to be able to install your package from Artifactory.
First, teach npm where to find @myteam
-scoped packages. Make an .npmrc
file in the root of your project:
@myteam:registry=https://myteam.jfrog.io/artifactory/api/npm/myteam-npm/
Now, all requests to download @myteam
-scoped packages will go to Artifactory. The rest will go to normal npmjs.com
.
Now authenticate with Artifactory manually. This will take you to the JFrog Artifactory login web ui:
npm login --registry=https://myteam.jfrog.io/artifactory/api/npm/myteam-npm/
Now your install should work:
npm install @myteam/myproject
Install a Package from JFrog Artifactory in CI
But in CI, you don't want a user to have to intervene and type something into a web ui. Instead, you need this to happen automatically. That'll take some more configuration. And I tried many configurations. The docs that helped the most were the official npm docs about private packages in CI and this Stack Overflow answer on the contents of your npmrc.
Further adjust your .npmrc
to look like this:
@myteam:registry=https://myteam.jfrog.io/artifactory/api/npm/myteam-npm/
//myteam.jfrog.io/artifactory/api/npm/myteam-npm/:_authToken=${NPM_TOKEN}
The new second line will be key to authenticating against that npm registry url. The ${NPM_TOKEN}
string is literally that. It is replaced at runtime by the npm tools. Don't paste your actual token value in there. When it is replaced, this will be the JFrog access token that you generated earlier.
Also note that the registry url is using the virtual repository on the JFrog side (not the local repository in JFrog). The virtual repo linking to external packages from JFrog if you want to. It's more flexible.
Now make sure that your client project repository settings include the secrets JF_URL
and JF_ACCESS_TOKEN
, as set in the package repo in the previous section.
On to CI: Once you're running Github Actions on your client project, you're doing things like building and static analysis. You'll need to install from npm to be able to do this. And installing needs to handle authenticated requests to the JFrog Artifactory npm registry. Let's say that we want to do some linting and schtuff. Our Github action will look something like:
# Creating an env var that the npm cli uses to replace the matching string in npmrc
env:
NPM_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }}
jobs:
static-analysis:
if: github.event_name == 'push'
strategy:
max-parallel: 3
matrix:
command: ['typecheck', 'lint:ci', 'test']
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version-file: '.tool-versions'
cache: 'npm'
# the job with the install that will now work
- run: npm ci
- run: npm run ${{ matrix.command }}
Create a Dockerfile that Installs from JFrog Artifactory
And now finally for your client project deploy. You want to be able to build a docker image that has the node dependencies it needs. For this, you'll need to install from JFrog Artifactory as well.
You are going to use the NPM_TOKEN
again, so keep the .npmrc
additions from the previous section.
In the Github action that builds the docker image, add some build-args
that will get passed as arguments to the docker command. Something like this:
- name: Build and push
id: docker_build_client
uses: docker/build-push-action@v5
with:
cache-from: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ github.event.repository.name }}:${{ env.ENV_TAG }}
cache-to: type=inline
context: .
file: "Dockerfile"
labels: ${{ steps.meta_client.outputs.labels }}
push: true
tags: ${{ steps.meta_client.outputs.tags }}
build-args: |
NPM_TOKEN=${{ secrets.JF_ACCESS_TOKEN }}
Now, finally, adjust the Dockerfile
. Use the build arg and set it in the environment of the image layer as NPM_TOKEN
. And make sure that you copy the .npmrc
file into the image. These two bits of data are required to authenticate the npm install, just as it was required in CI:
ARG NPM_TOKEN
ENV NPM_TOKEN=${NPM_TOKEN}
COPY package.json package-lock.json* .npmrc ./
RUN npm ci
And what do you have now? A local npm registry, a private package, a build pipeline for the package, and package install and deploy for your project.