# GitHub Actions setup-go unable to find go version, and what is in the quotes

> github actions setup-go unable to find go version quotes the spec it searched for. What sits inside those quotes is the whole diagnosis.

Source: https://latchkey.dev/learn/github-actions/setup-go-unable-to-find-go-version  
Updated: 2026-09-21

GitHub Actions setup-go unable to find go version is thrown after the action has searched the published Go download index and found nothing that satisfies the spec it was given for this operating system and this architecture. The spec is quoted in the message, and reading what is inside the quotes settles almost every instance of this failure, because the spec is frequently not the string anybody wrote.

## What this error means

A `setup-go` step fails before anything is installed, with a message naming a version, a platform and an architecture. Look at the quoted spec first and ask whether it is what you expected. Three shapes turn up. A plausible version number means the spec was right and no published build matches it for that platform and architecture. A version you never wrote means it came from a file rather than from the workflow. The contents of an entire file, comments and all, means the action read a file whose name it does not recognise and used the whole thing as a version string. The platform and architecture at the end of the message are not decoration: the search matches a per-file operating system and architecture, so a version that exists can still be unfindable here.

```Actions log, quoted from ctrl-research/mmo#3; the reporter marked the elisions
##[error]Unable to find Go version '# Single source of truth for language/tool versions (asdf/mise format).
# Humans and AI agents should use these versions...
golang 1.26.5
nodejs 24.16.0
...' for platform linux and architecture x64.
```

## Common causes

### go-version-file points at a file the action does not parse

The most surprising cause and the easiest to confirm, because the error quotes the file back at you. Only `go.mod`, `go.work` and `.tool-versions` are recognised, by exact basename. A `tested-go.mod`, a copied manifest under a different name or a version file in a format the action never supported all fall through to the branch that treats the whole file as a version string.

### A toolchain directive in go.mod pins a patch that cannot be resolved

The `toolchain` line is read before the `go` line, so a repository that bumps toolchains for the `go` command is also, silently, changing what `setup-go` tries to install. In our experience this is the cause whenever the quoted version is a precise patch that nobody remembers putting in the workflow.

### The version exists but has no build for this platform and architecture

The match is per file, not per release, so an `arm64` macOS job or a Windows job can fail on a version a Linux job installs happily. The end of the message names the platform and architecture that found nothing, which is the only place this cause announces itself.

### The spec is an exact patch that was never published

A typo, a patch number invented by a bump script, or a release that was pulled. This is the cause the message reads as if it always means, and it is real, but it is the one to check after the three above rather than before them.

## How to fix it

### Read what is inside the quotation marks before changing anything

1. If the quotes hold many lines or a comment, the action fell through to its unrecognised-basename branch and the fix is the file name, not the version.
2. If the quotes hold a patch nobody wrote in the workflow, search `go.mod` for a `toolchain` line.
3. If the quotes hold the version you expected, read the platform and architecture at the end of the message and check that a build exists for that pair.

### Give go-version-file a name the parser recognises

Point the input at a real `go.mod` or `go.work`, or copy the manifest to that basename first. A file staged under any other name is not rejected, so nothing warns you; the whole file simply becomes the spec.

```.github/workflows/ci.yml (illustrative)
- run: cp tested-go.mod "${RUNNER_TEMP}/go.mod"
      - uses: actions/setup-go@v6
        with:
          go-version-file: ${{ runner.temp }}/go.mod
```

### Decide which directive you want the action to follow

If the toolchain line is meant for the `go` command rather than for CI, keep the workflow on an explicit range so the two cannot drift apart. If you do want the toolchain line to drive the install, leave it alone and accept that bumping it changes what CI installs.

```.github/workflows/ci.yml (illustrative)
- uses: actions/setup-go@v6
        with:
          go-version: '1.24.x'
```

### Prefer a range over a hand-typed patch

A minor range lets the action pick the newest published patch that has a build for the runner it is on, which removes both the stale-patch cause and most of the platform cause at once. Reserve an exact patch for the rare case where you are pinning to reproduce something.

## How to prevent it

- Keep `go-version-file` pointed at a basename the action parses, and copy rather than rename when the source file is called something else.
- Treat a `toolchain` line in `go.mod` as a CI input, because on an ordinary run it is one.
- Use a minor range in the workflow so a new patch does not need a workflow edit.
- Read the platform and architecture at the end of the message before assuming the version is wrong.

## Where the quoted spec came from

`resolveVersionInput` in `src/main.ts` picks the spec. The `go-version` input wins outright, and when both inputs are set the action warns and uses `go-version` anyway. Otherwise `go-version-file` is read by `parseGoVersionFile`, which dispatches on the file's basename and nothing else.

That dispatch is the part worth memorising, because its last line is a fall-through. A basename the function does not know is not an error: the whole file, trimmed, becomes the version spec. That is how an entire `.tool-versions` or a renamed manifest ends up inside the quotation marks in the error, which is exactly what the log above shows.

