Make git credential helper visible to user-authored commands
What does this MR do?
When FF_GIT_URLS_WITHOUT_TOKENS=true, Runner writes a per-job seed file containing two [include] directives and exports GIT_CONFIG_GLOBAL pointing at the seed:
[include]
path = $HOME/.gitconfig ; runner user's existing global config
[include]
path = <per-job ext.conf> ; runner's credential helper, correlation header, etc.Every git invocation in the job process — Runner-managed AND user-authored, in any CWD, regardless of GIT_STRATEGY — reads the seed file as its global config and therefore sees Runner's credential helper. The runner user's existing global config (proxy settings, custom CA bundles, per-host helpers, etc.) is preserved via the first [include].
FF_GIT_URLS_WITHOUT_TOKENS=false is untouched; the file-based include.path mechanism remains the only transport for that path because its ext.conf embeds token-bearing insteadOf rewrites that must stay scoped to runner-invoked git.
Bootstrap path for GIT_STRATEGY=none / GIT_STRATEGY=empty
The seed file's second [include] points at the per-job extConfigFile. That file is normally written by setupTemplateDir, which is only called from the GitFetch/GitClone arms of handleGetSourcesStrategy. Under GitNone/GitEmpty the runner does no checkout, so setupTemplateDir is never called and the include used to resolve to a non-existent file — silently providing no credential helper. User-authored git clone / git fetch in those jobs fell back to interactive credential prompts and failed.
When FF is on and the strategy is none or empty, writeGetSourcesScript now bootstraps a minimal extConfigFile containing only the credential helper. The fetch/clone path is untouched: it continues to write the full extConfigFile (helper, Gitaly correlation header, bundleURI, SSL, insteadOf rewrites) via setupTemplateDir.
Why was this MR needed?
FF_GIT_URLS_WITHOUT_TOKENS removes the job token from git remote URLs and installs a credential helper instead. Today that helper is written to a per-job extConfigFile reached via an include.path chain set up in .git/config (template-based for the project, per-submodule write for submodules). That chain breaks in real scenarios — most notably:
GIT_STRATEGY=none/GIT_STRATEGY=empty, where Runner never initializes the project repo- Submodule operations run by the user before Runner's
configureSubmoduleCredentials - Manual
git clone/git fetchinto directories outside the project tree
In those cases, user-authored git commands in before_script / script / after_script (e.g. git submodule update --init, git fetch origin) fail to authenticate. Reported in #38801 (closed).
Previous attempt (now abandoned)
The first revision of this MR used Git's GIT_CONFIG_COUNT / GIT_CONFIG_KEY_<n> / GIT_CONFIG_VALUE_<n> env-var transport. The credential helper definition, username, and credential.interactive=never were emitted as runtime env-var pairs through Build.GetAllVariables, with the empty-string entry that resets the helper chain at the URL scope emitted as GIT_CONFIG_VALUE_n="".
That approach failed on git ≤ 2.39, which rejects empty GIT_CONFIG_VALUE_n with:
error: missing config value GIT_CONFIG_VALUE_0
fatal: unable to parse command-line configCI runs git 2.39.5, so every git invocation aborted before doing anything. The empty-value reset is fundamentally not portable via GIT_CONFIG_* env vars on the git versions Runner needs to support. The branch was reset and rewritten with the current approach.
Why the current approach works on git ≤ 2.39
GIT_CONFIG_GLOBAL is supported from git 2.32 onwards (already a de facto baseline; step-runner uses it in setupGlobalGitConfig). The empty-value chain reset that broke under GIT_CONFIG_* is fine here because it lives inside the config file (git config -f file --replace-all credential.<host>.helper ""), not in an env var. One mechanism (file-based), one transport (GIT_CONFIG_GLOBAL), works on every git version we ship against.
Inspired by !6665 (closed)
@guillaume.chauvel's MR !6665 (closed) introduces FF_GIT_CONFIG_LOCAL_ONLY which uses GIT_CONFIG_GLOBAL together with GIT_CONFIG_NOSYSTEM for full isolation from the runner user's git config. This MR adopts the same GIT_CONFIG_GLOBAL mechanism, and cherry-picks his commit moving the seed-file write before pre_clone_script plus the integration-test scaffolding (TestCredSetup), with two deliberate deltas from !6665 (closed):
- Gated on the existing
FF_GIT_URLS_WITHOUT_TOKENSrather than introducing a new feature flag. Aligns with the team's FF-reduction direction. GIT_CONFIG_NOSYSTEMis not set, and the seed file includes the runner user's~/.gitconfigvia[include]. The user's existing global config (proxy, CA bundles, etc.) still applies. !6665 (closed)'s stronger isolation story remains valuable as a separate, deliberate opt-in.
What's the best way to test this MR?
Automated
go test -tags '!integration' ./shells/...Test coverage includes:
TestSetupGlobalGitConfigSeed— pins the writer-call sequence for the seed file creation.TestAbstractShell_writeGetSourcesScript— covers all four strategies (none,empty,fetch,clone) with and without FF. Under FF=true:- All strategies assert the seed file is written.
noneandemptyadditionally assert the credential-helper-onlyextConfigFilebootstrap.fetchandclonecontinue to assert the fullsetupExternalGitConfigcall.
Manual
- Enable
FF_GIT_URLS_WITHOUT_TOKENS: "true"on a job that uses a private submodule. - Add to
script::git submodule sync git submodule update --init git fetch origin - Confirm the commands authenticate (without this MR, they fail with credential prompt errors).
- Confirm
$GIT_CONFIG_GLOBALis set in the build environment and points at a file under the job's.tmpdirectory. - Confirm
.git/configdoes not contain token-bearing URLs. - Confirm the raw
CI_JOB_TOKENvalue does not appear in the job log even withCI_DEBUG_TRACE: "true". - Toggle
FF_GIT_URLS_WITHOUT_TOKENStofalseand re-run; verify the legacy include.path / credential-helper flow still works unchanged. - With
GIT_STRATEGY: noneand FF=true, rungit clone https://gitlab.com/<private-project>.git /tmp/foofromscript:; verify it succeeds (it failed pre-fix withcould not read Username for 'https://gitlab.com').
Scope notes
Step-runner / concrete path (FF_USE_CONCRETE=true) is deferred to a follow-up. Its setupGlobalGitConfig lifecycle is scoped to the get_sources stage; the seed file would be deleted before user-script stages run. Cleanly fixing it requires moving the lifetime up to the runner level, which is outside this MR's scope. For now, FF_USE_CONCRETE=true users keep today's behavior on this dimension.
FF=false behavior is unchanged. No code path in the FF=false branch is touched. Existing include.path machinery, SetupGitCredHelper writes, submodule configureSubmoduleCredentials, and the token-bearing insteadOf rewrites in ext.conf all stay as today.
A separate runner bug (fatal: transport 'file' not allowed) is not addressed here. Manual testing surfaced #38908 (38 upvotes) and #39384, which describe a longstanding state-corruption pattern when jobs mix GIT_SUBMODULE_STRATEGY: recursive with subsequent GIT_SUBMODULE_STRATEGY: none + user-script git submodule update --init against a persistent docker volume. Root cause is writeGitCleanupAllConfigs deleting per-submodule config files that the subsequent user-script jobs never restore, so remote.origin.url is missing when git fetch origin runs and git falls back to inferring origin as a local path which protocol.file.allow=user (git 2.38+ default) then blocks. Reproducible on main, independent of FF_GIT_URLS_WITHOUT_TOKENS. Documented workaround in the upstream issues: clear-docker-cache prune-volumes or GIT_STRATEGY: clone.
What are the relevant issue numbers?
Validation target: gitlab-org/gitlab#593467 Related: #38801 (closed) (the user-facing bug this fixes) Inspired by: !6665 (closed) (community contribution from @guillaume.chauvel)