Skip to content

platform mode: non-root image cannot read the runner's root-owned checkout, 5 controls turn not_evaluable #464

Description

@thomasboni

Symptom

In a GitLab CI job using the plumber component (image getplumber/plumber v0.4.56, platform mode), the run logs:

ci config digest: not computed (no checkout of the analyzed project) - treated as divergent
level=warning msg="Could not read the project's own CI file; continuing on the platform's merged configuration" action=GetFullGitlabCI context=platform/gitlab err="<nil>" errPlatform="404 Not Found"
level=warning msg="The project's own CI file was not read; the controls that compare against it will report not_evaluable"

and ends with 5 controls Not Evaluated: 4 include_attribution_unavailable (external refs, includes up to date, forbidden versions, security jobs not weakened) and 1 raw_config_unavailable (job variables override). The job DID check out the repository (Checking out 309312ed as detached HEAD (ref is main) in the runner log). Observed on gitlab.com SaaS runners, job https://gitlab.com/bigtech-150/frontend/angular-dashboard-4/-/jobs/16421236156.

Root cause

The image ends with USER plumber (uid 65532), while the GitLab docker executor's helper clones $CI_PROJECT_DIR as root. Git 2.35.2+ then refuses the repository:

$ docker run --rm -v <root-owned repo>:/builds/x -w /builds/x --entrypoint /usr/bin/git getplumber/plumber@sha256:7191202... remote get-url origin
fatal: detected dubious ownership in repository at '/builds/x'

utils.DetectGitRemote() shells out to git remote get-url origin and returns nil on any error, so conf.GitRepoRoot == "" and conf.CheckoutIsAnalyzedProject == false (cmd/analyze_gitlab.go buildGitLabConf). From there:

  1. computeLocalCIDigest refuses the digest (cmd/platform_mode.go), the run is treated as divergent and goes through the resolve endpoint (Source = SourceResolved).
  2. useCheckoutInPlatformMode is false (control/task.go), so GetFullGitlabCI falls to the GitLab raw-file API arm. In platform mode resolveGitLabToken allows an empty token, so the read is anonymous and a private project answers 404 (masked 403). ConfString stays empty beside a merged pipeline with jobs, so RawConfigUnavailable = true and pipelineMustNotOverrideJobVariables reports raw_config_unavailable.
  3. ConfigAndIncludesAgree() is Source == SourceSnapshot only (internal/platform/run.go), so every enabled include-attribution control reports include_attribution_unavailable.

So a perfectly normal platform-mode job on a private project loses 5 controls whenever the runner's checkout is root-owned, which is the default on the docker executor.

Suggested fix

Either or both:

  • In the image (or in templates/plumber.yml before plumber analyze): git config --global --add safe.directory '*' (or safe.directory "$CI_PROJECT_DIR"), so the non-root user can read the runner's checkout.
  • Make DetectGitRemote() log the git error at warning level instead of silently returning nil, so the reason for "no checkout of the analyzed project" is visible in the job log.

Separately (smaller): the resolve endpoint's 200 already carries includes, but ResolvedConfig has no Includes field and ConfigAndIncludesAgree hard-codes the snapshot source; wiring that through would keep the 4 include controls evaluable on every digest-divergent run, independent of this bug.

Found by the platform team on 2026-09-10 while explaining the job log above; platform side has nothing to change for this.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions