From 70002d67fcf8486bacbad3e32f7370b194fe3724 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:08:02 +0000 Subject: [PATCH] GitLab CI guide: let the runner write the NuGet configuration The runner of "Every project on a runner" exchanges the ID token only for a project whose own nuget.config names Forge, so every project still carries one. A new section shows a runner whose pre_build_script writes the user's NuGet configuration, ~/.nuget/NuGet/NuGet.Config, itself: - a job with an ID token gets Forge's group feed under the key forge, and the exchanged key in NuGetPackageSourceCredentials_forge, out of the file; a failed exchange fails the job rather than falling back; - any other job gets the NuGet.Config an instance CI/CD variable, CI_NUGET_CONFIG, holds base64-encoded; a value that is no base64 fails the job, and without the variable the job keeps the image's configuration. The section says that a project's own nuget.config still comes first (one with drops the runner's sources), that an instance variable with credentials reaches every job of every project on every runner, and that the runner needs an executor with a container per job, since a shell executor shares $HOME between its jobs. Fixes #632 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012e6DJbjyXxvtjaxyAg1Crw --- docs/changes/2.1.1/632.docs.md | 4 ++ docs/gitlab-ci.md | 73 ++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 docs/changes/2.1.1/632.docs.md diff --git a/docs/changes/2.1.1/632.docs.md b/docs/changes/2.1.1/632.docs.md new file mode 100644 index 00000000..b58ff3d9 --- /dev/null +++ b/docs/changes/2.1.1/632.docs.md @@ -0,0 +1,4 @@ +[GitLab CI with workload identity](docs/gitlab-ci.md#the-runner-writes-the-nuget-configuration) +shows a runner that writes the NuGet configuration itself, so that projects +carry none: a job with an ID token gets Forge's feed and the exchanged key, +any other job a `NuGet.Config` kept base64-encoded in a CI/CD variable. diff --git a/docs/gitlab-ci.md b/docs/gitlab-ci.md index a653825f..21d22dea 100644 --- a/docs/gitlab-ci.md +++ b/docs/gitlab-ci.md @@ -426,6 +426,79 @@ build: - **The jobs run on that runner**: give them its tags, or let it pick up untagged jobs. +### The runner writes the NuGet configuration + +The runner above exchanges only for a project whose own `nuget.config` names +Forge. It can write the configuration itself instead, so that a project needs +none: a job with a token gets Forge's feed and the key, and every other job a +`NuGet.Config` of your own, which an instance CI/CD variable, `CI_NUGET_CONFIG`, +holds base64-encoded. + +```toml +[[runners]] + # name, url, token, executor, … + environment = ["FORGE_URL=https://forge.example.com"] + pre_build_script = ''' +nuget_config="$HOME/.nuget/NuGet/NuGet.Config" +if [ -n "${FORGE_ID_TOKEN:-}" ]; then + FORGE_KEY="$(curl --fail --silent --show-error -X POST "$FORGE_URL/api/v2/token" \ + -H "Authorization: Bearer $FORGE_ID_TOKEN" \ + -H 'Content-Type: application/json' \ + -d '{"username":"oidc","tokenType":"ApiKey"}' \ + | sed -n 's/.*"apiKey":"\([^"]*\)".*/\1/p')" + test -n "$FORGE_KEY" + export FORGE_KEY + export NuGetPackageSourceCredentials_forge="Username=oidc;Password=$FORGE_KEY" + mkdir -p "$(dirname "$nuget_config")" + cat > "$nuget_config" < + + + + + + +EOF +elif [ -n "${CI_NUGET_CONFIG:-}" ]; then + mkdir -p "$(dirname "$nuget_config")" + printf '%s' "$CI_NUGET_CONFIG" | base64 -d > "$nuget_config" +fi +''' +``` + +- **The user's configuration.** The runner writes + `~/.nuget/NuGet/NuGet.Config`, the file NuGet reads beneath every + project's own, in place of the one the image brings (the .NET SDK image's + names nuget.org). A project without a `nuget.config` restores from what the + runner wrote. A project's own `nuget.config` still comes first: one with + `` drops the runner's sources, and then names Forge under the key + `forge` itself, as in [step 5](#5-the-project). +- **A job with a token** gets Forge's group feed under the key `forge`. Its + key goes in `NuGetPackageSourceCredentials_forge`, as above, and stays out + of the file. The token decides alone: a project that asks for it uses + Forge, whether or not it has a `nuget.config`. +- **A failed exchange fails the job.** The job does not fall back to + `CI_NUGET_CONFIG`: a job that asks for a token is meant to reach Forge with + it. +- **Every other job** gets `CI_NUGET_CONFIG`, decoded as it is: the jobs of + a project that asks for no token. Add the variable under **Admin → + Settings → CI/CD → Variables**, or in a group's CI/CD variables for that + group's projects alone; `base64 -w0 NuGet.Config` prints its value. A value + that is no base64 fails the job. While the variable is unset, a job without + a token keeps the image's configuration. +- **`CI_NUGET_CONFIG` is a secret** when it holds credentials. An instance + variable reaches every job of every project on every runner, not only this + one's, and any of those jobs can print it, as with + [the runner's environment](#without-workload-identity). Give a token in it + the **read** level alone and an expiry. +- **A container per job.** With the Docker or Kubernetes executor every job + runs in a fresh container, and the file and the key go with it. A shell + executor shares `$HOME` between all its jobs, concurrent ones too: they + would overwrite each other's configuration, and the last one's would stay. + Use this runner only with an executor that gives each job a container. +- **The image** of a job needs `curl`, `sed`, `base64` and a POSIX shell; the + .NET SDK image has them. + ### Without workload identity A runner can also hand every job a [service account](ci-service-accounts.md)'s