113

I recently hooked up my project with github actions for continuous integration. I created two separate jobs: the first one checks if the code in the pull request is accepted by our linter, and the second one checks if the code passes the test suite. I like that having two jobs like this shows up as two separate checkmarks in the Github webpage for the pull request:

enter image description here

The problem I'm having now is that there is some duplicated code in workflow YAML file: the first 3 steps, which install Lua and Luarocks. Not only is it annoying to maintain, but it also wastes CI minutes by running the same actions twice. Is there a way to avoid this? So that the setup code is only written in one place, and only runs once when the workflow executes?

But I am confused what would be the proper way to proceed:

  1. Should I create my own Github Action with the shared setup code?
  2. Should I create a Docker image that already has Lua and Luarocks pre-installed?
  3. Should I use a single job? Can I still have independent checkmarks for the linter and the test suite if they are steps of the same job?
  4. Something else?

Here is the current YAML file for my workflow:

name: Github Actions CI

on: [ pull_request ]

jobs:
    lint:
        name: Lint
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v2
            - uses: leafo/gh-actions-lua@v8.0.0
            - uses: leafo/gh-actions-luarocks@v4.0.0

            - run: luarocks install luacheck
            - run: ./run-linter.sh

    test:
        name: Test
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v2
            - uses: leafo/gh-actions-lua@v8.0.0
            - uses: leafo/gh-actions-luarocks@v4.0.0

            - run: luarocks install busted
            - run: ./build-project.sh
            - run: ./run-test-suite.sh

I tried searching for similar questions but couldn't find anything that exactly answered my question:

  1. Caching APT packages in GitHub Actions workflow: I can't use this solution because I don't have a way to precisely specify all the versions of all the dependencies that I am using, so that they may be cached. I also don't mind if separate runs of the workflow are not cached. I'm more worried about the code duplication.
  2. Github actions share workspace/artifacts between jobs? I don't want to have to manage uploading uploading artifacts to a separate service and then deleting them afterwards.
  3. Reuse portion of github action across jobs: In that question the only difference between the jobs is a single variable, so accepted answer is to use a build matrix. But I don't think a build matrix would work as well in my case, where only the setup code is the same?
hugomg
  • 68,213
  • 24
  • 160
  • 246
  • Does this answer your question? [Github actions share workspace/artifacts between jobs?](https://stackoverflow.com/questions/57498605/github-actions-share-workspace-artifacts-between-jobs) – gcode Apr 08 '23 at 21:53

3 Answers3

44

As of today (August 2021) composite action is no longer limited to run. GitHub Actions: Reduce duplication with action composition

name: "Publish to Docker"
description: "Pushes built artifacts to Docker"

inputs:
  registry_username:
    description: “Username for image registry”
    required: true
  registry_password:
    description: “Password for image registry”
    required: true

runs:
  using: "composite"
  steps:
      - uses: docker/setup-buildx-action@v1

      - uses: docker/login-action@v1
        with:
          username: ${{inputs.registry_username}}
          password: ${{inputs.registry_password}}

      - uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: user/app:latest

Old Answer

What you are looking for is composite action which help you reuse once defined set of steps.

jobs:
    lint:
        name: Lint
        runs-on: ubuntu-latest
        steps:
            - uses: octocat/say-hello@v1

            - run: luarocks install luacheck
            - run: ./run-linter.sh

    test:
        name: Test
        runs-on: ubuntu-latest
        steps:
            - uses: octocat/say-hello@v1

            - run: luarocks install busted
            - run: ./build-project.sh
            - run: ./run-test-suite.sh

octocat/say-hello/action.yml:

runs:
  using: "composite"
  steps: 
    - run: echo "Nice to meet you!"
      shell: pwsh

For more details you can also check's ADR here.

And why you can't simply run it once for all jobs, because each job may run on a different machine.

A job is a set of steps that execute on the same runner. By default, a workflow with multiple jobs will run those jobs in parallel. You can also configure a workflow to run jobs sequentially. For example, a workflow can have two sequential jobs that build and test code, where the test job is dependent on the status of the build job. If the build job fails, the test job will not run.

Krzysztof Madej
  • 32,704
  • 10
  • 78
  • 107
  • @AJNinja this is now supported! – Krzysztof Madej Aug 26 '21 at 15:55
  • Can you define this composite action in the same repo that will use it? For example I have 4 jobs that run when a PR is opened in a repo, but they all need the same series of "setup" steps. All examples I see are where the action lives in a separate repo – TemporaryFix Jan 10 '23 at 23:16
  • 1
    Yes. Of course, you can have composite action in the same repo. Take a look here: https://github.com/kmadof/github-actions-manual/blob/main/.github/actions/say-hello/action.yml – Krzysztof Madej Jan 11 '23 at 11:20
16

There are 3 main approaches for code reusing in GitHub Actions:

There is an article describing their pros and cons.

In your case if the duplicated steps are in the single workflow you also can:

  1. extract them to the "preparation" job
  2. upload build artifacts
  3. add "preparation" job to "needs" key of both jobs
  4. download build artifact in both jobs
Cardinal
  • 1,321
  • 1
  • 11
  • 5
  • I tried using the upload artifacts route but it is horrendously slow. Granted I was also uploading the repo I checkout out with `node_modules` included. But, I don't think I should have to run checkout and `npm i` for each job that needs it. That's unnecessary duplication. – TemporaryFix Jan 10 '23 at 23:19
  • 1. You can [cache](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) node dependencies 2. You can extract repeated steps to the [Composite Action](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action) that runs on the same worker in the context of your job, sharing the file system – Cardinal Jan 12 '23 at 16:40
  • 3
    Yes, but composite actions just allow you to group common tasks. Each job still needs to do those common tasks which is redundant. There should be a way to have 1 job that all other jobs rely on through the `needs` keyword that does what should be 1 time setup work – TemporaryFix Jan 12 '23 at 16:59
-4

The below code snippet should do the work for you. Instead of creating two jobs, one for lint and another for test, you can use one job and create multiple tasks

name: Github Actions CI

on: [ pull_request ]

jobs:
    lint:
        name: Lint and Test
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v2
            - uses: leafo/gh-actions-lua@v8.0.0
            - uses: leafo/gh-actions-luarocks@v4.0.0

            - name: Install Luacheck
              run: luarocks install luacheck

            - name: Run Lint Check
            - run: ./run-linter.sh

            - name: Install Busted
              run: luarocks install busted

            - name: Build Project
              run: ./build-project.sh

            - name: Run Test Suite
            - run: ./run-test-suite.sh

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
Sam-Sundar
  • 511
  • 1
  • 4
  • 12
  • 1
    This doesn't allow the linter and unit tests to run in parallel, and it doesn't surface unit test failures until after lint failures are resolved. – Charles Stover Aug 24 '21 at 19:31