A cross-platform package registry sounds fine on a whiteboard: NuGet packages live in one place, pipelines pull them from another, everybody moves on. Then a token expired at the worst possible moment, three pipelines broke for a full day, and I got to watch a team discover exactly how much invisible tax they were paying on every deploy. This is the story of that day and the cleanup that followed.
Packages in Azure, pipelines in GitLab
The setup was the kind of thing nobody designs on purpose — it just accretes. The NuGet packages lived in Azure DevOps. The CI/CD ran in GitLab. Between the two sat a personal access token, a PAT, quietly counting down to an expiry date nobody had written on a calendar.
Every build reached across that gap. dotnet restore would authenticate against Azure with the PAT, pull the packages, and hope the credential was still valid. Most days it was. Until it wasn't. The token expired mid-sprint, and three separate pipelines went red at the same time because they all leaned on the same secret. A full day gone to something that produced zero features and zero fixes.
Zero trust, and not the good kind
The worse discovery was the culture that had grown around this fragility. Nobody trusted anything.
- PAT tokens expired mid-sprint with no warning and no owner.
- CI/CD variables were scattered across projects with zero documentation.
- Developers were genuinely afraid to touch the NuGet config, because the last time someone edited it, three pipelines broke for a full day.
That fear is the real cost. When people stop touching a system because it might explode, the system stops improving. It calcifies. You end up with tribal knowledge about which token goes where, and the one person who understood it is on holiday the week it breaks.
The migration: one platform for code, packages, and pipelines
The fix was not clever. It was structural. I moved everything to GitLab Package Registry — the same platform that already held the code and ran the pipelines. Same walls, same door.
The important part was authentication. Every personal access token got replaced with CI_JOB_TOKEN. It is built into GitLab CI, scoped to the job that is running, and it needs zero rotation. There is no expiry date to track because the token only exists for the life of the job. The credential-rotation calendar and the mid-sprint surprises simply stopped being a category of problem.
The variable graveyard
Migrating the packages was only half the work. The CI/CD variables were their own horror show, and cleaning them was the part that actually took judgement.
Opening the variables list felt like opening a junk drawer:
- Old Azure DevOps PATs nobody remembered creating.
- Duplicate NuGet source URLs with slightly different names, so you could never be sure which one a job actually used.
- Variables that had outlived the people who created them.
I audited every single one. For each variable the question was blunt: does anything still read this? If nothing did, it died. If two variables did the same job, they got consolidated into one. This is boring, unglamorous work, and it is exactly the work nobody does until the boredom turns into an emergency.
What the pipeline looks like now
Once the packages were in GitLab and the variables were clean, the pipeline collapsed into something you can explain in one breath:
dotnet restorepulls from the GitLab Package Registry.- Build and pack.
- Push the package back to the same registry.
Every step authenticates automatically with the job token. No PAT sitting in a variable somewhere. No cross-platform handshake. No calendar reminder to rotate a secret before it takes out three pipelines on a Friday afternoon.
The lesson: don't let packages and pipelines live in different worlds
If your packages live in a different world than your pipelines, you are paying a tax on every deploy. It shows up as authentication overhead, as configuration drift, as tribal knowledge about which credential goes where — and every so often it shows up as a full lost day when a token you forgot about finally expires.
Consolidate. Put the code, the packages, and the pipelines behind one wall so the platform can hand out short-lived, scoped credentials instead of you managing long-lived ones by hand. One registry. One auth flow. One less reason for the pipeline to break at 5 PM on a Friday.
So here is the honest question to end on: how many authentication tokens in your pipelines could expire tomorrow — and do you actually know?