Sorry for the long break between posts. You may have noticed that there’s a lot of events coming up on my calendar in the next two months, so I’ve been busier than normal preparing for those. Now that things are settling down a bit, I wanted to share some details that others might find helpful. As part of my preparations, I needed to create and publish a package to GitHub Packages. There’s plenty of guidance for publishing with plain npm or classic Yarn, but far less that covers the details of using GitHub Actions and to publish packages from a project that uses Yarn 4.
What is yarn
Yarn is an open source package manager for JavaScript and TypeScript. It reimplements most of the npm functionality and improves on it, making the development experience a bit easier in my opinion. It supports parallel downloads of dependencies and a “PnP” mode that doesn’t require it to create a node_modules
folder to expand the packages. Instead, it can read directly from the compressed packages, which can really make life easier when working with JavaScript.
It’s important to know that Yarn was changed significantly in version 2. However, there’s still a lot of documentation that focuses on version 1 (“classic”). This article focuses on version 4 (“berry”), which is the latest stable version as of this writing.
Publishing the package
To create and publish the package to GitHub Packages, you need to configure a few things. Knowing what to configure (and how) is the key to successfully publishing the package. Since this involves a few different files, I’ll walk through each of them.
package.json
This contains the metadata for the package. The most important field to configure is name
. When publishing to GitHub Packages, the name must be in the format @OWNER/package
, where OWNER
is your GitHub username or organization name, and package
is the name of the package. For example, if your GitHub username is kenmuse
and your package is named my-package
, the name would be @kenmuse/my-package
. The package name and repository name don’t have to match, but it’s often best practice to keep them the same to avoid confusion.
Next, make sure you have configured the repository
entry. This indicates where the source code should be located for your package. You’ll also want to link to the registry for the package. This is done with the publishConfig
entry. For publishing to GitHub Packages, the entries will look like this:
1{
2 "name": "@kenmuse/my-package",
3 "version": "0.0.1",
4 "repository": {
5 "type": "git",
6 "url": "https://github.com/kenmuse/your-repo.git"
7 },
8 "publishConfig": {
9 "registry": "https://npm.pkg.github.com"
10 }
11}
You may notice that the registry link in this case does not contain your username or organization name. This is because GitHub Packages uses a single registry for all packages, and the package name is used to determine the owner. This is why you must specify the owner in the package name. If you don’t, the publish step will fail.
.yarnrc.yml
This file contains the configuration for Yarn. The most important entry to configure is the npmScopes
entry. This entry tells Yarn where to find packages for a specific scope. In this case, you need to configure it to use GitHub Packages when it references your username or organization (in my case, kenmuse
).
As part of that configuration, you can provide an authentication token. It’s a bad practice to rely on hard-coded credentials, so you also want to make sure to pick up the value dynamically from your environment. To do that, you can use the ${VARIABLE_NAME}
syntax. There is one issue with this approach, however. If that environment variable is missing, many Yarn operations will generate an error. This happens even after any dependent packages have been downloaded. The easiest way to avoid this is to provide a default value of an empty string. This way, if the environment variable is missing, it will simply use an empty string instead of generating an error. You do this by using the ${VARIABLE_NAME:-default_value}
syntax. In this case, you want to use ${NODE_AUTH_TOKEN:-}
to pick up the value of the NODE_AUTH_TOKEN
environment variable, or an empty string if it’s not set.
The file will now include something that looks like this:
1npmScopes:
2 kenmuse:
3 npmRegistryServer: 'https://npm.pkg.github.com'
4 npmAuthToken: ${NODE_AUTH_TOKEN:-}
5 npmAlwaysAuth: false
The npmAlwaysAuth
entry is optional. If it is set to true
, Yarn will always send the authentication token when accessing the registry. This is not strictly required for GitHub Packages since the act of publishing will send the authentication token, so I wanted to show that it can be set to false
.
.npmignore
This isn’t technically specific to Yarn, but it is very important. In fact – please don’t publish without this! It’s so easy to include files by accident that you didn’t intend to include. This file works similarly to .gitignore
, allowing you to specify files and directories that should be excluded from the package when it is published. Without this file, everything in the project directory becomes fair game and might get bundled into the package.
I tend to use a deny-all approach, excluding everything by default and then adding back the items I want to include. This way, I can be sure that I’m only including what I intend to include. Here’s an example of what my .npmignore
file looks like:
1*
2!dist/**/*
This will exclude everything except for the dist
directory and its contents. This ensures that I don’t accidentally include any source files, configuration files, secrets, .env
, or other files into my package by accident.
Creating the GitHub Actions workflow
Now that the project is configured, you can create a GitHub workflow to build and publish the package. There are a few Actions steps that you’ll want to include, so let’s walk through those.
First, make sure that you configure actions/checkout
with persist-credentials set to false
. This prevents it from storing the workflow’s GitHub token into the repository’s local configuration. This adds one more level of security in case you accidentally capture the .git
directory in your package, especially if you forget to use a .npmignore
file.
1- name: Checkout code
2 uses: actions/checkout@v4 # For more security, prefer a SHA over a version tag!
3 with:
4 fetch-depth: 1
5 persist-credentials: false
Next, set up the version of Node.js you want to use. I like to explicitly set the registry-url
and scope
so that as it creates some of the configuration files, it has the correct values in those files. Note that the scope is my username prefixed with @
, just like in the package.json
file.
1- name: Setup Node.js
2 uses: actions/setup-node@v4
3 with:
4 node-version: '24'
5 registry-url: 'https://npm.pkg.github.com'
6 scope: '@kenmuse'
Some runners may not have corepack and Yarn configured by default, so I like to explicitly install it. This ensures that the correct version is installed and available for use.
1- name: Enable Corepack
2 run: corepack enable && yarn set version berry
3 env:
4 COREPACK_ENABLE_DOWNLOAD_PROMPT: 0
I’m using COREPACK_ENABLE_DOWNLOAD_PROMPT
to suppress the prompt that appears the first time you use Corepack to download a package manager. This is useful in a CI/CD environment where you want to avoid any interactive prompts that will break the build. If you want a more cross-platform
If you want to more explicitly ensure that you are only configuring the version in your package.json, you can use a command like this to dynamically retrieve the value, prepare the version of Yarn, and activate it:
1- run: |
2 YARN_VERSION=$(jq -r '.packageManager' package.json)
3 corepack prepare "${YARN_VERSION}" --activate
Finally, you need the actual publication step. It looks like this:
1- name: Publish to npm
2 run: |
3 yarn npm publish
4 env:
5 NODE_AUTH_TOKEN: ${{ github.token }}
This sets the environment variable NODE_AUTH_TOKEN
to the value of the GitHub token for the workflow. This token is automatically generated by GitHub as part of the workflow. Since we’ve configured .yarnrc.yml
to be able to use that value, it can handle the authentication. It just needs the necessary permissions to publish packages to GitHub Packages.
Permissions
Okay, so this isn’t its own file, but it is important to mention. For this to work, you will need some permissions configured for the workflow. I recommend breaking this into two parts.
First, before any of the jobs
, add permissions: {}
. This tells the workflow to start with no permissions. That means that if you forget to explicitly grant a permission to one of your jobs, it won’t have any permissions by default. That makes sure you are following the principle of least privilege and granting only the permissions that are absolutely necessary.
Next, grant the specific permissions you need for the publishing job:
1permissions:
2 contents: read
3 packages: write
Why do you need these?
contents: read
- This is needed to read the contents of the repository. This is required for the
actions/checkout
step to work.
packages: write
- This is needed to publish packages to GitHub Packages.
That’s it! With these files and configurations in place, you can run the workflow and publish your package to GitHub Packages.
Consuming packages with Yarn
To consume packages in Yarn, you need to make sure that your .yarnrc.yml
file is configured to use the correct registry and authentication token. This is similar to the configuration needed for publishing. If it’s a private package, you will need to provide an authentication token as well. We can reuse the same approach we used for publishing to make this easier.
It will look like this:
1npmScopes:
2 kenmuse:
3 npmAlwaysAuth: false
4 npmAuthToken: '${NODE_AUTH_TOKEN:-}'
5 npmRegistryServer: 'https://npm.pkg.github.com'
And that’s everything you need to know. As you can see, publishing npm packages to GitHub Packages using Yarn is a straightforward process once you have the necessary configurations in place. By following the steps outlined in this post, you can make it easy to automate the process from your workflows.