runner icon indicating copy to clipboard operation
runner copied to clipboard

Conditional operator or function for expression syntax

Open StephenCleary opened this issue 5 years ago • 26 comments

Describe the enhancement

I'd like some kind of conditional operation added to expression syntax. This can be an actual ternary operator (? :) or a built-in function (e.g., if(<condition>, <true-value>, <false-value>)).

Additional information

There is a workaround: using shell scripts to evaluate the condition, setting step outputs, and having other steps reference those outputs. But it would be much cleaner to have some kind of ternary / if/then/else / conditional branching built in to the expression syntax.

StephenCleary avatar Apr 06 '20 13:04 StephenCleary

Another alternative syntax could be:

${{ if(condition) }} evaluateIfTrue ${{ else }} evaluateIfFalse ${{ endif }}

like CircleCI.

ylemkimon avatar Aug 16 '20 07:08 ylemkimon

It seems, although not identical, <condition> && <true-value> || <false-value> works in most cases.

ylemkimon avatar Nov 15 '20 12:11 ylemkimon

Syntax: ${{ x && 'ifTrue' || 'ifFalse' }}

fake ternary example

    steps:
      - name: stuff
        env:
          PR_NUMBER_OR_MASTER: ${{ github.event.number == 0 && 'master' ||  format('pr-{0}', github.event.number)  }}

on condition is that your first <true-value> must be true for this to work

molszanski avatar Dec 30 '20 22:12 molszanski

Example use case https://github.com/TWiStErRob/net.twisterrob.gradle/pull/136/files Related StackOverflow: https://stackoverflow.com/a/68422069/253468

TWiStErRob avatar Jul 17 '21 15:07 TWiStErRob

The fact that in a year no one said a word about this, means that the team isn't interested in following up on this one?

LaPeste avatar Aug 26 '21 13:08 LaPeste

A note about the workaround ${{ x && <yes> || <false> }}:

This only works if <yes> isn't the empty string. If <yes> == '' then '' is considered as false, which then evaluates the right hand side of the ||. Just lost ten minutes over this, hope it helps!

nmattia avatar Jan 14 '22 17:01 nmattia

Another word to the wise about the workaround, which is that it only works with single quotes. In other words ${{ (<boolean> && 'true value') || 'false value' }} works but ${{ (<boolean> && "true value") || "false value" }} is a syntax error.

