fix: attribute pipelines to the pipeline user, guard zero project ids

Problem

Pipeline attribution derived Pipeline.UserID from glJobs[0].User.ID (the first job's user). That is wrong for retried jobs and bridge/downstream pipelines, where the first job's user differs from who triggered the pipeline, and it yields 0 for pipelines with no jobs or a null job user.

Separately, deriveProjectIDs did not guard against a null project_id (value 0) on a merge request (MR) or issue. Project 0 was added to the active set and drove a guaranteed GET /projects/0/pipelines 404 that burns the collection error budget.

Fix

  1. Attribute to the pipeline's own user. Added a User field to the gitlabPipeline struct. The GitLab pipelines endpoint returns a top-level user object identifying the trigger. UserID now comes from pipeline.user.id, falling back to the first job's user only when the pipeline user is absent (for example, system-triggered pipelines). Jobs are still fetched: artifact file_type drives security and infrastructure scoring downstream. A slog.Debug line records pipelines with no attributable user (UserID stays 0).

  2. Guard ProjectID == 0 in deriveProjectIDs. A null project_id on an MR or issue is skipped, so project 0 is never requested.

  3. UserID == 0 inertness (verified, no analyzer change). A pipeline with no attributable user stays UserID: 0. The analyzer's per-user scoring (cicd, infrastructure, security) reads PipelinesByUser[userID] keyed by roster ids from /users, which never include 0, so the 0 bucket is never scored or surfaced there. One map-iteration site (scorer.go bucket build feeding teams.go's userPrimaryProjects) does range over PipelinesByUser directly and is not provably inert; that is flagged in a code comment for a follow-up analyzer guard. This MR is collector-only by scope, so no analyzer file is touched.

Validation

  • make check (go vet + full test suite) passes on Go 1.24.2.
  • go test -race ./... passes on Go 1.24.2.
  • New tests:
    • TestCollectPipelines_AttributesToPipelineUser: pipeline user.id 99 differs from jobs[0].user.id 7; asserts attribution to 99.
    • TestCollectPipelines_FallsBackToJobUser: null pipeline user falls back to job user 7.
    • TestCollectPipelines_NoAttributableUser: null user, no jobs, yields UserID 0.
    • TestDeriveProjectIDs_SkipsZero: project 0 excluded from the derived set.
    • TestDeriveProjectIDs_NoZeroPipelineRequest: recording server confirms no /projects/0/ path is ever requested.

Fails-on-old proof

Temporarily reverting attribution to jobUserID (the old jobs[0] behavior) made TestCollectPipelines_AttributesToPipelineUser fail:

pipelines_test.go:246: expected user_id=99 (pipeline user), got 7
--- FAIL: TestCollectPipelines_AttributesToPipelineUser

Restoring glp.User.ID returns the test to PASS.

🤖 Generated with Claude Code

Merge request reports

Loading