Autodeploy javascript library to npmjs via Github Actions
Tags: javascript, github, devops, tutorial
Published: 2021-09-12 19:22:54

## Overview

It's time to simplify and automate the release of our js library in npmjs using Github Actions.

- [Project initialization](#project-initialization)
- [Automating via Github Actions](#automating-via-github-actions)
- [Finalizing the result](#finalizing-the-result)
- [Bonus: Vuepress documentation](#bonus-vuepress-documentation)
- [Conclusion](#conclusion)
- [Reference](#reference)

**Node.js 14 version with yarn was used**

## Project initialization

- Let's create a project called `javascript-library-autodeploy`.

```shell
mkdir javascript-library-autodeloy && cd javascript-library-autodeploy
```

- Initialize `package.json` and add a couple of libraries for our library to work.

```shell
yarn init
yarn add -D webpack@5.51.1 webpack-cli@4.8.0 eslint@7.32.0 jest@27.1.0
```

Yes, this example will use Javascript Linter - [ESLint](https://eslint.org/) and for running tests - [Jest](https://jestjs.io/). We all write tests, right? :)

- Final version of `package.json`.

```json
{
  "name": "@alexeykhr/javascript-library-autodeploy",
  "version": "1.0.0",
  "license": "MIT",
  "scripts": {
    "dev": "webpack --mode=development",
    "build": "webpack --mode=production",
    "test": "jest",
    "lint": "eslint src"
  },
  "devDependencies": {
    "eslint": "^7.32.0",
    "jest": "^27.1.0",
    "webpack": "^5.51.1",
    "webpack-cli": "^4.8.0"
  }
}
```

- Done, now let's add some logic to our library. In this example, it will be a simple function to add two numbers.

```js
// src/index.js

function sum(a, b) {
    return a + b;
}

module.exports = { sum };
```

- And we will immediately cover this complex logic with tests.

```js
// tests/sum.test.js

const { sum } = require('../src/index');

test('1 + 2 = 3', () => {
    expect(sum(1, 2)).toBe(3);
});
```

- And add a couple more files: [.eslintrc.js](https://raw.githubusercontent.com/oleksiikhr/javascript-library-autodeploy/main/.eslintrc.js), [.npmignore](https://raw.githubusercontent.com/oleksiikhr/javascript-library-autodeploy/main/.npmignore), [.gitignore](https://raw.githubusercontent.com/oleksiikhr/javascript-library-autodeploy/main/.gitignore)
- Done, there is a small project that can be built, added tests, linter, etc.

## Automating via Github Actions

In this step, we will create 2 [Github Workflows](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions).

The first one will be executed after each code change in the Github repository, in any branch, and the second one will push our build into npmjs and Github Packages after the release.

- Let's create the first Workflow to constantly check our code for a working build, Code Style and tests.

```yaml
# .github/workflows/library.yml

name: Library

on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-node@v2
        with:
          node-version: '14.x'

      - name: Get yarn cache directory path
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"

      - uses: actions/cache@v2
        id: yarn-cache
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-

      - run: yarn
      - run: yarn lint
      - run: yarn test
      - run: yarn build
```

The workflow is as follows:

- Calling trigger on `push` event
- Installing the latest Ubuntu
- Receive the code from Github
- Installing Node.js version 14
- Install js libraries and cache them until the `yarn.lock` file changes
- Run a static code analyzer, tests and build

At this stage, we have already automated most of the work for checking Pull Requests.

- Now let's create a second `Workflow`, which will release the build.

```yaml
# .github/workflows/release.yml

name: Release

on:
  release:
    types: [published]

jobs:
  library:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-node@v2
        with:
          node-version: '14.x'
          registry-url: 'https://registry.npmjs.org'

      - name: Get yarn cache directory path
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"

      - uses: actions/cache@v2
        id: yarn-cache
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-

      - run: yarn

      - run: npm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - uses: actions/setup-node@v2
        with:
          registry-url: 'https://npm.pkg.github.com'

      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

The logic of this workflow is very similar to the previous workflow. The only difference is that it fires on the `release` event, and instead of executing linters and tests,` npm publish` is called for npmjs and Github Packages.

But to work correctly, we need to add [Github Secrets](https://docs.github.com/en/actions/reference/encrypted-secrets) to this repository called `NPM_TOKEN`.

You don't need to add `GITHUB_TOKEN`, it is already enabled by default - [`About the GITHUB_TOKEN secret`](https://docs.github.com/en/actions/reference/authentication-in-a-workflow#about-the-github_token-secret)

- Go to the page https://www.npmjs.com/settings/SET_YOUR_LOGIN/tokens
- Click on `Generate New Token`
- Choosing a type `Automation`

![generate-npm-token](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zmcqz8l6ksz5saxvi9fg.png)

- Add it to our repository under the key `NPM_TOKEN`.

*Settings > Secrets > New Secret*

![npm-token](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4bx01zd4xwkeura7p70.png)

## Finalizing the result

Ready! Now we can upload our code to the Github repository and immediately see that the first `Workflow` is launched.

![git-push](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9acphpob90jbdswg2cgh.png)

Now let's try to create a new release. To do this, on the right sidebar, click on **Releases**, and then **Create a new release**.

![first-release](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h1rzubszy6dl1wwww5ks.png)

The `release` event was triggered, and now we can watch how our project is being built and uploaded to the npmjs resource.

![github-workflow-release](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w4ow6s9szk9rxfr3sbf5.png)

Done, here is our library: [@alexeykhr/javascript-library-autodeploy](https://www.npmjs.com/package/@alexeykhr/javascript-library-autodeploy)

You can also notice that the library has appeared in the `Packages` sidebar block.

## Bonus: Vuepress documentation

And of course, how can it be without documentation, I took the first thing that came to hand - this is [Vuepress](https://vuepress.vuejs.org/).

The documentation will live in the current repository, and of course, the build process will be done for us by Github.

- Let's start by adding a `docs` folder and initializing` package.json`.

```shell
mkdir docs && cd docs && yarn init
yarn add -D vuepress@1.8.2
```

Yes, we make a separate `package.json` so that the library versions do not conflict with each other (as, for example, webpack with a different major version). This way the library will not affect our core libraries in any way.

```json
{
  "license": "MIT",
  "scripts": {
    "dev": "vuepress dev .",
    "build": "vuepress build ."
  },
  "devDependencies": {
    "vuepress": "^1.8.2"
  }
}
```

- Let's add a simple `docs/README.md` file that will display the contents of the file.

```markdown
# VuePress

<<< @/../package.json
```

- And some settings for `Vuepress`.

```js
// docs/.vuepress/config.js

const { version, name } = require('../../package');

const parts = name.split('/');

module.exports = {
    title: `Version ${version}`,
    base: `/${parts[parts.length - 1]}/`
}
```

*My library name is the same as the name of the Github repository, so the base url is taken from package.json*

This is enough to be able to build something, and for an explicit display of the build, we are publishing the version of the library in the documentation.

- Now let's update our Github Workflows.

In **.github/workflows/library.yml** let's change the trigger so that it doesn't fire when we just edit the documentation.

```yaml
on:
  push:
    paths:
      - '*/**'
      - '!docs/**'
```

And in **.github/workflows/release.yml** add another job.

```yaml
docs:
  runs-on: ubuntu-latest
  defaults:
    run:
      working-directory: ./docs
  steps:
    - uses: actions/checkout@v2

    - uses: actions/setup-node@v2
      with:
        node-version: '14.x'

    - name: Get yarn cache directory path
      id: yarn-cache-dir-path
      run: echo "::set-output name=dir::$(yarn cache dir)"

    - uses: actions/cache@v2
      id: yarn-cache
      with:
        path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
        key: ${{ runner.os }}-yarn-docs-${{ hashFiles('yarn.lock') }}
        restore-keys: |
          ${{ runner.os }}-yarn-docs-

    - run: yarn
    - run: yarn build

    - name: Commit changes
      working-directory: ./docs/.vuepress/dist
      run: |
        git config --global user.name "github-actions"
        git config --global user.email "github-actions@github.com"
        git init
        git add -A
        git commit -m "deploy"

    - name: Push changes
      uses: ad-m/github-push-action@master
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        branch: gh-pages
        force: true
        directory: ./docs/.vuepress/dist
```

The logic is as follows:

- Runs `yarn install` inside the` docs` folder
- Then the documentation is built
- The entire build is uploaded using force push under the `gh-pages` branch

And these 2 jobs will run in parallel.

- Add new code, update the library version and push to Github.
- Add one more release to execute a new job
- It remains to add the `gh-pages` branch to [Github Pages](https://pages.github.com/) to display the documentation

![github-pages](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m5q2xl76x5ysp5gv6p1i.png)

## Conclusion

Congratulations, we have automated a lot of our work, now there is no need to write huge scripts, what and why to perform at release. Sometimes forget to deploy code in npmjs :)

But you can also automate even more, if, for example, you write according to [Conventional Commits](https://github.com/conventional-commits/conventionalcommits.org), you can also automate the creation of tags using the [standard-version](https://github.com/conventional-changelog/standard-version) library.

And use all sorts of tools, like [actions/labeler](https://github.com/actions/labeler).

## Reference

- [Github repository](https://github.com/oleksiikhr/javascript-library-autodeploy)
- [npmjs library](https://www.npmjs.com/package/@alexeykhr/javascript-library-autodeploy)