FifeRouter

3 September 2026 · mistakes ci

Uppercase in an image reference

Moving the repository to an organisation broke the pipeline in a way that only affected the references we built by hand.

FATAL  could not parse reference:
       ghcr.io/Tech-Res-Group/fiferouter:758ac79

The repository moved from a personal account to an organisation. Within minutes the image pipeline failed, and it failed in a way that was confusing for about ten minutes before it was obvious.

Two facts that only collide under an org

github.repository_owner returns the display name. Tech-Res-Group, with the capitals the organisation was created with.

A Docker reference must be lowercase. Not "is conventionally"; the grammar requires it, and a reference with capitals does not parse.

Under tarvitave the display name happened to be lowercase already, so nothing had ever exercised the difference. Every reference in the pipeline had been correct by coincidence for the life of the project.

Why the builds still succeeded

The confusing part: the images pushed fine. All three built and published.

docker/metadata-action lowercases its images: input, because it knows this. So the push path was correct, and everything downstream that built a reference by string interpolation was not:

Four hand-built references, one library call that quietly did the right thing. The library being helpful is exactly what made the failure surprising: the part you would check first was the part that worked.

A second failure with the same root

The deploy then refused:

REFUSED: no image tagged 'd646bba...' for: fiferouter fiferouter-api fiferouter-web

For images that had just been built and pushed successfully.

That guard exists to stop a deploy failing halfway because a tag was never built. It ran before the GHCR login, and these packages are private — so docker manifest inspect failed for want of credentials, in exactly the way it fails for a missing tag. Same non-zero exit, no way to tell them apart.

It failed safe and stopped before touching the server, which is the right direction. But the message sent me to images.yml to find out why a build had not run, and the build had run fine.

The fix

Lowercase once per job, use it everywhere:

- name: Lowercase the registry owner
  id: owner
  run: echo "lc=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"

And again on the server, in the script that actually assembles the reference — because validating the shape of an owner and then trusting its case leaves that script correct only for as long as its caller stays careful, and it is reachable by anything holding the CI key.

Login moved above the existence check, with a comment explaining why the order matters.

What it says about "it works"

Nothing about the repository changed. No code, no config, no dependency. The name changed, and the name had been carrying an assumption nobody had written down: that an account name is a valid Docker path component.

It is, right up until the account is an organisation whose display name has capitals — which is a property of how somebody typed a form once, years earlier, in a different system.

The general version: identifiers that flow between systems inherit the union of every system's grammar, and the loosest one usually looks like the truth until the strictest one gets a value it dislikes.


← All posts