Reputation: 3535
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?
Upvotes: 94
Views: 74994
Reputation: 4635
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'
Upvotes: 33
Reputation: 1256
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.
Upvotes: 5
Reputation: 67
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.
Upvotes: 3
Reputation: 43206
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:
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.
Upvotes: 76