I was creating my first GitHub Action (it is now working, which you can check out here). I am familiar with JavaScript, so I chose to create a JavaScript action. According to the official documentation, I need to do one of the following to make my JavaScript code correctly import 3rd-party modules:

  • commit the node_modules to the repo, or
  • use @vercel/ncc to bundle everything into a single script.

I think both of these methods are not good practices. Especially, for my case, the way with @vercel/ncc is not feasible because some package uses require.

The best way to handle this is to make GitHub cache and install all the dependencies for the user every time the action is run. This can be made possible by creating a composite action. Here is a snippet in action.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
runs:
  using: composite
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20'

    - name: Get cache key and path
      id: lock
      run: |
        cd $GITHUB_ACTION_PATH
        node <<'JAVASCRIPT'
          const fs = require('fs');
          const os = require('os');
          const sha256 = require('crypto').createHash('sha256');
          const hash = sha256.update(fs.readFileSync('package-lock.json')).digest('hex');
          fs.appendFileSync(process.env.GITHUB_OUTPUT, `key=${os.platform()}-${os.arch()}-grs-modules-${hash}\n`);
        JAVASCRIPT
        modules=$(pwd)/node_modules
        command -v cygpath &> /dev/null && modules=$(cygpath -w $modules)
        echo "modules=$modules" >> $GITHUB_OUTPUT
        cat $GITHUB_OUTPUT
        cd $GITHUB_WORKSPACE
      shell: bash

    - name: Restore cache
      uses: actions/cache/restore@v4
      id: cache-restore
      with:
        path: ${{ steps.lock.outputs.modules }}
        key: ${{ steps.lock.outputs.key }}

    - name: CI
      if: steps.cache-restore.outputs.cache-hit != 'true'
      run: |
        cd $GITHUB_ACTION_PATH
        npm ci
        cd $GITHUB_WORKSPACE
      shell: bash

    - name: Save cache
      if: steps.cache-restore.outputs.cache-hit != 'true'
      uses: actions/cache/save@v4
      with:
        path: ${{ steps.lock.outputs.modules }}
        key: ${{ steps.lock.outputs.key }}

    - name: Run
      run: node $GITHUB_ACTION_PATH/index.js
      shell: bash

I manually use actions/cache to cache the node_modules directory although actions/setup-node has caching functionality. This is because it is not good yet. Also, because github.action_path ends with /./, it can cause some problem due to the relative pathing. My workflow here works around those problems.