Skip to content

Instantly share code, notes, and snippets.

@bencat-sixense
Forked from belgattitude/ci-yarn-install.md
Created May 4, 2023 08:52
Show Gist options
  • Save bencat-sixense/952459295f08fc0d1eeac522a60ffbba to your computer and use it in GitHub Desktop.
Save bencat-sixense/952459295f08fc0d1eeac522a60ffbba to your computer and use it in GitHub Desktop.
Composite github action to improve CI time with yarn 3+ / node-modules linker.

Why

Although @setup/node as a built-in cache option, it lacks an opportunity regarding cache persistence. Depending on usage, the action below might give you faster installs and potentially reduce carbon emissions (β™»οΈπŸŒ³β€οΈ).

Requirements

Yarn 3+ with nodeLinker: node-modules. (Not using yarn ? see the corresponding pnpm 7/8+ action gist)

Bench

With cache: at least twice faster than without (see PS at the bottom for results)

Structure

.
└── .github
    β”œβ”€β”€ actions
    β”‚   └── yarn-nm-install/action.yml (composite action)    
    └── workflows
        └── ci.yml (uses: ./.github/actions/yarn-nm-install)    

Composite action

Create a file in .github/actions/yarn-nm-install/action.yml and paste

########################################################################################
# "yarn install" composite action for yarn 3/4+ and "nodeLinker: node-modules"         #
#--------------------------------------------------------------------------------------#
# Requirement: @setup/node should be run before                                        #
#                                                                                      #
# Usage in workflows steps:                                                            #
#                                                                                      #
#      - name: πŸ“₯ Monorepo install                                                     #
#        uses: ./.github/actions/yarn-nm-install                                       #
#        with:                                                                         #
#          enable-corepack: false # (default)                                          #
#          cache-install-state: false # (default)                                      #
#          cache-node-modules: false # (default)                                       #
#                                                                                      #
# Reference:                                                                           #
#   - latest: https://gist.github.com/belgattitude/042f9caf10d029badbde6cf9d43e400a    #
########################################################################################

name: 'Monorepo install (yarn)'
description: 'Run yarn install with node_modules linker and cache enabled'
inputs:
  enable-corepack:
    description: 'Enable corepack'
    required: false
    default: 'false'
  cache-node-modules:
    description: 'Cache node_modules, might speed up link step (invalidated lock/os/node-version/branch)'
    required: false
    default: 'false'
  cache-install-state:
    description: 'Cache yarn install state, might speed up resolution step when node-modules cache is activated (invalidated lock/os/node-version/branch)'
    required: false
    default: 'false'


runs:
  using: 'composite'

  steps:
    - name: βš™οΈ Enable Corepack
      if: ${{ inputs.enable-corepack }} == 'true'
      shell: bash
      run: corepack enable

    - name: βš™οΈ Expose yarn config as "$GITHUB_OUTPUT"
      id: yarn-config
      shell: bash
      env:
        YARN_ENABLE_GLOBAL_CACHE: "false"
      run: |
        echo "CACHE_FOLDER=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
        echo "CURRENT_NODE_VERSION="node-$(node --version | cut -d . -f 1  | sed 's/[^0-9]*//g')".x" >> $GITHUB_OUTPUT
        echo "CURRENT_BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's,/,-,g')" >> $GITHUB_OUTPUT

    - name: ♻️ Restore yarn cache
      uses: actions/cache@v3
      id: yarn-download-cache
      with:
        path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
        key: yarn-download-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
        restore-keys: |
          yarn-download-cache-

    - name: ♻️ Restore node_modules
      if: inputs.cache-node-modules == 'true'
      id: yarn-nm-cache
      uses: actions/cache@v3
      with:
        path: '**/node_modules'
        key: yarn-nm-cache-${{ runner.os }}-${{ steps.yarn-config.outputs.CURRENT_NODE_VERSION }}-${{ steps.yarn-config.outputs.CURRENT_BRANCH }}-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}

    - name: ♻️ Restore yarn install state
      if: inputs.cache-install-state == 'true' && inputs.cache-node-modules == 'true'
      id: yarn-install-state-cache
      uses: actions/cache@v3
      with:
        path: .yarn/ci-cache
        key: yarn-install-state-cache-${{ runner.os }}-${{ steps.yarn-config.outputs.CURRENT_NODE_VERSION }}-${{ steps.yarn-config.outputs.CURRENT_BRANCH }}-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}

    - name: πŸ“₯ Install dependencies
      shell: bash
      run: yarn install --immutable --inline-builds
      env:
        # Overrides/align yarnrc.yml options (v3, v4) for a CI context
        YARN_ENABLE_GLOBAL_CACHE: "false" # Use local cache folder to keep downloaded archives
        YARN_NM_MODE: "hardlinks-local"   # Reduce node_modules size
        YARN_INSTALL_STATE_PATH: ".yarn/ci-cache/install-state.gz" # Might speed up resolution step when node_modules present
        # Other environment variables
        HUSKY: '0' # By default do not run HUSKY install

