How To Run a Github-Actions Step, Even If The Previous Step Fails, While Still Failing The Job

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:

  1. Be intentional about which steps should continue after failures – Not every step should run after a failure; choose carefully
  2. Use descriptive step names – Clear naming helps with debugging and understanding workflow execution
  3. Add IDs to critical steps – This enables referencing them in conditional expressions
  4. Consider using composite steps or reusable workflows – For complex error handling patterns you use frequently
  5. 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.

 

Marianne elanotta

Marianne is a graduate in communication technologies and enjoys sharing the latest technological advances across various fields. Her programming skills include Java OO and Javascript, and she prefers working on open-source operating systems. In her free time, she enjoys playing chess and computer games with her two children.

Leave a Reply