[EDIT: So, it turns out you can never use double quotes inside an expression if you use ${{ ... }, which explains the above. They actually do say that here, but I'd missed it. Nonetheless, I'll leave this up in case anyone else misses it, too.]

lobsterkatie avatar Mar 16 '22 22:03 lobsterkatie

Great workaround, but can someone from @actions triage this so we get proper support without edge cases?

mrcnski avatar May 24 '22 15:05 mrcnski

In case it helps anyone else: I was trying to a conditional runs-on, doing something like:

runs-on: ${{ (inputs.shouldUseSelfHosted && [ 'self-hosted', 'Linux', 'X64' ] || 'ubuntu-latest' }}

But this was a syntax error, since I guess array literals don't work in expressions?

So then I tried wrapping it in quotes:

runs-on: ${{ (inputs.shouldUseSelfHosted && '[ ''self-hosted'', ''Linux'', ''X64'' ]') || 'ubuntu-latest' }}

But while this is syntactically correct, and it works for the falsy case ('ubuntu-latest'), the truthy case doesn't seem to properly resolve a runner—presumably becaues the value type should be an array, not a string.

Finally, after some trial an error, I arrived at a working solution:

${{ (inputs.shouldUseSelfHosted && fromJSON('[ "self-hosted", "Linux", "X64" ]')) || 'ubuntu-latest' }}

The fromJSON properly converts the string into an array when it evaluates, and resolves to a valid array for github purposes.

Anyway, uh, yeah, this would all be a lot nicer with ternaries/an if function 😓

rubencodes avatar Jun 17 '22 13:06 rubencodes

It supports workflow_call as well as a manual triggers which specific to stage

runs-on: ${{ (inputs.shouldUseSelfHosted || github.event.inputs.stage == 'prod') && fromJSON('[ "self-hosted", "Linux", "X64" ]') || 'ubuntu-latest' }}

vignesh-desk avatar Aug 05 '22 18:08 vignesh-desk

I found that using fromJSON worked nicely for me if i had to deal with more complex conditionals

env:
  IMAGE: ${{ fromJSON('{"main":"prod","dev":"sat"}')[github.ref_name] || github.ref_name }}

instead of

env:
  IMAGE: ${{ (github.ref_name == 'main' && 'prod') || (github.ref_name == 'dev' && 'sat') || github.ref_name }}

NoodleOfDeath avatar Sep 20 '22 00:09 NoodleOfDeath

3 years anniversary soon!

patrickelectric avatar Feb 03 '23 00:02 patrickelectric

I think it is funny that GitHub is able to develop immense complex and scalable systems. But this simple conditional is not yet there 🤡

indymaat avatar Feb 06 '23 21:02 indymaat

@andrewakim fyi 😄

ericsciple avatar Feb 23 '23 09:02 ericsciple

Happy 3 year anniversary! 🎉

gabeins avatar Apr 06 '23 16:04 gabeins

A note about the workaround ${{ x && <yes> || <false> }}:

This only works if <yes> isn't the empty string. If <yes> == '' then '' is considered as false, which then evaluates the right hand side of the ||. Just lost ten minutes over this, hope it helps!

You can bypass this limitation by constructing an array with the false and true branch values (in this order) using fromJSON(), then access the correct element by using the bracket notation and placing the condition inside - it is implicitly cast from a boolean to a number, false => 0, true => 1, acting as an array index:

${{ fromJSON('["other value", "empty value"]')[something == ''] }}

Simran-B avatar May 19 '23 09:05 Simran-B

Seems that there is already an approach

https://docs.github.com/en/actions/learn-github-actions/expressions#example

env:
  MY_ENV_VAR: ${{ github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches' }}

I think this issue can be closed.

islishude avatar Aug 31 '23 03:08 islishude

@islishude you ~~can't~~ shouldn't just comment on an issue that hundreds of people have subscribed to, just to say "this can be closed" when your suggested approach (and its limitation) has already been mentioned. see https://github.com/actions/runner/issues/409#issuecomment-1013325196

mayank99 avatar Aug 31 '23 03:08 mayank99

Interesting, you saying I can't comment, is the comment illegal?

you can't just comment on an issue that hundreds of people have subscribed to

islishude avatar Aug 31 '23 03:08 islishude

It's good to see GitHub embracing the workaround as a stop-gap, they even mention its limitation in the docs @islishude linked.

On one hand it's good they added it, because at least now it's easier to discover, but on the other hand I do hope it doesn't mean they consider ternary "supported".

It would be great to get some official response from GitHub staff. 🤔

TWiStErRob avatar Aug 31 '23 08:08 TWiStErRob

It's ridiculous that we do not have ternary operator and this issue is 3+ y.o. Github actions is so silly engineered, will avoid it for any future projects by all means.

igor-pinchuk avatar Sep 08 '23 12:09 igor-pinchuk

Correct answer for booleans ${{ (condition && 'ifTrue') || (!condition && 'ifFalse') }}

ameknite avatar Sep 18 '23 19:09 ameknite

It's good to see GitHub embracing the workaround as a stop-gap, they even mention its limitation in the docs @islishude linked.

The docs state, "In this example, we're using a ternary operator to set the value of the MY_ENV_VAR environment variable," which is laughable, because there is no ternary operator, just a logical kludge that relies on short-circuit boolean evaluation:

env:
  MY_ENV_VAR: ${{ github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches' }}

Great exposition on just why this is a poor solution: https://7tonshark.com/posts/github-actions-ternary-operator/

socketbox avatar Oct 05 '23 19:10 socketbox

Thank you for your interest in the runner application and taking the time to provide your valuable feedback. We kindly ask you to redirect this feedback to the GitHub Community Support Forum which our team actively monitors and would be a better place to start a discussion for new feature requests in GitHub Actions. For more information on this policy please read our contribution guidelines. 😃

github-actions[bot] avatar Nov 18 '23 00:11 github-actions[bot]

Looks more as unsolved than "completed"

patrickelectric avatar Nov 18 '23 00:11 patrickelectric

Couldn't find an existing thread there so I created one: https://github.com/orgs/community/discussions/75928

enumag avatar Nov 18 '23 10:11 enumag

Hi everyone, we just announced our new case function which is adds conditionals for expressions.

salilsub avatar Jan 30 '26 03:01 salilsub

BTW it seems that this example from the case docs is not valid YAML:

env:
  MY_ENV_VAR: ${{ case(
    github.ref == 'refs/heads/main', 'production',
    github.ref == 'refs/heads/staging', 'staging',
    startsWith(github.ref, 'refs/heads/feature/'), 'development',
    'unknown'
  ) }}

The ) }} is not indented, so is not considered part of the MY_ENV_VAR value. Maybe this, with double quotes?

env:
  MY_ENV_VAR: "${{ case(
    github.ref == 'refs/heads/main', 'production',
    github.ref == 'refs/heads/staging', 'staging',
    startsWith(github.ref, 'refs/heads/feature/'), 'development',
    'unknown'
  ) }}"

lpulley avatar Jan 30 '26 15:01 lpulley

Also, I'm seeing errors like this when using non-boolean values as predicates in case:

The workflow is not valid. Error when evaluating 'run-name'. .github/workflows/my-workflow.yml (Line: 2, Col: 11): Error from function 'case': case predicate must evaluate to a boolean value

but the expressions docs say:

Note that in conditionals, falsy values (false, 0, -0, "", '', null) are coerced to false and truthy (true and other non-falsy values) are coerced to true.

Is there a reason that case doesn't coerce non-falsy values to true?

lpulley avatar Jan 30 '26 16:01 lpulley

@lpulley we've fixed the invalid YAML in the docs,

env:
  MY_ENV_VAR: |-
    ${{ case(
      github.ref == 'refs/heads/main', 'production',
      github.ref == 'refs/heads/staging', 'staging',
      startsWith(github.ref, 'refs/heads/feature/'), 'development',
      'unknown'
    ) }}

Thanks for calling that out!

As for the boolean values, we intentionally require predicates to be booleans to avoid common failure scenarios that unintentionally or incorrectly rely on falsy coercions. I'll take a look to see how we can improve the docs in regards to that as well.

AllanGuigou avatar Jan 30 '26 16:01 AllanGuigou