Workflow action

To use it in the workflows

    steps:
      - uses: actions/checkout@v3

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: πŸ“₯ Monorepo install
        uses: ./.github/actions/yarn-nm-install

The action accepts few parameters, for example if saving node_modules is what you're looking for: set the cache-node-modules and cache-install-state to true. This option is really worth in complex workflows - install is fully shared between jobs at the cost of @action/cache compression which is lighter in general)

      - name: πŸ“₯ Monorepo install
        uses: ./.github/actions/yarn-nm-install
        with:
          enable-corepack: false 
          cache-install-state: true 
          cache-node-modules: true 

Caution cache-node_modules is false by default, but you might try both options on your specific repo. It's a balance between cache persitence/restoration speed/size and install time. There's no one rule that fits all. In general if you want to reuse between multiple steps, cache-node-modules to true is ideal. But be warned in some situations (prisma postinstall generation...) there might be issues.

yarnrc.yml

# .yarnrc.yml
nodeLinker: node-modules
# This line can be omiited with corepack enabled 
# in this case set the packageManager field in package.json
yarnPath: .yarn/releases/yarn-4.0.0-rc.43.cjs # or 3.5.1 (rc are really stable imho)...

Results

On install, when only few deps changed

image

Cost of action/cache compression

image

Cleanup caches

When a PR is closed or merged the best is to remove install cache rather than letting github reach the max (10GB) and prune.

image

Link: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries

Here's an example (feel free to adapt if you need to preserse some things, ie gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 | grep yarn will only clear yarn related caches)

# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
name: Cleanup caches for closed branches

on:
  pull_request:
    types:
      - closed
  workflow_dispatch:

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v3

      - name: Cleanup
        run: |
          gh extension install actions/gh-actions-cache

          REPO=${{ github.repository }}
          BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"

          echo "Fetching list of cache key"
          cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )

          ## Setting this to not fail the workflow while deleting cache keys. 
          set +e
          echo "Deleting caches..."
          for cacheKey in $cacheKeysForPR
          do
              gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
          done
          echo "Done"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Bench details

Based on the nextjs-monorepo-example and the results of https://github.com/belgattitude/compare-package-managers it's twice fast. Depending on repo (renovatebot...), the slight complexity increase in ci setup might worth it. Example based on yarn 4.0.0-rc.36 with most post install scripts disabled and supportedArchitecture: current.

CACHE YARN_COMPRESSION_LEVEL=mixed (default) YARN_COMPRESSION_LEVEL=0
COLD 1m38s 44s
FULL WARM 54s 18s

PS: YARN_COMPRESSION_LEVEL=0 disable zip compression, on the ci it creates 2-3 more extra seconds (github will (un-)zstd it), in your local install you'll have to deal with a bigger cache (it's a choice you need to do for all as it changes the yarn.lock md5 checksums)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment