Orchestrating Yarn in GitHub Actions: Corepack, Berry, and Caching Strategies

The integration of Yarn into GitHub Actions workflows has evolved significantly, shifting from simple command substitution within legacy npm actions to sophisticated, version-pinned installations via Corepack and dedicated Berry actions. For modern Node.js projects, particularly those utilizing Yarn 2+ (commonly referred to as "Yarn Berry"), the configuration requires a precise alignment of Node.js versions, package manager specifications, and caching strategies to ensure reliable, high-performance continuous integration. Understanding the nuances between classic Yarn, modern Yarn Berry, and the environment constraints of self-hosted versus GitHub-hosted runners is critical for maintaining robust CI/CD pipelines.

The Legacy Approach and Early Workflows

In earlier iterations of GitHub Actions, the ecosystem relied heavily on the actions/npm action for package management, regardless of whether the underlying tool was npm or Yarn. This was possible because the -slim Docker images provided by GitHub included Yarn by default. Consequently, developers could pivot from npm to Yarn simply by altering the execution command within the same action wrapper.

The syntax for installing dependencies using npm was straightforward, utilizing the actions/[email protected] action with the install argument. To switch to Yarn, users introduced a runs parameter set to yarn, while keeping the core action reference identical. This allowed for a seamless transition where the action handled the environment setup, and the specific package manager command was passed as an argument.

```yaml

Legacy Yarn Install via actions/npm

action "install" {
uses = "actions/[email protected]"
runs = "yarn"
args = "install"
}
```

This pattern extended to building, testing, and filtering tags within a workflow. A typical legacy workflow might define a sequence of actions: install, build, test, check for new tags, and finally publish. Each step explicitly called actions/[email protected] with runs = "yarn" and the appropriate argument, such as build or test. The publish step, however, often reverted to npm, requiring authentication secrets like NPM_AUTH_TOKEN. This approach was "delightfully simple" for its time but lacked the version specificity and modern performance optimizations that contemporary workflows demand.

```yaml
workflow "build, test and publish on release" {
on = "push"
resolves = "publish"
}

action "install" {
uses = "actions/[email protected]"
runs = "yarn"
args = "install"
}

action "build" {
needs = "install"
uses = "actions/[email protected]"
runs = "yarn"
args = "build"
}

action "test" {
needs = "build"
uses = "actions/[email protected]"
runs = "yarn"
args = "test"
}

action "check for new tag" {
needs = "Test"
uses = "actions/bin/filter@master"
args = "tag"
}

action "publish" {
needs = "check for new tag"
uses = "actions/[email protected]"
args = "publish"
secrets = ["NPMAUTHTOKEN"]
}
```

Transitioning to Yarn Modern (Berry)

The introduction of Yarn 2+, known as "Berry," fundamentally changed how package managers are managed in CI environments. Yarn Berry introduces a more robust, zero-install architecture and requires specific configuration to function correctly. Migrating a project to Yarn Berry involves upgrading the local environment and ensuring compatibility with Node.js 18 or higher.

The migration process begins with enabling Corepack, the official Node.js package manager manager. Corepack ensures that the exact version of Yarn specified in the project is used, preventing version drift across different environments. The steps for local migration include running corepack enable to activate the feature, followed by yarn set version stable to install the latest stable release of Yarn Berry. A final yarn install migrates the lockfile to the new format.

bash corepack enable yarn set version stable yarn install

A critical configuration detail for projects transitioning from the classic node_modules format is the .yarnrc.yml file. To maintain compatibility with existing tooling or preferences, developers can specify nodeLinker: node-modules in this configuration file. This instruction tells Yarn Berry to generate a traditional node_modules directory rather than using its default Plug'n'Play (PnP) file system, which can complicate integration with certain build tools or plugins like Storybook.

Upon completion of the migration, the package.json file is updated to include a packageManager field. This field explicitly states the required Yarn version, such as "[email protected]". This declaration is vital for Corepack to function, as it reads this field to install the correct Yarn version automatically when the project is cloned.

Configuring GitHub Actions for Yarn Berry

With Yarn Berry, relying on pre-installed global Yarn binaries is no longer sufficient or recommended. Instead, developers should utilize dedicated actions that leverage Corepack to set up the specific Yarn version defined in package.json. The setup-yarn-berry action, available on the GitHub Marketplace, is designed explicitly for this purpose. It sets up Yarn to a specified version and installs dependencies with cache support, significantly speeding up subsequent workflow runs.

This action supports Yarn 2+ exclusively. If a project still uses classic Yarn, migration is strongly suggested due to the enhanced security and performance features of Berry. The action accepts two primary input parameters: version and cache.

Parameter Type Description
version String Specifies the Yarn version to set up. Can be a tag (e.g., stable), a semver range (e.g., 4.x), or a specific version (e.g., 4.1.0). If omitted, it uses the default version.
cache Boolean Enables caching during Yarn installation. Defaults to true.

Using this action ensures that the CI environment matches the local development environment precisely, adhering to the version specified in package.json. This eliminates the "works on my machine" discrepancies caused by global Yarn version mismatches.

yaml - name: Setup Yarn Berry uses: threeal/setup-yarn-action@v1 with: version: 'stable' cache: true

Caching and Performance Optimization

One of the most significant bottlenecks in CI pipelines is the installation of dependencies. Without caching, every workflow run downloads and installs all dependencies from scratch, leading to longer build times and increased resource consumption. GitHub Actions provides mechanisms to cache dependencies, but the implementation varies depending on the Node.js setup.

The standard actions/setup-node action can handle caching for npm packages, but its behavior with Yarn requires careful configuration. For simple setups, developers often use actions/setup-node with a specified Node.js version, followed by a yarn install --frozen-lockfile command. The --frozen-lockfile flag is crucial; it ensures that the installation fails if the lockfile needs to be updated, preventing silent drift in dependency versions during CI. This flag only matters if a package was added to package.json locally without updating the lockfile, which would otherwise cause the CI to install packages and modify the lockfile without saving the changes.

```yaml
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '16.x'

  • name: Install dependencies
    run: yarn install --frozen-lockfile
    ```

For more complex workflows, such as those deploying to GitHub Pages, caching becomes even more critical. A typical deployment workflow might involve a matrix strategy to test across multiple Node.js versions (e.g., 8.x, 10.x, 12.x) before deploying. In such cases, the actions/checkout step is followed by actions/setup-node and then yarn install. The test step often sets the CI environment variable to true, which informs Yarn and other tools that they are running in a continuous integration environment, potentially altering their behavior for stricter validation.

yaml jobs: install-and-test: runs-on: ubuntu-latest strategy: matrix: node-version: [8.x, 10.x, 12.x] steps: - uses: actions/checkout@master - name: Set up Node.js ${{ matrix.node-version }} uses: actions/setup-node@master with: node-version: ${{ matrix.node-version }} - name: Install packages run: yarn install --frozen-lockfile - name: Test run: yarn test env: CI: true

Self-Hosted Runners and Environment Constraints

A common point of failure arises when developers migrate from GitHub-hosted runners to self-hosted runners. GitHub-hosted runners come with Yarn pre-installed in their base images (such as Ubuntu 20.04), ensuring that yarn commands work out of the box. However, self-hosted runners, such as those on AWS EC2 instances, often do not have Yarn installed by default.

When using the actions/setup-node action on a self-hosted runner, it installs Node.js and npm but does not install Yarn. Consequently, subsequent steps attempting to run yarn install --ignore-scripts --frozen-lockfile fail with a "command not found" error, resulting in an exit code 127. This discrepancy highlights the importance of explicitly installing Yarn in the workflow for self-hosted environments.

```bash
Run yarn install --ignore-scripts --frozen-lockfile
/actions-runner/work/temp/58e53b82-f9ae-4c68-9cec-75f75831208b.sh: line 1: yarn: command not found

[error]Process completed with exit code 127.

```

To resolve this, developers must either install Yarn manually in the runner environment or use an action like setup-yarn-berry that handles the installation regardless of the runner type. The setup-node action alone is insufficient for Yarn-based projects on bare-metal or cloud VM runners that lack pre-bundled package managers.

Deployment to GitHub Pages

Integrating Yarn with GitHub Actions for deployment to GitHub Pages requires specific configuration in package.json. The homepage field must be set to the correct URL, such as https://MichaelCurrin.github.io/my-app/, to ensure that the build tools (like React) infer the correct base path for assets.

The deployment workflow typically involves a predeploy script that triggers the build process, followed by a deploy script that uses the gh-pages NPM package to push the build output to the gh-pages branch. The gh-pages package is added as a development dependency via yarn add --dev gh-pages.

json { "scripts": { "build": "...", "predeploy": "yarn build", "deploy": "gh-pages -d build" }, "homepage": "https://MichaelCurrin.github.io/my-app/" }

In the GitHub Actions workflow, the deployment job depends on the completion of the test job. It installs dependencies, runs the deploy script, and uses the GITHUB_TOKEN secret for authentication. This token is automatically provided by GitHub Actions, eliminating the need for manual token generation and storage.

yaml jobs: build-deploy: needs: test runs-on: ubuntu-latest steps: - name: Install packages run: yarn install --frozen-lockfile - name: Deploy GitHub Pages run: yarn deploy env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Conclusion

The management of Yarn in GitHub Actions has transitioned from a simplistic command-line override to a robust, version-controlled process driven by Corepack and specialized actions. For modern projects using Yarn Berry, the explicit setup of Yarn via actions like setup-yarn-berry is essential to ensure consistency between local development and CI environments. Developers must be mindful of the differences between GitHub-hosted and self-hosted runners, particularly regarding pre-installed tools, and leverage caching mechanisms to optimize build times. By adhering to best practices such as using --frozen-lockfile and correctly configuring packageManager in package.json, teams can achieve reliable, efficient, and maintainable continuous integration workflows.

Sources

  1. GitHub Actions and Yarn
  2. Setup Yarn Berry Action
  3. Setup Yarn Berry Action (Marketplace)
  4. Yarn Modern (2+) and GitHub Actions
  5. Code Cookbook: Yarn in GitHub Actions
  6. Setup Node Issue #182

Related Posts