Lesson 12 / 25
Conditional Steps and Jobs
Run only when appropriate.
if with status and event checks
if: on a job or step decides whether it runs. Combine event and ref checks (deploy only on pushes to main), labels, matrix values and status functions: success() (the default), failure(), always() and cancelled(). Prefer !cancelled() over always() for cleanup that should not run on cancellation. Conditions keep one workflow flexible, but too many make it hard to follow; split into separate workflows when logic grows.
A tag-only release job, linted
I checked this workflow with actionlint 1.7.12 (actionlint -oneline .github/workflows/perms.yml); the output and exit code are copied from that run. actionlint validates syntax, expressions, job dependencies and known actions offline; shellcheck was not installed, so shell scripts inside run: were not linted. The workflow was not executed on GitHub. The job runs only for tags; actionlint flags the permission scope pull-request, which does not exist (the correct scope is pull-requests), and lists the valid scopes.
name: Release
on:
push:
tags: ["v*"]
permissions:
contents: write
pull-request: write
jobs:
release:
runs-on: ubuntu-latest
if: ${{ github.ref_type == 'tag' }}
steps:
- uses: actions/checkout@v4
- run: echo "Releasing ${{ github.ref_name }}"
Output:
.github/workflows/perms.yml:9:3: unknown permission scope "pull-request". all available permission scopes are "actions", "artifact-metadata", "attestations", "checks", "contents", "deployments", "discussions", "id-token", "issues", "models", "packages", "pages", "pull-requests", "repository-projects", "security-events", "statuses" [permissions] (exit code 1)
Upload test reports on failure
A step with if: ${{ failure() }} can upload logs and screenshots exactly when you need them.
Quick check: Which status function runs a step even when earlier steps failed but not when the run is cancelled?
- success()
- !cancelled()
- always() only
- never()
Answer
!cancelled() — Cleanup without running on cancellation.