skip to Main Content

I am trying to build a trivial workflow using GitHub Actions consisting of the following jobs:

  1. Infrastructure Provisioning
  2. Application Deployment
  3. Status Callback

Thereby, I am using a ternary operator to determine whether ‘OK’ or ‘NOK’ is passed as an input variable to the Status Callback job.
(offical documentation: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#example)

However, my ternary operator seems to be behaving weirdly based on the following scenarios I have tested.

The code underlying my investigations/scenarios is always the same apart from the ternary operator determining the status:

  Status-Callback-Generic-A:
    # needs: [Infrastructure_Provisioning, Dummy_Application_Deployment]
    needs: [Dummy_Application_Deployment]
    if: ${{ always() }}
    uses: ./.github/workflows/status-callback.yml
    with:
      connectionId: ${{ inputs.connectionId }}
      status: ${{ needs.Dummy_Application_Deployment.outputs.is_success == 'true' && 'OK' || 'NOK' }}

Scenario A

status: ${{ needs.Dummy_Application_Deployment.outputs.is_success == 'true' && 'OK' || 'NOK' }}

Scenario B

status: ${{ needs.Dummy_Application_Deployment.outputs.is_success && 'OK' || 'NOK' }}

Scenario C

status: ${{ needs.Dummy_Application_Deployment.outputs.is_success == true && 'OK' || 'NOK' }}

Scenario D

status: ${{ needs.Dummy_Application_Deployment.outputs.is_success == 1 && 'OK' || 'NOK' }}

Scenario E

status: ${{ needs.Dummy_Application_Deployment.outputs.is_success == '1' && 'OK' || 'NOK' }}

Below table depicts the results from above scenario-specific code snippets. In summary: No single scenario seems to work properly.

is_success expected status status(A) status(B) status(C) status(D) status(E)
true OK NOK OK NOK NOK NOK
false NOK NOK OK NOK NOK NOK

Notes from official documentation:
(https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#operators)

  • job outputs evaluate as strings
  • GitHub transforms non-matching types to numbers. In case of Boolean:
    • true returns 1
    • false returns 0

Am I doing something fundamentally wrong with my conditionals?

The is_success is based on the steps in the respective workflow

name: Dummy Application Deployment

on:
  workflow_call:      
    outputs:
      is_success: 
        description: Dummy Description
        value: ${{ jobs.build_and_deploy_dummy_application.outputs.is_success }}

jobs:
  build_and_deploy_dummy_application:
    runs-on: ubuntu-latest
    outputs:
      is_success: |
          ${{ 
          steps.failing-step.outcome == 'success' &&
          steps.build-dummy-application.outcome == 'success' &&
          steps.deploy-dummy-application.outcome == 'success'
          }}
steps:
# omitted

2

Answers


  1. Chosen as BEST ANSWER

    Excuse my belated response! Most likely I would never have figured this out on my own... (one reason more to not like YAML ;))

    Learning: Do expression evaluations in a single line ;)

    For everyone interested please find below the action files highlighting the issue

    main.yml

    name: Main Flow
    
    on:
      workflow_dispatch:
        inputs:
          host:
            type: text
            required: true
          connectionId:
            type: text
            required: true
    
    jobs:
      Job-1:
        uses: ./.github/workflows/job-1.yml    
    
      Job-2:
        needs: [ Job-1 ]
        if: ${{ always() }}
        runs-on: ubuntu-latest
        steps:
          - id: log-outputs-of-job-1
            run: |
              echo "Job-1 - is_success_naive = ${{ needs.Job-1.outputs.is_success_one_liner }}"
              echo "Job-1 - is_success_naive = ${{ needs.Job-1.outputs.is_success_naive }}"
              echo "Job-1 - is_success_chomp = ${{ needs.Job-1.outputs.is_success_chomp}}"
              echo "Job-1 - is_success_other = ${{ needs.Job-1.outputs.is_success_other }}"
              echo "Job-1 - step-1_outcome = ${{ needs.Job-1.outputs.step-1_outcome }}"
              echo "Job-1 - step-1_outcome = ${{ needs.Job-1.outputs.step-2_outcome }}"
          
      Status-Callback:
        needs: [ Job-1 ]
        if: ${{ always() }}
        uses: ./.github/workflows/status-callback.yml
        with:
          host: ${{ inputs.host }}
          connectionId: ${{ inputs.connectionId }}
          status_one_liner: ${{ needs.Job-1.outputs.is_success_one_liner == 'true' && 'OK' || 'NOK' }}
          status_naive: ${{ needs.Job-1.outputs.is_success_naive == 'true' && 'OK' || 'NOK' }}
          status_chomp: ${{ needs.Job-1.outputs.is_success_chomp == 'true' && 'OK' || 'NOK' }}
          status_other: ${{ needs.Job-1.outputs.is_success_other == 'true' && 'OK' || 'NOK' }}
    

    job-1.yml

    name: Job 1
    
    on: 
      workflow_call:
        outputs:
          is_success_one_liner:
            value: ${{ jobs.Job-1.outputs.is_success_one_liner }}
          is_success_naive:
            value: ${{ jobs.Job-1.outputs.is_success_naive }}
          is_success_chomp:
            value: ${{ jobs.Job-1.outputs.is_success_chomp }}
          is_success_other:
            value: ${{ jobs.Job-1.outputs.is_success_other }}
          step-1_outcome:
            value: ${{ jobs.Job-1.outputs.step-1_outcome }}
          step-2_outcome:
            value: ${{ jobs.Job-1.outputs.step-2_outcome }}
            
    jobs:
      Job-1:
        runs-on: ubuntu-latest
        outputs:
          #Working (single line): 
          is_success_one_liner: ${{ steps.step-1.outcome == 'success' && steps.step-2.outcome == 'success' }}
          
          #Naive (|)
          is_success_naive: |
            ${{ 
              steps.step-1.outcome == 'success' &&
              steps.step-2.outcome == 'success'
             }}
          
          #Chomp (|-)
          is_success_chomp: |-
            ${{ 
              steps.step-1.outcome == 'success' &&
              steps.step-2.outcome == 'success'
            }}
          
          #Other indicator (>-)
          is_success_other: >-
            ${{ 
              steps.step-1.outcome == 'success' &&
              steps.step-2.outcome == 'success'
             }}
          step-1_outcome: ${{ steps.step-1.outcome }}
          step-2_outcome: ${{ steps.step-2.outcome }}
        steps:
          - id: step-1
            run: echo "Hello from step-1"
          - id: step-2
            #run: echo "Hello from step-2"
            run: failing-command
          - id: log-previous-outcomes
            run: |
              echo "Outcome step-1 = ${{ steps.step-1.outcome }}"
              echo "Outcome step-2 = ${{ steps.step-2.outcome }}"
    

    status-callback.yml

    name: Status Callback
    
    on:
      workflow_call:
        inputs:
          host:
            type: string
            required: true
          connectionId:
            type: string
            required: true
          status_one_liner:
            type: string
            required: true
          status_naive:
            type: string
            required: true
            default: 'naive'
          status_chomp:
            type: string
            required: true
            default: 'chomp'
          status_other:
            type: string
            required: true
            default: 'other'
    
    jobs:
      Status-Callback:
        runs-on: ubuntu-latest
        steps:
          - name: Log Input Variables
            id: log-input-variables
            run: |
              echo "host" = ${{ inputs.host }}
              echo "connectionId = ${{ inputs.connectionId }}"
              echo "status_one_liner = ${{ inputs.status_one_liner }}"
              echo "status_naive = ${{ inputs.status_naive }}"
              echo "status_chomp = ${{ inputs.status_chomp }}"
              echo "status_other = ${{ inputs.status_other }}"
          
          - name: Status Callback (One-Liner)
            id: status-callback-one-liner
            continue-on-error: true
            run: curl -X "POST" ${{ inputs.host }}/${{ inputs.connectionId }}/${{ inputs.status_one_liner }}
            
          - name: Status Callback (Naive)
            id: status-callback-naive
            continue-on-error: true
            run: curl -X "POST" ${{ inputs.host }}/${{ inputs.connectionId }}/${{ inputs.status_naive }}      
          
          - name: Status Callback (Chomp)
            id: status-callback-chomp
            continue-on-error: true
            run: curl -X "POST" ${{ inputs.host }}/${{ inputs.connectionId }}/${{ inputs.status_chomp }}      
          
          - name: Status Callback (Other)
            id: status-callback-other
            continue-on-error: true
            run: curl -X "POST" ${{ inputs.host }}/${{ inputs.connectionId }}/${{ inputs.status_other }}
    

  2. When is_success is set like this:

    is_success: |
      ${{ 
        steps.failing-step.outcome == 'success' &&
        steps.build-dummy-application.outcome == 'success' &&
        steps.deploy-dummy-application.outcome == 'success'
      }}
    

    using YAML’s literal style indicator i.e. |, there is always a trailing newline at the end which makes it a multiline output.

    Adding the strip block chomping indicator i.e. - should fix this:

    is_success: |-
      ${{ ... }}
    

    Better yet, to avoid scenarios where intermediate newlines may also cause such issues, prefer using fold with strip i.e. >- instead:

    is_success: >-
      ${{ ... }}
    

    You may experiment with https://yaml-multiline.info/ for different use cases of multiline strings.

    With this fix, the comparison against string literal 'true' or 'false' should work as expected:

    status: ${{ needs.Dummy_Application_Deployment.outputs.is_success == 'true' && 'OK' || 'NOK' }}
    

    Given above, you may dry run all your scenarios to figure out why some comparisons work and others won’t. The comparison was being performed against a multiline string:

    'truen' == 'true' (false)
    

    For scenario B:

    status: ${{ needs.Dummy_Application_Deployment.outputs.is_success && 'OK' || 'NOK' }}
    

    needs.Dummy_Application_Deployment.outputs.is_success is only being evaluated for a null or not-null value. It’s populated so it’s a not-null value that’s why it’s evaluated to true ('OK').

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search