Debug Detective: the locked library
A pipeline that would not start. No failing job, no logs, no red cross next to a step, because nothing ever got as far as running. Just a refusal at the door and an error message written to tell me as little as possible.

The scene
The setup was one I had built deliberately. Pipeline logic does not live in the application repository; it lives in a components project, a library of small versioned YAML files that every project includes and extends. One fix, every consumer, no copy-paste. That is the whole point of it.
Then a pipeline stopped starting. Not failing: starting. There was no job graph to look at, no log to open, no stage to expand. GitLab had refused to assemble the configuration at all, and all it would tell me was this:
Project `group/components-ci` not found or access denied!
First checks
I did the obvious things, in the obvious order, and every one of them came back clean.
- The components project existed. I opened it in the browser.
- The path was right. The file was sitting there at exactly the location I had written.
- The ref was right. The tag existed and pointed where I expected.
- No typo. I compared the include block against the repository path character by character, which is the kind of thing you do twice and still do not quite believe.
So: the project is there, the file is there, the reference is there, and GitLab says the project is not found. That contradiction is where I lost the next stretch of time.
The false lead
I went after the .gitlab-ci.yml. Of course I did. The error appeared when the configuration was parsed, the configuration is that file, therefore the fault is in that file. That reasoning is clean and it is wrong.
I rewrote the include block three different ways. I tried it with the ref quoted and unquoted. I moved it to the top of the file in case ordering mattered. I ran it through the CI lint tool, which told me the syntax was fine, which I decided must mean the lint tool was not checking hard enough. When your model of a problem is wrong, every piece of evidence that contradicts it gets explained away rather than believed.
Then I reached for the second trap, and I want to name it because it looks so much like the answer: the CI/CD job token allowlist. I had read about it, it is about one project accessing another, it lives under CI/CD settings, and adding my project to the allowlist felt exactly like the fix. It did nothing, because the job token governs what a running job may call at runtime. My problem happened before any job existed.
The clue
The break came from reading the error message properly instead of skimming it. Not found or access denied. Two possibilities, joined by an or, and I had spent hours investigating only the first one.
That ambiguity is deliberate. If GitLab told you a private project existed but you could not read it, that would confirm the existence of private projects to anyone who could guess a path. So it collapses both cases into one message. It is a defensible security decision, and it is also the reason the wrong branch is so easy to take: it hands you a sentence in which the word found is doing all the work, and quietly buries the half that turns out to matter.
The reveal
An include: project: is not resolved by the pipeline. It is resolved as the user who triggered the pipeline.
GitLab reads the configuration on that person's behalf, and applies that person's permissions on the other project. For a private project the triggering user needs at least the Reporter role on it. For an internal project, any authenticated non-external user will do. For a public project, nothing is required at all.
My components library was not readable by the account that ran that pipeline. The file was exactly where I said it was; the pipeline was simply not allowed to know that. Every check I had run, I had run as myself in a browser, where I had access. I had been confirming, over and over, that I could see the file, and the question was never about me.
The detail that would have cracked it in five minutes
Because the check is per user, this failure is not consistent across a team. The same pipeline, the same commit, the same configuration, is green when one person runs it and red when another does. If you ever see "it works when I trigger it and fails when they do", stop looking at the YAML. That symptom does not belong to a file. Files do not change based on who reads them; permissions do.
I did not have that clue at the time because I was working alone. It is the first thing I would ask about now.
The fix, and what it costs
There are two honest answers, and which one is right depends on what the library is for.
- Grant access. Add the user, or better a group, as Reporter on the components project. Correct when the library contains anything that must stay private. The cost is that you now maintain a membership list, and every new person or new project is a permission request waiting to happen.
- Change the visibility. Make the components project internal, or public. Correct when the library is deliberately generic, which mine is: it holds build, scan, deploy, health-check, rollback and backup jobs, and there is nothing in it that is a secret. Secrets belong in variables, not in the YAML that references them.
I went with visibility. Not to avoid the membership work, but because being forced to answer the question was useful: if a shared library needs its contents kept secret, the secret is in the wrong place. Making it public turned a permissions problem into a design check that it passed.
Detective's note
- Read the whole error, including the word "or". Ambiguous messages are often ambiguous on purpose, and the half you skip is the half that costs you the afternoon.
- Check who is asking, not just what is asked for. "The file exists" and "this identity may read the file" are different claims. I verified the first one four times and never once tested the second.
- Do not verify a permission by logging in as yourself. Every check I ran was in a browser where I already had access. That is not a test, it is a reassurance.
- Know which layer a setting belongs to. The job token allowlist controls runtime API calls. Include resolution happens before runtime. Two features that sound identical, in different phases of the same system.
- "It works for me and not for them" is a permissions symptom. Almost never a file one.
Whose permissions is your pipeline actually using?
Most CI setups have at least one place where access is granted to a person rather than to a system, and it stays invisible until that person is on holiday. Finding those is what a platform audit is for: one week, a ranked list, a ninety-day plan. Tell us what your pipeline reaches for.
Get in touch
flochai