GitHub Actions — Conditional Job Execution
Conditional job execution might not be the most used feature in GitHub Actions but it’s worth mentioning, especially because the conditions might be a bit confusing.
Let’s take this workflow as an example:
name: teston:
workflow_dispatch:jobs:
a:
name: a
runs-on: ubuntu-latest
steps:
- run: echo "A"
- id: set
run: |
echo "::set-output name=run_job_b::yes"
outputs:
run_job_b: ${{ steps.set.outputs.run_job_b }} b:
name: b
runs-on: ubuntu-latest
needs:
- a
if: needs.a.outputs.run_job_b == 'yes'
steps:
- run: echo "B" c:
name: c
runs-on: ubuntu-latest
needs:
- a
- b
if: needs.b.result == 'success' || needs.b.result == 'skipped'
steps:
- run: echo "C"
How do you expect the jobs will be executed?
Here are my expectations I had before I actually tested it:
- job
a
runs and setsrun_job_b
output variable - job
b
depends ona
, ifa
runs without problems (succeeds) andrun_job_b
variable is set toyes
, jobb
runs as well - job
c
runs only whena
succeeds andb
secceeds or is skipped
Let’s run the workflow:
Looks good.
Let’s run another test, let’s skip job b
:
name: teston:
workflow_dispatch:jobs:
a:
name: a
runs-on: ubuntu-latest
steps:
- run: echo "A"
- id: set
run: |
echo "::set-output name=run_job_b::no"
outputs:
run_job_b: ${{ steps.set.outputs.run_job_b }} b:
name: b
runs-on: ubuntu-latest
needs:
- a
if: needs.a.outputs.run_job_b == 'yes'
steps:
- run: echo "B" c:
name: c
runs-on: ubuntu-latest
needs:
- a
- b
if: needs.b.result == 'success' || needs.b.result == 'skipped'
steps:
- run: echo "C"
Job b
will be skipped, but I still expected job c
to be executed because that’s what the condition says after all:
if: needs.b.result == 'success' || needs.b.result == 'skipped'
This happened:
Wait, what?
This made me confused for a bit. I tried all sorts of variations of the conditions, e.g.:
if: |
needs.a.result == 'success' &&
(needs.b.result == 'success' || needs.b.result == 'skipped')
I checked the job result statuses.
Eventually I found this “issue”. It turns out that GitHub says in their docs:
You can use the following status check functions as expressions in
if
conditionals. A default status check ofsuccess()
is applied unless you include one of these functions.
Where “one of these functions” refers to:
sucess()
always()
cancelled()
failure()
That leaves me with something like this:
if: |
always() &&
needs.a.result == 'success' &&
(needs.b.result == 'success' || needs.b.result == 'skipped')
Or more general for my situation:
if: always() && <my-initial-condition>
Not very readable at first glance in my opinion. But when executed, it works:
It also works in some other situations:
- job
a
succeeds and jobb
succeeds, jobc
runs - job
a
fails, jobb
andc
don’t run - job
a
succeeds and jobb
fails, jobc
doesn’t run
It took me a while to figure it out, but it seems like it eventually works fine. So beware when building something similar, the behaviour might not be what you initially expect it to be.