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 fetch into 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 config

CI 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):

  1. Gated on the existing FF_GIT_URLS_WITHOUT_TOKENS rather than introducing a new feature flag. Aligns with the team's FF-reduction direction.
  2. GIT_CONFIG_NOSYSTEM is not set, and the seed file includes the runner user's ~/.gitconfig via [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.
    • none and empty additionally assert the credential-helper-only extConfigFile bootstrap.
    • fetch and clone continue to assert the full setupExternalGitConfig call.

Manual

  1. Enable FF_GIT_URLS_WITHOUT_TOKENS: "true" on a job that uses a private submodule.
  2. Add to script::
    git submodule sync
    git submodule update --init
    git fetch origin
  3. Confirm the commands authenticate (without this MR, they fail with credential prompt errors).
  4. Confirm $GIT_CONFIG_GLOBAL is set in the build environment and points at a file under the job's .tmp directory.
  5. Confirm .git/config does not contain token-bearing URLs.
  6. Confirm the raw CI_JOB_TOKEN value does not appear in the job log even with CI_DEBUG_TRACE: "true".
  7. Toggle FF_GIT_URLS_WITHOUT_TOKENS to false and re-run; verify the legacy include.path / credential-helper flow still works unchanged.
  8. With GIT_STRATEGY: none and FF=true, run git clone https://gitlab.com/<private-project>.git /tmp/foo from script:; verify it succeeds (it failed pre-fix with could 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)

Edited by Romuald Atchadé

Merge request reports

Loading