Skip to content

GitLab CI guide: run the token exchange once on a GitLab Runner for every job it builds - #631

Merged
schmitch merged 1 commit into
mainfrom
schmitch/gitlab-ci-runner-exchange
Oct 28, 2026
Merged

schmitch merged 1 commit into
mainfrom
schmitch/gitlab-ci-runner-exchange

Conversation

@schmitch

@schmitch schmitch commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Fixes #630

GitLab CI with workload identity had each project run the token exchange in a hidden job. An organization with its own runner had to copy that job into every project.

Changes

docs/gitlab-ci.md gets a new section, Every project on a runner. Each job restores with the realm rule's read-only role, and only a job a trusted publisher matches can push.

  • The realm rule names the projects by namespace. A subgroup is a namespace of its own, and https://gitlab.com needs project IDs as well. The role stays read-only, and a project with a role of its own goes into an earlier rule.
  • The runner's pre_build_script (config.toml) runs in the job's shell right before its before_script. It exchanges only in a job that has a token, for a project whose nuget.config names $FORGE_URL. It exports FORGE_KEY and NuGetPackageSourceCredentials_forge, and a failed exchange fails the job. The section also states the limits:
    • the image needs curl, sed, find and a POSIX shell;
    • the source key must be forge;
    • GitLab.com's instance runners can't be configured.
  • Each project asks for the token under default: and runs no exchange of its own, since a token exchanges once. The release job pushes with the runner's $FORGE_KEY.
  • Without workload identity, the runner can carry a service account's read token in its environment instead. The section says that every job on the runner can then read that secret.

Smaller changes in the same guide:

  • A job with a before_script of its own no longer repeats the exchange; it lists !reference [.forge, before_script] first.
  • The troubleshooting row for a 401 names the project's exchange after the runner's as a cause.
  • Changelog fragment docs/changes/2.1.1/630.docs.md.

Testing

  • I pulled the runner's TOML block out of the guide, parsed it with Python's tomllib, and ran its pre_build_script under sh -e (dash) with a stub curl:
    • no token: skipped;
    • a token but no nuget.config: skipped;
    • a nuget.config with another feed: skipped;
    • src/app/NuGet.Config naming Forge: FORGE_KEY and the NuGet credentials exported, with the expected exchange request;
    • a failing exchange: exit 1.
  • The service-account TOML block parses too.
  • cargo xtask docs --check (links and anchors) and cargo xtask changelog --check pass.

Not verified

  • Not run on a real GitLab Runner. The section relies on how GitLab Runner builds a job's script, as I know it: pre_build_script comes first in the same shell as before_script and script, so its exports reach the job. It also relies on id_tokens being allowed under default:.
  • Not checked against GitLab's docs. The session that wrote this could not reach docs.gitlab.com.

Worth a run on a runner before merging.

🤖 Generated with Claude Code

https://claude.ai/code/session_012e6DJbNRFYp72ZsDpL9kCF

The guide had each project run the exchange in a hidden job. A new section,
"Every project on a runner", shows how a GitLab Runner of your own runs it
for every job it builds, so that each job restores with the realm rule's
read-only role and only a job a trusted publisher matches can push:

- the realm rule names the projects by their namespaces (a subgroup is a
  namespace of its own; gitlab.com needs project IDs as well);
- the runner's pre_build_script, which runs in the job's shell right
  before its before_script, exchanges only in a job that has a token, for
  a project whose nuget.config names Forge, and exports the key and the
  NuGet source credentials;
- each project asks for the token under default: and runs no exchange of
  its own, since a token exchanges once;
- a runner without workload identity can carry a service account's read
  token in its environment, a secret every job on it can read.

A job with a before_script of its own no longer repeats the exchange: it
lists !reference [.forge, before_script] first. The troubleshooting table
names the runner's exchange as a cause of a 401.

Fixes #630

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJbNRFYp72ZsDpL9kCF
@schmitch
schmitch added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 7fe9b30 Oct 28, 2026
17 checks passed
@schmitch
schmitch deleted the schmitch/gitlab-ci-runner-exchange branch October 28, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GitLab CI guide: run the token exchange once on a runner for every project's jobs, and keep it in jobs with a before_script of their own

2 participants