When building CI/CD pipelines with GitHub Actions, one common challenge is handling test failures while still ensuring critical follow-up steps execute.
For instance, how do you ensure test results are archived as artifacts even when tests fail? In this guide, we’ll explore multiple approaches to running GitHub Actions steps after a failure while maintaining appropriate job status reporting.
Understanding the Problem
GitHub Actions workflows typically stop executing when a step fails. This behavior makes sense for most scenarios – why continue building if your tests don’t pass? However, there are legitimate cases where you need subsequent steps to run regardless of a previous step’s outcome:
- Archiving test results for debugging failed tests
- Cleaning up resources created during the workflow
- Sending notifications about the failure
- Collecting logs or diagnostics information
Read: How to install Git on Ubuntu 18.04
Solution 1: Using if: always()
The most straightforward solution is to use the always() condition in your workflow steps. This ensures the step runs regardless of whether previous steps succeeded or failed.
Here’s how to implement it:
steps:
– uses: actions/checkout@v1
– name: Run Tests
run: ./gradlew tes
– name: Archive Test Results
if: always()
uses: actions/upload-artifact@v1
with:
name: test-results
path: app/build/reports/tests
The if: always() expression guarantees that the “Archive Test Results” step will execute even if the tests fail. Importantly, the overall job will still be marked as failed if any step fails, which preserves the correct reporting of the workflow status.
Solution 2: Using success() || failure()
While always() works well in many cases, it has one significant drawback: steps with if: always() will run even when a workflow is manually canceled. This might not be desirable when you deliberately stop a workflow.
A more precise alternative is to use the success() || failure() condition:
steps:
– name: Run Tests
run: ./gradlew test
– name: Archive Test Results
if: success() || failure()
uses: actions/upload-artifact@v1
with:
name: test-results
path: app/build/reports/tests
This condition ensures the step runs if previous steps either succeeded or failed, but not if the workflow was canceled. This approach gives you more control while still addressing the original problem.
Read: How to discard changes in Git
Solution 3: Using !cancelled()
If you prefer a more concise expression that achieves the same result as success() || failure(), you can use:
steps:
– name: Run Tests
run: ./gradlew test
– name: Archive Test Results
if: ‘!cancelled()’
uses: actions/upload-artifact@v1
with:
name: test-results
path: app/build/reports/tests
Note the required quotes around the expression due to YAML syntax requirements – the exclamation mark can’t be the first character in an unquoted string.
Solution 4: Targeting Specific Step Failures
Sometimes you need more granular control. For instance, you might want to run a step only when a specific previous step failed, but not when other steps fail:
steps:
– name: Run Tests
id: test-step
run: ./gradlew test
– name: Archive Test Results
if: failure() && steps.test-step.conclusion == ‘failure’
uses: actions/upload-artifact@v1
with:
name: test-results
path: app/build/reports/tests
By giving an id to the test step and checking its conclusion, you can ensure the archive step runs only when that specific step fails.
Solution 5: Using continue-on-error
Another approach is to use the continue-on-error property on steps that might fail but shouldn’t stop the workflow:
steps:
– name: Run Tests
run: ./gradlew test
continue-on-error: true
– name: Archive Test Results
uses: actions/upload-artifact@v1
with:
name: test-results
path: app/build/reports/tests
However, be aware that this approach has an important limitation: when using continue-on-error: true, steps that fail won’t cause the overall job to fail. If maintaining the failed status of the workflow is important (which it typically is for test failures), this approach isn’t recommended.
Understanding GitHub Actions Status Check Functions
To effectively use conditional execution in GitHub Actions, it’s helpful to understand the available status check functions:
- success(): Returns true when all previous steps have succeeded
- failure(): Returns true when any previous step has failed
- always(): Always returns true, even if the workflow was canceled
- cancelled(): Returns true if the workflow was canceled
An important nuance is that GitHub Actions implicitly applies success() to if conditions when no status check function is specified. For example, if: true behaves like if: success() && true, meaning the step won’t run if previous steps failed.
Combining Conditions for Complex Scenarios
You can combine status functions with other conditions for more complex logic:
steps:
– name: Run Tests
id: tests
run: ./gradlew test
– name: Upload Test Results Only If Tests Ran
if: always() && (steps.tests.conclusion == ‘success’ || steps.tests.conclusion == ‘failure’)
uses: actions/upload-artifact@v1
with:
name: test-results
path: app/build/reports/tests
This example ensures test results are uploaded if the tests ran (regardless of whether they passed or failed), but not if they were skipped or the workflow was canceled before they could run.
Best Practices for Error Handling in GitHub Actions
When implementing error handling in your workflows, consider these best practices:
- Be intentional about which steps should continue after failures – Not every step should run after a failure; choose carefully
- Use descriptive step names – Clear naming helps with debugging and understanding workflow execution
- Add IDs to critical steps – This enables referencing them in conditional expressions
- Consider using composite steps or reusable workflows – For complex error handling patterns you use frequently
- Test your error handling logic – Deliberately trigger failures to ensure your workflow behaves as expected
Practical Example: Complete Workflow
Here’s a complete workflow example showing how to run tests, archive results regardless of test outcome, but still properly mark the job as failed when tests fail:
- name: Test and Archive Results
- on:
- pull_request:
- branches:
- – main
- push:
- branches:
- – main
- jobs:
- test-and-report:
- runs-on: ubuntu-latest
- steps:
- – uses: actions/checkout@v3
- – name: Set up Java
- uses: actions/setup-java@v3
- with:
- distribution: ‘temurin’
- java-version: ’17’
- – name: Run Tests
- id: run-tests
- run: ./gradlew test
- – name: Generate Test Report
- if: success() || failure() # Run even if tests fail, but not if canceled
- run: ./gradlew testReport
- – name: Archive Test Results
- if: success() || failure() # Run even if tests fail, but not if canceled
- uses: actions/upload-artifact@v3
- with:
- name: test-results
- path: build/reports/tests
- retention-days: 7
- – name: Notify on Test Failure
- if: failure() && steps.run-tests.conclusion == ‘failure’
- run: |
- echo “Tests failed – would send notification here”
- # Integration with notification system would go here
This workflow demonstrates several of the techniques we’ve discussed, ensuring that test results are always available for inspection, even when tests fail.
FAQ
Does if: always() affect the overall job status?
No. Using if: always() only affects whether that particular step runs – it doesn’t change the overall job status. If any step fails, the job will still be marked as failed.
What’s the difference between success() || failure() and always()?
The main difference is that always() will run the step even if the workflow was manually canceled, while success() || failure() will not run the step if the workflow was canceled.
Can I combine status functions with other conditions?
Yes. You can use logical operators to combine status functions with other conditions, for example: if: always() && github.event_name == ‘push’.
What happens if I use continue-on-error: true on a step?
The step will be allowed to fail without stopping subsequent steps, but importantly, it won’t mark the job as failed. If maintaining the correct job status is important, use if: always() or if: success() || failure() instead.
How do I reference a specific step’s outcome?
First, assign an ID to the step using the id property. Then, reference it using steps.<id>.conclusion or steps.<id>.outcome in your conditional expression.
If you like the content, we would appreciate your support by buying us a coffee. Thank you so much for your visit and support.