GitHub Actions · beginner
GitHub Actions job is skipped unexpectedly: evaluate condition inputs
Diagnose a GitHub Actions job that is reported as skipped even though it appears in the workflow file. The job is typically excluded by an evaluated `if` condition, a `needs` dependency that was skipped or failed, a matrix entry that did not match, or a workflow-level trigger that does not deliver the expected event payload. The playbook walks through reading GitHub's own UI state for the job, isolating which guard fired, and adjusting only the condition that evidence supports.
The symptoms
- •Workflow run shows a yellow "Skipped" badge next to a job that the engineer expected to execute.
- •Job appears in the workflow YAML at a specific path, yet the Actions run timeline does not list it as started or queued.
- •Required checks list shows the job as "Skipped" instead of "Success", blocking merges that depend on it.
- •Multiple jobs in the same run are skipped in sequence, and the first skipped job points to a prior `needs` dependency.
- •Workflow dispatch input fields are populated, yet downstream conditionals evaluate to false against those inputs.
- •Pull request event from a forked repository causes the job to be skipped even though branches match.
Likely causes
- •`jobs.<id>.if` or `jobs.<id>.steps[*].if` condition references a context such as `github.event`, `inputs`, or `vars` that is undefined for the triggering event, so the expression evaluates to falsy.
- •`jobs.<id>.needs` references an earlier job that was itself skipped or failed, propagating the skipped state downstream through GitHub Actions' default failure propagation.
- •Matrix strategy (`jobs.<id>.strategy.matrix`) configured with `exclude` entries that match every value for the active event, leaving no candidate matrix combinations to run.
- •Workflow-level or job-level `permissions` together with an `if` expression that gates execution on a token scope; a restricted GITHUB_TOKEN makes the expression false on certain event types.
- •Concurrency group with `cancel-in-progress: true` cancelling a previous in-flight run before the new job starts, leaving the latest run with skipped slots.
- •The triggering event (`on:` filter such as `pull_request_target` vs `pull_request`) does not surface the same `github.event` payload fields that the condition inspects.
First ten minutes
- 01Open the failed or unexpected run in the GitHub Actions UI and read the run header to confirm the triggering event, branch, and SHA; record the event name exactly as shown.
- 02Expand every skipped job in the timeline and capture the "Skipped" reason text that GitHub renders beneath the job, since GitHub prints the evaluated condition outcome there.
- 03Verify that the workflow file path on the recorded SHA contains the job identifier you expected, ruling out a typo or a workflow file that lives on a different branch than the run used.
- 04List the immediate `needs` of the skipped job and check whether any prerequisite job shows "Skipped" or "Failed" first, because GitHub skips downstream jobs by default when an upstream job is skipped.
- 05Inspect the workflow file's `on:` filter versus the actual event from the run header to confirm the trigger accepted the event payload in the first place.
- 06Open the run's "Annotations" or summary panel for any GitHub-supplied diagnostic message that names the `if` expression or the missing context key.
- 07Capture the run URL and the exact workflow file SHA so subsequent edits can be reviewed against the same evaluation context.
Evidence to collect
- •Run ID, workflow file path, commit SHA, branch, and triggering event name as displayed on the run header.
- •The literal "Skipped" reason text GitHub prints under each skipped job in the UI timeline.
- •The full `jobs.<skipped_id>.if` expression string and any matching `needs` array from the workflow YAML at the recorded SHA.
- •The matrix definition block (`strategy.matrix` and `strategy.matrix.exclude`) for the skipped job, if applicable.
- •For `workflow_dispatch` runs, the input values that were submitted and the corresponding `inputs.*` references inside the condition.
- •Concurrency group settings at workflow or job level, including `cancel-in-progress`, and the timestamps of competing runs that were cancelled.
Where to look
- •The GitHub Actions run timeline at the per-job "Skipped" tooltip, which mirrors GitHub's evaluated condition result rather than what the file literally says.
- •The workflow file at the commit SHA the run executed, not the latest commit on the default branch; runners always read the YAML pinned to that SHA.
- •The `jobs.<id>.needs` array and the resolved run order, since default behavior is `if: success()` and skipped upstream jobs propagate skipped state.
- •The `on:` trigger block matched against the recorded event name; filters such as `branches`, `paths`, or `types` silently skip runs that do not match.
- •The `concurrency` block at workflow or job level, where `cancel-in-progress: true` will mark older runs as superseded and skip their still-pending jobs.
- •GitHub's expression reference for `github.event` to confirm which fields exist for the recorded event name, especially for `pull_request` versus `pull_request_target`.
Diagnostic steps
- 01From the run header, derive the event type and branch, then form a hypothesis: trigger mismatch, `if` evaluation false, `needs` propagation, matrix exclusion, or concurrency cancellation.
- 02Read the "Skipped" reason text under the job; if GitHub cites the evaluated `if` expression (for example, the literal expression string), the cause is the conditional, not trigger or needs.
- 03If no `if` explanation is shown, chain through `needs`: walk upstream until you find a job that is itself skipped or failed, and treat that as the root skip.
- 04For matrix jobs, compute the Cartesian product of `strategy.matrix` and apply each `exclude` entry against the inputs in the matrix; if the result set is empty, the job is skipped for every combination on this event.
- 05For `workflow_dispatch` runs, substitute the recorded input values into the `if` expression and evaluate it manually against the official expression syntax to find the falsy operand.
- 06For `pull_request` events from forks, verify whether the job references secrets or write permissions; GitHub hides those scopes on fork PRs and any `if` that checks them returns false.
- 07For runs that were cancelled by concurrency, compare the run's start time with any earlier in-flight run in the same group; the newer run's timeline will show the older run as cancelled mid-flight.
Common mistakes
- •Editing the workflow on a different branch than the one used for the run; the runner evaluates the YAML at the recorded SHA, so the visible "skipped" reason does not change until a new run is triggered.
- •Adding `if: ${{ always() }}` blindly to dodge the skip without verifying why the upstream was skipped; this masks the root cause and can let a failed job silently satisfy downstream.
- •Confusing `github.event.pull_request.head.ref` with `github.ref`; the two strings differ, and `pull_request` events do not populate `github.head_ref` the way `pull_request_target` does.
- •Assuming `needs: [job]` only orders execution; in GitHub Actions it also propagates a default `if: success()` skip when the needed job is skipped, which is frequently misread as a bad condition.
- •Relying on the workflow file's default branch version when the run was triggered from a pull_request event; runner reads the merge commit SHA, not the base branch file.
Safe fixes
- •If the "Skipped" reason names the `if` expression, narrow the condition by referencing only keys documented for the recorded event, then re-run via the same trigger so the same `github.event` payload drives the evaluation.
- •If `needs` propagation is the cause, decide explicitly: either set the downstream job's `if` to `needs() || success()` to allow independent execution, or fix the upstream job so it is no longer skipped; pick based on whether the downstream job actually depends on the upstream artifact.
- •If matrix exclusion empties the matrix for this event, adjust `exclude` entries so at least one valid combination remains for the recorded branch and event type, then re-run the trigger.
- •If the trigger filter (`branches`, `paths`, `types`) excluded the event, broaden only the filter dimension the run violated and document the change in the PR description so reviewers can audit it.
- •If concurrency cancellation is the cause, switch `cancel-in-progress` to `false` for the affected group, or scope the group so it does not include both the skipped run and the cancelling run; verify on a fresh dispatch that no run is cancelled mid-flight.
- •If the `if` references a secret or write permission on a fork PR, move that logic to `pull_request_target` on a trusted ref or restrict the condition to keys GitHub guarantees for fork PRs; confirm by running the same fork PR again.
Prove the fix
- 01Re-dispatch the same trigger (push, pull_request, or workflow_dispatch) on the same branch and confirm the previously skipped job now appears with a green "Success" or expected failure status in the run timeline.
- 02For `needs`-based fixes, confirm the root upstream job is no longer skipped on a subsequent run and the downstream job executes; record both job names in the run's timeline.
- 03For matrix fixes, verify that the run timeline lists at least one non-skipped matrix shard for the previously affected event type and branch.
- 04For trigger-filter fixes, run the workflow once with the previously excluded branch or path and confirm a new run is queued instead of the run being marked skipped before any job starts.
- 05Capture the new run ID and the updated "Skipped" reason text (now empty or "Completed") as the regression check for the change.
Prevention and next steps
- •Pin workflows to the SHA they were authored against; treat the run header's SHA as the source of truth when reading "Skipped" reasons rather than the default branch file.
- •Add comments next to each `if:` expression naming the event types and context keys it depends on, so future edits do not silently change evaluation when the trigger list is updated.
- •Use explicit `needs: [job] if: needs.job.result != 'skipped' || ...` patterns instead of relying on the default `success()` propagation, and document that choice in the workflow file.
- •Run workflow linting or a local YAML/JSON schema check for Actions on every pull request so trigger filters, matrix exclusions, and condition expressions surface review comments before merging.
- •Keep an audit table of skipped jobs and their root cause in the repository's runbook so recurring skip patterns are visible across releases and event types.
Safe commands and checks
gh run view <run-id> --json event,headBranch,headSha,displayTitle,workflowName --jq '.event,.headBranch,.headSha,.workflowName'
gh run view <run-id> --json jobs --jq '.jobs[] | select(.conclusion=="skipped") | {name: .name, conclusion: .conclusion}'
gh workflow view <workflow-file-or-id> --yaml
gh api repos/<owner>/<repo>/contents/.github/workflows/<file>.yml?ref=<sha> --jq '.content' | base64 -d
gh run list --workflow=<workflow-file-or-id> --branch <branch> --limit 5 --json databaseId,event,conclusion,headSha