| File basename | What parseGoVersionFile reads | Spec when nothing matches |
| --- | --- | --- |
| `go.mod` or `go.work` | The `toolchain goX.Y.Z` line first, then the `go X.Y` line | An empty string |
| `.tool-versions` | The value after `golang` on its own line | An empty string |
| Anything else | Nothing is parsed | The entire file contents, trimmed |

> An empty spec is not an error either. `run()` in `src/main.ts` only installs when the spec is truthy, so an unparseable `go.mod` produces a step that succeeds and installs nothing, and the failure lands later on whatever Go the image happened to ship.

## The toolchain line beats the go line, unless you say otherwise

Inside `go.mod` the function looks for `toolchain go...` before it looks for `go ...`, and it only skips that first look when `GOTOOLCHAIN` is already set to `local` in the environment. The action does set `GOTOOLCHAIN=local`, but `setGoToolchain()` runs after `resolveVersionInput()`, so on an ordinary run the toolchain directive is the one that decides.

The practical effect is that a `toolchain go1.24.2` line pins the action to that exact patch. Go toolchain directives are written for the `go` command, which will download a toolchain on demand, and they are routinely bumped to patches that are newer than whatever the action can resolve for a given platform. The workflow file then looks innocent while the failure quotes a version nobody typed into it.

```go.mod (illustrative)
// go.mod
go 1.24

toolchain go1.24.2
```

## What the search actually matches

`findMatch` in `src/installer.ts` fetches the published Go download index, converts each candidate version into semver notation, and asks whether it satisfies the spec as a semver range. A candidate that satisfies the range is not enough on its own: the function then looks through that candidate's file list for an entry whose `arch` and `os` both equal the runner's, and only stops when it finds one.

So two different failures print the same sentence. Either nothing satisfied the range, or something did and had no file built for this operating system and architecture. The trailing `for platform X and architecture Y` is the part of the message that distinguishes them, and it is the part readers skip.

> The spec is a semver range, not a literal. `1.24` matches the newest published 1.24.x, `1.24.x` does the same, and `1.24.0` matches exactly one release. A hand-typed exact patch is the only one of the three that can go stale.

## The other two ways this step fails, which are not this message

A `go-version-file` pointing at a path that does not exist throws `The specified go version file at: <path> does not exist` from `resolveVersionInput`, before any search happens. That is a checkout or working-directory problem and no version is involved.

A version that resolves but fails to download throws `Failed to download version <spec>: ` with the underlying error, or, on a custom download base URL, a message naming HTTP 404 and the URL it tried. Both of those mean the resolution step succeeded, which is the opposite of what this page is about, so the fix ladder below does not apply to them.

## Why there is no recorded run on this page

The search this message reports on runs against the published Go download index, a document that changes whenever a Go release is cut. A reproduction of ours would pin a spec that fails today, and the useful half of it, namely which versions exist for which platforms, would start rotting the moment it was recorded. The mechanism does not rot, so this page records the resolution order and the matching rule from the action's source and quotes a failure that a third party reported against their own manifest.

## FAQ

### Why does the error quote my whole version file?

Because `parseGoVersionFile` dispatches on the exact basename and falls through for anything it does not recognise, returning the trimmed file contents as the version spec. Only `go.mod`, `go.work` and `.tool-versions` are handled. A manifest saved under any other name is used verbatim.

### Where did this version number come from? It is not in my workflow.

Almost always from a `toolchain goX.Y.Z` line in `go.mod`. The parser looks for that line before the `go` directive and only skips it when `GOTOOLCHAIN` is already `local`, and the action sets that variable after it has resolved the version, so on a normal run the toolchain line decides.

### The version definitely exists. Why can setup-go not find it?

Because the match is against a file within a release, not the release itself. `findMatch` requires a candidate whose `os` and `arch` both equal the runner's. A release with no build for that pair is skipped, and the message names the platform and architecture that came up empty.

### What happens if the version file parses to nothing at all?

The step succeeds and installs nothing. `run()` only calls the installer when the resolved spec is truthy, so an empty result is silently skipped and the job goes on using whatever Go the runner image already had. That is a quieter failure than this error and worth checking for.

## References

- [actions/setup-go: src/installer.ts, parseGoVersionFile, findMatch and both throw sites](https://github.com/actions/setup-go/blob/main/src/installer.ts)
- [actions/setup-go: src/main.ts, resolveVersionInput and setGoToolchain](https://github.com/actions/setup-go/blob/main/src/main.ts)
- [Go modules reference: the toolchain directive in go.mod](https://go.dev/ref/mod#go-mod-file-toolchain)
- [ctrl-research/mmo#3: the failure quoted here, with the whole file inside the quotes](https://github.com/ctrl-research/mmo/pull/3)
- [IceCodeNew/maniud#55: the same fall-through hit with a manifest named tested-go.mod](https://github.com/IceCodeNew/maniud/pull/55)

---

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
