No error, no run. GitHub evaluated the workflow and decided it did not apply. Work through these.
1. Does the file parse?
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml')); print('ok')"
An unparseable workflow is skipped. GitHub sometimes surfaces this as an annotation and sometimes not.
Note a YAML quirk that bites here: unquoted on is parsed as the boolean true by YAML 1.1 parsers. GitHub handles it, and your own tooling may not, so d.get('on') returning None in a script does not mean the key is missing.
actionlint catches schema errors that a YAML parse will not:
actionlint .github/workflows/ci.yml
2. Do the filters match?
on:
push:
branches: [main]
paths:
- 'src/**'
Both must match. A push to main touching only README.md does not run this, and that is correct behaviour rather than a fault.
branches:
- main
- 'release/**'
paths-ignore:
- '**.md'
- 'docs/**'
Points worth knowing:
pathsandpaths-ignoreare mutually exclusive in one event block.branchesandbranches-ignorelikewise.- Path filters use glob patterns where
**crosses directory boundaries and*does not. - Path filters do not apply to
workflow_dispatchorschedule. - On a pull request, paths are evaluated against the whole diff, not the latest push.
A required status check on a workflow that path-filters itself out never reports, so the pull request blocks forever. Use a skipped-but-reporting job for that:
check:
if: always()
needs: [test]
runs-on: ubuntu-latest
steps:
- run: '[[ "${{ needs.test.result }}" =~ ^(success|skipped)$ ]]'
3. workflow_dispatch and the default branch
This workflow has a workflow_dispatch event trigger, but it is not in the default branch.
The Run workflow button only appears once the workflow file exists on the default branch. Adding workflow_dispatch on a feature branch does nothing visible until it is merged.
Once merged, you can dispatch it against any branch, and the version of the file that runs is the one on the branch you select.
schedule has the same rule: cron triggers only fire from the default branch.
4. A push made by GITHUB_TOKEN
Events created using the default GITHUB_TOKEN deliberately do not trigger further workflow runs. This prevents a workflow that pushes from triggering itself indefinitely.
git log -1 --format='%an <%ae>'
# github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
That explains a workflow that never starts after an automated commit. If you genuinely need the chain, use a PAT or a GitHub App token, and think about the loop you are creating. A workflow_run trigger is often cleaner:
on:
workflow_run:
workflows: ["Release"]
types: [completed]
5. Scheduled workflows
on:
schedule:
- cron: '0 3 * * *'
- Always UTC, with no timezone option.
- Only from the default branch.
- Delayed under load, sometimes by a long time, and occasionally skipped. Not suitable for anything requiring punctuality.
- Disabled automatically after 60 days of repository inactivity, which is the cause of a nightly job that quietly stopped months ago.
6. Actions disabled, or a fork
Check Settings → Actions → General. A repository, or an organisation policy above it, can disable Actions entirely or restrict which actions may run.
Forks are their own case: workflows are disabled by default on a fork and must be enabled manually, and a pull_request from a fork runs with a read-only token and no secrets.
The GitLab equivalent
rules and workflow:rules decide whether a pipeline is created at all:
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- when: never
A trailing when: never means anything not matched creates no pipeline, which is intentional and easy to forget.
GitLab also skips pipeline creation when every job is excluded by its own rules, which produces the same silence.
# validate before pushing
curl -s --header "PRIVATE-TOKEN: $TOKEN" \
"https://gitlab.com/api/v4/projects/$PROJECT_ID/ci/lint" \
--data-urlencode "content@.gitlab-ci.yml"
A commit message containing [skip ci] or [ci skip] prevents a pipeline on both platforms, which catches people who use it habitually.
A checklist
- Parse the YAML. Then run
actionlintfor schema errors. - Compare
branchesandpathsagainst what you actually pushed. - Path filters do not apply to
workflow_dispatchorschedule. workflow_dispatchneeds the file on the default branch to appear.git log -1 --format='%an'. Agithub-actions[bot]push triggers nothing.- Scheduled workflows are UTC, default branch only, and disabled after 60 days idle.
- Settings → Actions → General, and organisation policy above it.
- GitLab → check
workflow:rules, and for[skip ci]in the commit message.
Frequently Asked Questions
Why does my workflow_dispatch button not appear?
Because the workflow file must exist on the repository's default branch for GitHub to offer the Run workflow control. Adding workflow_dispatch on a feature branch has no visible effect until it is merged. Once it is on the default branch you can dispatch it against any branch, and the version of the file that executes is the one on the branch you select, not the default branch copy. Scheduled triggers follow the same default-branch rule.
Why did my workflow not run after an automated commit?
Because events created with the default GITHUB_TOKEN do not trigger further workflow runs, which is a deliberate guard against a workflow that pushes triggering itself forever. Check the author of the triggering commit: github-actions[bot] explains it. If you genuinely need the chain, authenticate the push with a personal access token or a GitHub App installation token, or restructure using a workflow_run trigger, which is usually the cleaner option.
How do path filters interact with required status checks?
Badly, if you are not careful. A workflow that filters itself out on a pull request touching no matching paths never reports a status, so a branch protection rule requiring that check blocks the pull request indefinitely. The usual solution is a small gate job that always runs and passes when the real job either succeeded or was skipped, so the required check always reports. Note also that path filters are ignored entirely for workflow_dispatch and schedule.
Why did my scheduled workflow stop running?
Most likely because GitHub disables scheduled workflows after 60 days without repository activity, and it emails the last committer rather than announcing it loudly. Re-enable it in the Actions tab. Two other things worth knowing about schedule: the cron expression is always interpreted as UTC with no timezone option, and runs are queued on a best-effort basis and can be significantly delayed or occasionally skipped under load, so it is unsuitable for anything time-critical.
Why is my GitLab pipeline not created at all?
Usually workflow:rules. A rule set ending in when: never means any commit not matching an earlier condition produces no pipeline, which is intentional but easy to forget when adding a new branch pattern. GitLab also creates no pipeline when every individual job is excluded by its own rules. Validate the file against the project's CI lint API before pushing, and check the commit message for [skip ci], which suppresses pipelines on both platforms.