Skip to content

bug: GitLab PR attestation sends an invalid payload when the MR has no commits (CI red) #1081

Description

@dangrondahl

Symptom

TestAttestGitlabPRCommandTestSuite fails 10/10 subtests. Every failure is the same rejected payload:

Error: Input payload validation failed: map[
  data_json.pull_requests[0].FoundPullRequestV1.author: Extra inputs are not permitted
  data_json.pull_requests[0].FoundPullRequestV1.title / base_ref / head_ref / created_at / merged_at: Extra inputs are not permitted
  data_json.pull_requests[0].FoundPullRequestV2.commits: Field required
]

First seen in CI on 2026-08-10 (run 31365784844, on an unrelated docs PR #1080). The previous run, 2026-08-04 (run 30917903429), was green on identical code.

Root cause

  1. GitLab now returns 200 [] from GET /projects/:id/merge_requests/:iid/commits for the test's fixture MR (kosli-dev/merkely-gitlab-demo !1, merged 2024-10-10).
  2. GetMergeRequestCommits (internal/gitlab/gitlab.go:166) returns an empty slice with err == nil, so nothing aborts.
  3. PREvidence.Commits is tagged json:"commits,omitempty" (internal/types/types.go:14), so the empty slice is dropped from the JSON entirely.
  4. The API validates pull_requests against List[FoundPullRequestV1 | FoundPullRequestV2]. V1 forbids the extended fields; V2 requires commits. Matching neither, both branches are reported.

commits has been required on FoundPullRequestV2 since the model was introduced (server, 2025-07-03); nothing changed server-side.

Why it changed now

GitLab deleted merge-request commit rows for old merge requests some time between 2026-08-04 14:22 UTC and 2026-08-10 07:33 UTC.

Evidence it was present on 2026-08-04: the green run's coverage reports GetMergeRequestCommits at 81.2%. The function has 16 statements across 9 blocks; a successful call that iterates commits covers 13 (81.25%), while a call returning [] covers only 7 (43.75%). The loop body ran.

The retention boundary, bisected against gitlab-org/gitlab (project 278964) anonymously:

MR merged_at /commits
!211375 2025-11-19 []
!214116 2025-11-25T20:21Z []
!214312 2025-11-26T23:57Z 3 commits
!214582 2025-11-28 1 commit
!249198 2026-08-10 1 commit

Cutoff: between 2025-11-25T20:21Z and 2025-11-26T23:57Z. Merge requests whose diff predates it return []; newer ones return commits. Reproducible on GitLab's own repository, so it is not specific to our project or token.

The merge_request_diffs record survives — /merge_requests/1/versions still reports state: "collected", real_size: "1" — and the head commit is still readable via /repository/commits/:sha. So this looks like loss of merge_request_diff_commits rows, not the diff-file pruning described in gitlab-foss#37639, which explicitly says those rows should be retained.

Unknown: whether the deletion was one-off or is a rolling window. If rolling, the affected range grows over time. Re-probing !212000 and !214582 in a week would tell.

Customer impact

Low. kosli attest pullrequest gitlab normally runs immediately after a merge, when the MR diff is minutes old and well inside the retained window. It fails only when attesting a commit whose MR diff predates the cutoff — backfills, replays, or re-runs of historical pipelines. kosli assert pullrequest gitlab uses the same V2 path but posts no attestation, so it is unaffected.

Proposed fix

  1. Drop omitempty from Commits in internal/types/types.go:14 so the field always serialises. commits: [] validates against FoundPullRequestV2, and the server's set_relevant_commits validator handles an empty list (the merge commit is still recorded).
  2. Decide whether a zero-commit MR is legitimate. If not, GetMergeRequestCommits should error explicitly rather than silently attest an MR with no commits.
  3. Normalise Approvers from nil to []any{} in newPRGitlabEvidenceV2, as the V1 path already does (internal/gitlab/gitlab.go:95). The API tolerates null today, so this is consistency, not a fix.
  4. Add a serialisation test asserting commits is always present in a V2 payload.
  5. Stop pinning a 2024 MR in the test fixture. The suite depends on GitLab's live answer for a two-year-old merge request staying constant; it didn't, and that fixture can never return commits again. Either use a fake (the repo already uses httpfake elsewhere) or a fixture that is maintained.

PREvidence is shared by GitHub, Bitbucket and Azure, so the omitempty defect is latent wherever a provider can return zero commits.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions