100

I want to exit a job if a specific condition is met:

jobs:
  foo:
    steps:
      ...
      - name: Early exit
        run: exit_with_success # I want to know what command I should write here
        if: true
      - run: foo
      - run: ...
 ...

How can I do this?

1

4 Answers 4

79
+25

There is currently no way to exit a job arbitrarily, but there is a way to allow skipping subsequent steps if an earlier step failed, by using conditionals:

jobs:
  foo:
    steps:
      ...
      - name: Early exit
        run: exit_with_success # I want to know what command I should write here
      - if: failure()
        run: foo
      - if: failure()
        run: ...
 ...

The idea is that if the first step fails, then the rest will run, but if the first step doesn't fail the rest will not run.

However, it comes with the caveat that if any of the subsequent steps fail, the steps following them will still run, which may or may not be desirable.


Another option is to use step outputs to indicate failure or success:

jobs:
  foo:
    steps:
      ...
      - id: s1
        name: Early exit
        run: # exit_with_success
      - id: s2
        if: steps.s1.conclusion == 'failure'
        run: foo
      - id: s3
        if: steps.s2.conclusion == 'success'
        run: ...
 ...

This method works pretty well and gives you very granular control over which steps are allowed to run and when, however it became very verbose with all the conditions you need.


Yet another option is to have two jobs:

  • one which checks your condition
  • another which depends on it:
jobs:
  check:
    outputs:
      status: ${{ steps.early.conclusion }}
    steps:
      - id: early
        name: Early exit
        run: # exit_with_success
  work:
    needs: check
    if: needs.check.outputs.status == 'success'
    steps:
      - run: foo
      - run: ...
 ...

This last method works very well by moving the check into a separate job and having another job wait and check the status. However, if you have more jobs, then you have to repeat the same check in each one. This is not too bad as compared to doing a check in each step.


Note: In the last example, you can have the check job depend on the outputs of multiple steps by using the object filter syntax, then use the contains function in further jobs to ensure none of the steps failed:

jobs:
  check:
    outputs:
      status: ${{ join(steps.*.conclusion) }}
    steps:
      - id: early
        name: Early exit
        run: # exit_with_success
      - id: more_steps
        name: Mooorreee
        run: # exit_maybe_with_success
  work:
    needs: check
    if: !contains(needs.check.outputs.status, 'failure')
    steps:
      - run: foo
      - run: ...

Furthermore, keep in mind that "failure" and "success" are not the only conclusions available from a step. See steps.<step id>.conclusion for other possible reasons.

5
  • I tried with object filter syntax. However, I'm using strategy matrix and your code with wildcard doesn't work. I found nothing related this in the doc. Do you know if anything is different for strategy matrix? For reference I'm getting this error Error: Fail to evaluate job outputs Error: The template is not valid. .github/workflows/build.yml (Line: 54, Col: 19): A sequence was not expected. Commented Jun 8, 2021 at 18:38
  • 1
    @It'sK that kinda makes sense because steps.*.conclusion will evaluate to a list of conclusions, whereas the docs state that output must be a string. Try changing it to join(steps.*.conclusion), and see if that works for you. Let me know so that I can properly update the answer Commented Jun 8, 2021 at 18:44
  • I've tested but strangly the output of multiple steps with join(steps.*.conclusion) result into just only success. Not getting conclusion of every step. FYI one step succeeds, one fails and one is skipped. Commented Jun 8, 2021 at 19:10
  • @It'sK that sounds like a bug. Please report it if you can Commented Jun 8, 2021 at 20:59
  • 6
    These are all creative but inconvenient workarounds for a feature Github should simply provide by now. The ticket is here, please upvote: github.com/actions/runner/issues/662 Commented Jul 6, 2022 at 3:36
37

The exit behavior can be achieved with gh run cancel and gh run watch commands:

- name: Early exit
  run: |
    gh run cancel ${{ github.run_id }}
    gh run watch ${{ github.run_id }}
  env:
    GH_TOKEN: ${{ secrets. GITHUB_TOKEN }}

The watch is required since cancellation will not abort immediately.

You may need actions: 'write' permission added for the job, something like:

permissions:
  ...
  actions: 'write'
9
  • 3
    Exactly what I was looking for to skip a step/job and also mark it as skipped, not success or failed. +1 Commented Apr 17, 2023 at 2:49
  • 11
    It cancels the whole workflow. And mark the whole workflow as cancelled Commented Sep 13, 2023 at 8:47
  • 3
    Thank you for this workaround. Not ideally because it marks workflow as canceled and sends a notification email which we cannot suppress AFAIK. Commented Sep 26, 2023 at 10:58
  • 5
    is there a way to mark the workflow as a success after the cancellation Commented Oct 29, 2023 at 3:36
  • 1
    @SDIDSA not easily as far as I know. You could try experimenting with docs.github.com/en/actions/using-workflows/… Commented Oct 29, 2023 at 16:25
7

If the other posted answers did not solve your problem then, you could consider creating a job that determines which jobs should be run and which should be skipped. This solution makes use of job outputs.
Here is an example:

jobs:
  planner:
    name: Determine which jobs to run
    runs-on: ubuntu-latest
    # To keep it simple name the step and output the same as job
    outputs:
      foo: ${{ steps.foo.outputs.should-run }}

    steps:
      # Checkout if necessary to determine whether 'foo' needs to run
      # - uses: actions/checkout@v4

      - name: Mark foo job as 'to be run'
        id: foo
        # Replace 'true' with your condition
        run: echo "should-run=true" >> $GITHUB_OUTPUT

  foo:
    runs-on: ubuntu-latest
    needs: planner
    # Skip this job when condition not met
    if: needs.planner.outputs.foo == 'true'

    steps:
      - uses: actions/checkout@v4
      # ...

An example use case for this approach would be when you have an expensive job which can be skipped if the output is the same. For example running nightly tests against latest tag, but latest tag has not changed.

3

I just did this by running exit 1

job:
  runs-on: ubuntu-latest
    steps:
      - name: 'Stage 1'
        run: |
          if [[ "${{ github.event.head_commit.message }}" =~ \[run-stage-2\] ]];
          then
            echo "Valid commit message detected. stage 2 will run."
          else 
            echo "Valid commit message not detected. stage 2 will not run."
            exit 1
          fi
      - name: 'Stage 2'
        run: <whatever>

So here, stage 1 one checks to see if the commit message contains [run-stage-2]. If it does, stage 1 will complete and stage 2 will begin; but it if doesn't, exit 1 is called and the workflow will fail.

2
  • 7
    OP wants to skip without failing the workflow. Commented Jun 11, 2024 at 13:41
  • exit 1 will mark the run as a failure. Commented May 20 at 7:08

Your Answer

By clicking β€œPost Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.