CI/CD

GitHub Actions workflow not triggering

The push happened and no run appeared. The event, branch filter, path filter and default-branch rules that silently prevent a workflow from starting.

medium fix5 min read

the ci/cd error
(no error: the workflow simply does not appear in the Actions tab)

This workflow has a workflow_dispatch event trigger, but it is not in the default branch.

Workflow does not have 'workflow_dispatch' trigger.

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Check the file parses at all

    python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/ci.yml')); print('valid YAML')"

    An invalid workflow file does not run and does not always surface an error in the Actions tab. Valid YAML is necessary but not sufficient, since the schema also has to be right.

  2. 2

    Confirm the event and branch filters match what happened

    python3 -c "import yaml; d=yaml.safe_load(open('.github/workflows/ci.yml')); print(d.get(True) or d.get('on'))"

    Note the quirk: unquoted on is parsed as the boolean True by YAML, which is why this reads both keys. Compare the branches and paths filters against the branch and files you actually pushed.

  3. 3

    Check whether the trigger was another workflow's token

    git log -1 --format='%an <%ae>'

    A push made with the default GITHUB_TOKEN deliberately does not trigger further workflow runs, which prevents infinite loops. An author of github-actions[bot] on the triggering commit explains a workflow that never starts.

All 9 sections

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:

  • paths and paths-ignore are mutually exclusive in one event block.
  • branches and branches-ignore likewise.
  • Path filters use glob patterns where ** crosses directory boundaries and * does not.
  • Path filters do not apply to workflow_dispatch or schedule.
  • 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

  1. Parse the YAML. Then run actionlint for schema errors.
  2. Compare branches and paths against what you actually pushed.
  3. Path filters do not apply to workflow_dispatch or schedule.
  4. workflow_dispatch needs the file on the default branch to appear.
  5. git log -1 --format='%an'. A github-actions[bot] push triggers nothing.
  6. Scheduled workflows are UTC, default branch only, and disabled after 60 days idle.
  7. Settings → Actions → General, and organisation policy above it.
  8. 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.

Reference and practice

Learn the underlying concept

Other CI/CD errors