d
Amit DhamuSoftware Engineer
 

Using GitHub Actions with GitHub Packages

9 minute read 00000 views

Background

GitHub Packages is a place where you are able to publish and download packages for a variety of different platforms. At the time of writing, it supports NPM, Docker, NuGet, Apache Maven and RubyGems. There are no storage or transfer limits for open-source projects. Private projects have a 500MB storage and 1GB transfer limit under the free tier. These increase for paid customers respectively.

In this guide, we are going to focus on publishing an NPM package to GitHub packages using GitHub Actions and also show you how to install and use that package in a project. This guide will use Yarn as the preferred package manager.

Create a package

  1. Create a new blank repository on GitHub called my-project with main being the branch. Ensure the visibility of the project is public.

  2. Next, let's create our project locally and set the upstream to track our new repository. Ensure @github_username is your GitHub username.

mkdir new-project
cd $_
git init
git remote add origin git@github.com:github_username/my-project.git
echo "# my-project" > README.md
git add -A
git commit -m "Initial commit"
git push -u origin main
  1. Next, let's create a branch to work on version 1 of our project. The code below will create a new branch, add a basic package.json file with one dependency. It will then commit the package.json.
git checkout -b v1
cat > package.json << EOF
{
  "name": "@github_username/my-project",
  "version": "1.0.0",
  "license": "UNLICENSED",
  "author": "Your name",
  "dependencies": {
    "lodash": "^4.17.21"
  }
}
EOF
git add -A
git commit -m "Add package.json"
git push -u origin v1
  1. Now let's add a bit of code to our package and commit it.
mkdir src
echo 'console.log("hello")' > src/index.js
git add -A
git commit -m "Add index.js"
git push

Add a GitHub Actions Workflow file

mkdir -p .github/workflows
cat > .github/workflows/ci.yml << EOF
name: CI

on: push

jobs:
  pipeline:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Install Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '14.x'
          registry-url: 'https://npm.pkg.github.com'
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Publish to GitHub Registry
        run: |
          pkg_name=$(cat package.json | jq -r '.name')
          pkg_version=$(cat package.json | jq -r '.version')
          branch=${GITHUB_REF##*/}

          if [[ $branch != "main" ]]; then
            yarn version --new-version "$pkg_version-beta$GITHUB_RUN_NUMBER" --no-git-tag-version
            yarn publish --tag beta --access public
          else
            yarn publish --access public
          fi
EOF

Let's go through the steps of what's going on above

  • on: push

    • This workflow will trigger each time we push to any branch on the repository
  • Checkout repository

    • Fairly self-explanatory. This step checks out the repository
  • Install Node.js

    • Here we are saying install Node 14 for the runner to use for further steps. We set the registry-url to https://npm.pkg.github.com which is what we need to publish to GitHub packages. Lastly, we need to provide the step with a token. GitHub gives us GITHUB_TOKEN automatically for use with Actions.
  • Publish to GitHub Registry

    • This is where things get interesting. Luckily the GitHub runners provide some built-in utils on the image they run on. Good for us because we want to use jq to parse the contents of the package.json file.

    • We extract the package name and version number.

    • Important - the published version of the package will be derived from the version in package.json. Ensure you update this adequately when you push.

    • We also retrieve the branch name using a special GitHub variable called GITHUB_REF.

    • If branch is not main

      • If we are on a branch, we want to publish a beta version of the package appended with the Actions run number. This means for testing, we can keep committing and pushing without manually needing to change the version number.

      • For example, in our case, when pushed on a branch other than main, the package version will be 1.0.0-beta1 with 1 incrementally increasing each time a new push is made.

    • If branch is main

      • If we're on main, we will publish a non-beta version and this will solely be derived from the version specified in your package.json.

      • Please note: We are working with a public package so pass the --access public flag. You can adjust this for private packages if required.

Commit the workflow file

git add -A
git commit -m "Add workflow file"
git push

Once you have done this, if you head over to https://github.com/github_username/my-project/actions, you should see the action start.

Once complete, go to the repository homepage and click the settings cog next to About in the top right corner of the screen. You can then toggle the visibility of packages and they should appear, after ticking the packages checkbox, on the right hand sidebar.

1.0.0

Time to merge our branch to main and get the non-beta version published. The below code will also delete the v1 branch.

git checkout main
git merge v1
git branch -d v1
git push origin :v1

Again, check the Actions area on the repository to ensure it successfully publishes.

Consuming the package

Now we are ready to use our package. Unlike NPM, GitHub packages is what's known as a custom registry. This is basically the case for anything not published to NPM.

It means, we can't just do the below as we need to tell Yarn to look somewhere else for our package:

yarn add @github_username/my-project

Get a token

In order to install packages from GitHub packages, we need a token.

You'll need a GitHub Personal Access Token with read:packages permission. You can create one at https://github.com/settings/tokens.

Once you have it, copy it to your clipboard as you'll never see it again and add it to your .zshrc or .bash_profile

export GITHUB_REGISTRY_TOKEN=YOUR_TOKEN_HERE

Then source ~/.zshrc or source ~/.bash_profile.

To confirm it's in your environment, run the following and it should show the contents of your token.

echo $GITHUB_REGISTRY_TOKEN

Create somewhere to install a package

Create a folder somewhere where you'd like to use your new package.

Create an .npmrc

Now we have our token, we need to create an .npmrc file. Go to the folder you just created from the last step and run the following:

cat > .npmrc << EOF
always-auth=true
@github_username:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=\${GITHUB_REGISTRY_TOKEN}
EOF

This is doing a couple of things. Firstly, we're saying that any dependencies prefixed with @github_username should be derived from the GitHub packages registry. Secondly, as we need to authenticate with GitHub to do this, we tell .npmrc to grab the required authToken from the $GITHUB_REGISTRY_TOKEN variable we created earlier.

Install the package

Now we can do:

yarn add @github_username/my-project

With custom registries, Yarn/NPM will try to resolve from the custom registries first before defaulting to https://www.npmjs.org.

Congratulations, you should now have your custom package finally installed in a project!

Tip - To install a beta version of a package:

yarn add @github_username/my-project@beta

.npmignore

As package authors are already probably aware, you can selectively decide what you want your published package to contain.

Run yarn pack inside the folder the package resides in to see what your published package will actually include. If there's files and directories in there that you don't want published, you can create an .npmignore file in your package that will exclude things you tell it. This works similar to a .gitignore with glob patterns supported.

Further Reading