GitHub Actions always() After Cancellation Explained
Résumé
Pixel shares a DEV Community guide explaining why GitHub Actions always() can run steps even after a workflow is cancelled. It clarifies how implicit success checks interact with status functions and shows when to use !cancelled() to avoid unwanted deployments, cleanup, or notifications.
DEV Community
GitHub Actions if: always() runs a step after cancellation
GitHub Actions if: always() is a function that returns true and checks nothing, so a step guarded with it starts after somebody presses cancel. The companion belief, that adding any condition removes the built-in success check, is the opposite of what the parser does, and it is worth unlearning before you add success() to conditions that never needed it. The grid on the left is the table reproduced in the body of this page. The rewrite on the right is quoted from ConvertToIfCondition in actions/runner. What this error means Nothing here is an error message. The symptom is a step that ran when you did not expect it to, and the bill or the side effect that followed. Somebody cancels a run halfway through and a deployment still goes out, or a cleanup job tears down an environment that the next run needed, or a notification announces a failure for a run a person deliberately stopped. In the log the step is green and the run conclusion is cancelled, which is an unusual combination and the clearest sign that this is what you are looking at. The second half of the problem produces the mirror image: a step that never runs after a failure, because somebody added success() to a condition that already had it. .github/workflows/ci.yml (illustrative) name: Upload logs if: always() # true in every state, cancellation included run: ./upload-logs.sh name: Notify if: ${{ !cancelled() }} # every state except cancellation run: ./notify.sh
Common causes always() was used to mean success or failure The function name reads like natural language and the intention is almost always narrower than what it does. Teams reach for it on artifact uploads, notifications and teardown steps, and it works until the first time somebody cancels a run, at which point the teardown runs against a half-finished environment. A status function was added that was already there The mirror mistake, and it is caused by the belief that any condition drops the gate. Writing success() next to a branch test changes nothing, because the parser would have added it. Writing failure() next to one changes everything, because now the step runs only after a failure. The negated form was written without braces A leading exclamation mark is YAML tag syntax, so the bare form is rejected at parse time rather than misbehaving at run time. It looks like a problem with the function when it is a problem with the quoting. A job condition was copied from a step condition The two positions allow different contexts. A job-level condition accepts the four status functions but not steps, env, job, runner or the file-hashing function, so a condition that works on a step can be rejected outright when it is moved up to the job. How to fix it Say which states you want, then pick the expression Write down whether the step should run after success, after failure, and after cancellation. Success and failure but not cancellation is the negated cancellation check, in braces. Failure only is failure(). Success only is nothing at all. All three, including cancellation, is always(), and it should be rare. Replace always() on anything with an external effect Deployments, teardown, cloud resources and anything that posts to a third party are the steps where cancellation should mean stop. Swap the function and leave the rest of the step alone..github/workflows/ci.yml (illustrative) name: Tear down preview environment if: ${{ !cancelled() }} run: ./teardown.sh Stop adding success() to ordinary conditions A condition with no status function in it is already gated. Removing the redundant call makes the expression shorter and makes the ones that really do override the gate visible in a diff..github/workflows/ci.yml (illustrative) name: Deploy to production if: github.ref == 'refs/heads/main' run: ./deploy.sh ``` Combine a status function with your own test explicitly When you do want a step to run after a failure and only in some cases, name both halves. The status function is what turns the gate...

Powered by Readybot.io