# GitHub Actions labeler applies no labels, and which exit you took

> When GitHub Actions labeler applies no labels the step is usually red, not green. Find out which of three exits your run took before you edit a glob.

Source: https://latchkey.dev/learn/github-actions/gha-labeler-no-labels-applied  
Updated: 2026-09-21

When GitHub Actions labeler applies no labels there are three different exits from the action and only one of them is quiet. The loudest of the three throws before a single glob is matched, so the advice to go and check your path patterns is aimed at a run that never reached them.

## What this error means

A pull request opens, the labeler job appears in the checks list, and the pull request carries no labels. What you do next depends entirely on the color of that job, and the color is the first thing to read. A red job means the configuration file was rejected while it was being parsed, and the message names the label it choked on. A green job with no labels means the file parsed, the rules were evaluated, and nothing matched, which is a completely different investigation. A green job that names an option in passing means the file parsed and one of your rules was quietly discarded on the way through.

```Reconstructed from the throw in getLabelConfigMapFromObject, actions/labeler src/api/get-label-configs.ts
Error: found unexpected type for label 'docs' (should be array of config options)
```

## Common causes

### The configuration file is still in the pre-version-5 shape

A label whose value is a flat list of glob strings. This was correct for years and is now a type error, which is why the failure feels so abrupt: nothing was deprecated in place, the parser simply stopped accepting a string where it wants a mapping.

### A rule uses a key the action does not recognize

Only `any`, `all`, `changed-files`, `head-branch` and `base-branch` mean anything inside a rule. Anything else is dropped with one informational line. A near miss on a key name, or a key invented from an older example, leaves the label with an empty rule that can never match.

### The file is not where the action looked

The default path is `.github/labeler.yml`. A repository that stores it elsewhere, or that runs the action without checking the repository out in a context where it reads from disk, gets a warning about the path and the access rights and then a failed run.

### The job cannot write labels

Missing `pull-requests: write`, or a restrictive default permission set at the organization level. In our experience this is the one people reach for first and it is the one that fails last, because everything before the write succeeds.

### The patterns are right and the files really did not change

A pull request that touches only files outside every rule gets no labels, correctly. Before rewriting globs, list the changed files the action saw rather than the ones you believe are in the branch.

## How to fix it

### Read the job color, then the first log line

1. Open the labeler job in the failed or passing check and look at whether it is red or green.
2. If it is red, find the thrown line. A quoted label name with the words about an array of config options is the pre-version-5 shape.
3. If it is green, search the log for a line about an unknown config option, which names both the label and the key it discarded.
4. If it is green and silent, the rules evaluated and did not match.

### Convert a flat glob list to the match-object shape

Each label takes a sequence of rules. A rule is a mapping. The common case, one set of globs where any match is enough, is `changed-files` with `any-glob-to-any-file` under it.

```.github/labeler.yml
# before, rejected by version 5 and later
docs:
  - docs/**

# after
docs:
  - changed-files:
      - any-glob-to-any-file: 'docs/**'
```

### Grant the write permission on the job that needs it

Declare the permission block on the labeler job rather than the workflow, so nothing else in the file inherits write access to pull requests. `contents: read` is enough for the rest.

```.github/workflows/labeler.yml
jobs:
  label:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/labeler@v6
```

### Turn on step debug logging to see the silent path

The evaluation stage writes at debug level. Set the `ACTIONS_STEP_DEBUG` repository secret or variable to true and re-run the job, and the log will name each rule as it is checked. Turn it off again afterwards, because it is noisy on every job in the repository, not only this one.

### Pin the configuration path when the file is not at the default

Pass `configuration-path` explicitly. It removes one whole class of confusion, because a missing file and an unmatched rule look nothing alike once you know which one you are looking at.

```.github/workflows/labeler.yml
- uses: actions/labeler@v6
  with:
    configuration-path: .github/config/labeler.yml
```

## How to prevent it

- Pin the action to a major version you have read the release notes for, and read them when you move the pin.
- Keep the labeler permission block on the labeler job, not on the workflow.
- After changing the configuration, open a throwaway pull request that touches one file per rule.
- Treat a green labeler job with no labels as a different bug from a red one, every time.

## Read the job color before you read the globs

The action loads your configuration file, converts it into a list of match rules per label, and then evaluates those rules against the changed files. Each of those three stages can end with no labels applied, and they are distinguishable without guessing, because the stages fail in different colors.

The parse stage is strict about shape. For every label key in the file, the value must be a sequence whose items are all mappings. If any item is not a mapping the action throws, and the message quotes the label name and says what it expected. A pre-v5 configuration is exactly this shape: the value under a label was a flat list of glob strings, and a string is not a mapping, so the run ends red before any file is compared against any pattern.

The convert stage is lenient. Inside a rule, a key that is not `any`, `all`, `changed-files`, `head-branch` or `base-branch` is not an error. The action writes one informational line naming the label and the key, and carries on with that key discarded. If it was your only key, the rule for that label is now empty.

