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 .npmignored. 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.