The integration of Node.js package managers into Continuous Integration (CI) pipelines requires precise configuration to ensure reproducibility, speed, and security. GitHub Actions has evolved from simple script execution to a sophisticated orchestration environment where the method of installing and running Yarn has shifted significantly over time. While early workflows relied on Docker images that happened to include Yarn, modern best practices dictate the use of the Setup Yarn Berry Action or Corepack for Yarn 2+ (Berry). Understanding the nuances between classic Yarn and Yarn Berry, managing dependency caching, and addressing the specific needs of self-hosted runners are critical for maintaining robust development workflows.
The Shift to Yarn Berry and Dedicated Actions
The landscape of Yarn in CI has bifurcated based on the version of the package manager. Yarn Berry, encompassing versions 2 and above, introduces significant architectural changes compared to the classic version, including a new plugin architecture and different directory structures. Consequently, generic setup actions often fail to handle Berry correctly. The Setup Yarn Berry Action is a specialized GitHub action designed explicitly to set up the Yarn package manager in GitHub workflows for Node.js projects. This action sets up Yarn to a specified version and installs dependencies for the current Node.js project with cache support. Cache support provides fast setup for Node.js projects by using dependencies installed from previous runs, significantly reducing build times.
This action currently only supports the berry version of Yarn (Yarn 2+). If a project still uses the classic version of Yarn, it is suggested to migrate to the berry version, with migration guides available from the Yarn documentation. The Setup Yarn Berry Action offers two key features: setting up Yarn to a specified version and installing dependencies for the current Node.js project with cache support. These features are controlled through specific input parameters that allow developers to fine-tune their CI environment.
The available input parameters for the Setup Yarn Berry Action include:
- version: A string that specifies the version of Yarn to set up using this action. The specified version can be a tag (e.g.,
stable), a semver range (e.g.,4.x), or a semver version (e.g.,4.1.0). If not specified, it uses the default Yarn version. - cache: A boolean that indicates whether to enable caching during Yarn installation. It defaults to
true.
By leveraging these parameters, developers can ensure that their CI environment uses the exact version of Yarn defined in their project, preventing discrepancies between local development and automated testing. The caching mechanism is particularly vital for large projects, as it stores the installed dependencies and reuses them in subsequent runs, bypassing the need to download and install packages from scratch every time.
Corepack and Yarn Modern Configuration
For projects migrating to Yarn Modern (Yarn 2+), the setup process extends beyond just installing the package manager; it involves configuring the Node.js environment to recognize and prioritize Yarn. A common approach involves enabling Corepack, a tool bundled with Node.js that allows for the management of package manager versions. The process begins by ensuring the use of Node 18 or higher, as Corepack requires a modern Node runtime. Once the environment is ready, developers run corepack enable to activate Corepack. Following activation, the command yarn set version stable is executed to use the latest stable version of Yarn. Finally, running yarn install migrates the lockfile to the new format.
When upgrading from a project that used node_modules, it is often simpler to continue using that same format to avoid complex migration issues. This can be achieved by adding specific configuration to the .yarnrc.yml file. For example:
```yaml
.yarnrc.yml
nodeLinker: node-modules
```
With this configuration in place, the package.json file gains a new entry to specify that the project uses a specific version of Yarn, such as:
json
"packageManager": "[email protected]"
This explicit declaration ensures that Corepack installs the correct version of Yarn during the CI process, aligning the runtime environment with the project's requirements. This method is particularly effective when combined with the actions/setup-node action, which can be configured to use Corepack.
Handling Self-Hosted Runners and Legacy Approaches
Not all CI environments rely on GitHub-hosted runners. Developers using self-hosted runners often encounter issues where Yarn is not pre-installed. On GitHub-hosted runners, Yarn is typically pre-installed, as documented in the virtual environment specifications. However, on self-hosted instances, such as an EC2 instance, the absence of Yarn leads to failures. A common error observed in these scenarios is:
```
/actions-runner/work/temp/58e53b82-f9ae-4c68-9cec-75f75831208b.sh: line 1: yarn: command not found
[error]Process completed with exit code 127.
```
This error occurs when a workflow attempts to run yarn install --ignore-scripts --frozen-lockfile after setting up Node.js via the setup-node action. While setup-node installs Node and npm, it does not inherently install Yarn on self-hosted runners unless explicitly configured or if Yarn is present in the system path. This discrepancy highlights the importance of explicitly installing Yarn or using the Setup Yarn Berry Action in self-hosted environments to ensure consistency.
In contrast, earlier approaches to using Yarn in GitHub Actions relied on the fact that many Docker images, such as node:10-slim, came with Yarn included. This allowed developers to use the actions/npm action to run Yarn commands by overriding the runner. For example:
yaml
action "install" {
uses = "actions/[email protected]"
runs = "yarn"
args = "install"
}
This method was a workaround that exploited the presence of Yarn in the underlying Docker image. A complete workflow using this approach might look like:
```yaml
workflow "build, test and publish on release" {
on = "push"
resolves = "publish"
}
install with yarn
action "install" {
uses = "actions/[email protected]"
runs = "yarn"
args = "install"
}
build with yarn
action "build" {
needs = "install"
uses = "actions/[email protected]"
runs = "yarn"
args = "build"
}
test with yarn
action "test" {
needs = "build"
uses = "actions/[email protected]"
runs = "yarn"
args = "test"
}
filter for a new tag
action "check for new tag" {
needs = "Test"
uses = "actions/bin/filter@master"
args = "tag"
}
publish with npm
action "publish" {
needs = "check for new tag"
uses = "actions/[email protected]"
args = "publish"
secrets = ["NPMAUTHTOKEN"]
}
```
While this method worked for older versions of Yarn and Node, it is no longer the recommended approach for modern Yarn Berry projects due to the lack of explicit version control and caching support.
Caching and Lockfile Integrity
Regardless of the installation method, maintaining lockfile integrity and utilizing caching are essential for CI efficiency and reliability. When using the standard actions/setup-node action with Yarn, developers often use the --frozen-lockfile flag during installation:
yaml
steps:
- name: Set up Node.js ⚙️
uses: actions/setup-node@v2
with:
node-version: '16.x'
- name: Install dependencies 📦
run: yarn install --frozen-lockfile
The --frozen-lockfile flag prevents changes to the lockfile. This is crucial because if a package is added to package.json locally without updating the lockfile, a CI install might install packages and change the lockfile each time, without saving it. This can lead to inconsistent builds and hidden dependencies. By enforcing a frozen lockfile, developers ensure that the CI environment installs exactly the same versions of dependencies as specified in the lockfile, mirroring the local development environment.
However, this simple approach does not handle caching of dependencies. Without caching, every CI run must download and install all dependencies from the network, leading to slow build times. The Setup Yarn Berry Action addresses this by including built-in cache support, which can be enabled or disabled via the cache parameter. This caching mechanism stores the installed dependencies and reuses them in subsequent runs, significantly reducing build times and network usage.
For projects using GitHub Pages, additional configuration may be required. The homepage field in package.json should be set to the correct subpath to ensure that the site is served correctly, as React infers this value:
json
{
"scripts": {
// ...
},
"homepage": "https://MichaelCurrin.github.io/my-app/"
}
This configuration ensures that the CI build generates the correct paths for assets and links, preventing broken resources when the site is deployed.
Conclusion
The evolution of Yarn in GitHub Actions reflects the broader trend towards more explicit, reproducible, and efficient CI/CD pipelines. While early methods relied on implicit dependencies in Docker images, modern workflows demand precise control over package manager versions and caching. The Setup Yarn Berry Action provides a robust solution for Yarn 2+ projects, offering version specification and built-in caching to optimize build times. For self-hosted runners, explicit installation of Yarn is necessary to avoid command-not-found errors, highlighting the importance of environment-specific configuration. By leveraging Corepack, enforcing frozen lockfiles, and utilizing dedicated actions, developers can ensure that their CI pipelines are both reliable and efficient, regardless of the underlying infrastructure.