The evaluate stage is silent. An empty rule cannot match, and the action records that at debug level, which means you will not see it unless step debug logging is switched on for the repository.

| Stage that ends it | Job color | What the log carries |
| --- | --- | --- |
| Configuration parse | red | a thrown error naming the label |
| Configuration convert | green | one informational line naming the key |
| Rule evaluation | green | a debug line, hidden by default |
| Configuration file missing | red | a warning, then the run fails |

> The missing-file case is worth separating from the other three. The action warns that the file was not found at the path it looked in and adds that the action may not have the access rights to read it, then rejects. A repository that keeps the file somewhere other than the default path needs `configuration-path` set.

## What version 5 changed, in the project own words

The release notes for version 5 say that the match object for changed files was expanded with new combinations, and that as a result the configuration file structure was significantly redesigned and is not compatible with the structure of the previous version. That is the sentence that matters here. It is not a deprecation with a fallback. The old structure does not produce fewer labels, it produces a type error.

The same release changed two other defaults that are easy to miss when a migration is finally done. The `sync-labels` input had a bug that made its value read incorrectly, and that was fixed, so a repository that relied on the broken behavior will now see labels removed when they stop matching. The `dot` input became true by default, so paths that begin with a dot, `.github` among them, are matched without being asked for.

Version 6 changed the runtime rather than the configuration. It moved the action from Node 20 to Node 24, and the notes say the runner must be on version 2.327.1 or later for compatibility. On GitHub hosted runners that is not something you manage. On a self hosted runner that has not been updated in a while, it is.

```.github/labeler.yml, the version 5 and later shape
docs:
  - changed-files:
      - any-glob-to-any-file: 'docs/**'

frontend:
  - changed-files:
      - any-glob-to-any-file:
          - 'src/**/*.tsx'
          - 'src/**/*.css'
```

## The permission, and why it fails later than you expect

The action needs `pull-requests: write` to attach a label, and the token it uses comes from `repo-token`, which defaults to the workflow token. A job without that permission still parses the configuration and still evaluates every rule, because none of that touches the API. It fails at the moment it tries to write, which is after all the work, so the log looks like a successful run that fell over at the end.

That ordering is why a permission problem and a configuration problem feel similar from the check list and read nothing alike in the log. A permission failure has matched labels in it. A configuration failure does not get that far.

One more ordering detail is worth knowing if your workflow runs on `pull_request_target` so that forks can be labeled. The project has a standing note about that trigger, because it runs with a token that has write access against a base branch you control while checking out code you may not. Read the note before you copy a `pull_request_target` example.

## Why there is no recorded run on this page

Every outcome on this page is produced inside a third party action running on somebody else configuration file, and the interesting variable is the file, not the machine. A recorded run would show one repository labeler configuration and one set of changed files, which proves nothing about yours. The two error strings quoted here are read out of the action own source, where the shape that triggers each one is visible next to the throw, and that is a stronger claim than a screenshot of one run.

There is also nothing here for a runner to repair. A rule that does not match the files in a pull request is a correct execution of the rule you wrote.

## FAQ

### Why does actions/labeler apply no labels after upgrading to v5?

Because version 5 redesigned the configuration file structure and the release notes state it is not compatible with the previous version. A pre-version-5 file, where a label maps to a flat list of glob strings, is not merely ignored. The parser expects each item under a label to be a mapping, so it throws and the job ends red.

### What does "found unexpected type for label" mean?

It is the action telling you that the value under that label name was not a sequence of mappings. It is raised while the configuration is being converted, before any changed file is compared against any pattern, so nothing about your globs is implicated yet.

### Why is my labeler job green but no labels appear?

Either a rule was discarded because it used a key the action does not recognize, in which case there is one informational line naming the label and the key, or the rules evaluated and did not match the changed files, which is recorded only at debug level.

### Which permission does actions/labeler need?

It needs `pull-requests: write` to attach labels. The check happens when the action calls the API, which is after the configuration has been parsed and the rules evaluated, so a permission failure appears at the end of an otherwise normal looking log.

### Does labeler match files that start with a dot?

Since version 5 it does by default, because the `dot` input changed to true. Before that you had to ask for it. This is one of the quieter reasons a migrated configuration applies more labels than it used to rather than fewer.

## References

- [actions/labeler: get-label-configs.ts, the throw and the unknown-key branch](https://github.com/actions/labeler/blob/main/src/api/get-label-configs.ts)
- [actions/labeler: README, breaking changes in V5 and V6](https://github.com/actions/labeler/blob/main/README.md)
- [actions/labeler: action.yml, the declared inputs and their defaults](https://github.com/actions/labeler/blob/main/action.yml)
- [GitHub Actions: enabling debug logging](https://docs.github.com/en/actions/how-tos/monitor-workflows/enable-debug-logging)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
