fix(attest): always serialise commits in pull request attestations - #1083
Conversation
The API validates pull request attestations against FoundPullRequestV1 | FoundPullRequestV2. V1 forbids the extended fields and V2 requires "commits", so a payload without "commits" matches neither and is rejected. PREvidence.Commits was tagged omitempty, so a provider returning no commits silently dropped the field. GitLab started returning an empty commit list for merge requests whose diff predates ~2025-11-26, which broke every gitlab pull request attestation against such a commit. Drop omitempty and normalise nil to [] in MarshalJSON: a nil slice would otherwise serialise as null, which fails validation just as surely as a missing field. Sending commits: [] validates, and the server records the merge commit regardless. Refs #1081
The suite pinned merkely-gitlab-demo !1, merged 2024-10-10. GitLab has stopped returning commits for merge requests whose diff predates ~2025-11-26, so that fixture can never return commits again and the suite broke without anything in this repo changing (#1081). Add NewGitlabRetrieverFunc and FakeGitlabClient, mirroring the GitHub seam and FakeGitHubClient, and point the suite at the fake seeded with a merge request that has a commit. Tests no longer depend on GitLab's data retention, and they regain the coverage lost when an empty commit list became a passing payload. The git repo is still cloned for real, so commit resolution stays honest; only the merge request API is faked. Refs #1081
|
Claude finished @dangrondahl's task in 3m 25s —— View job Claude PR Review
Static analysis
VerdictThis is a well-reasoned, correctly-scoped fix. The root-cause analysis in the description is accurate, and I independently verified the two claims the fix rests on:
What's good 👍
Observations (non-blocking)
On the "Not included" listThe deferrals are all reasonable, and I agree with keeping them separate. The most valuable follow-up is the zero-commits warning (blocked only on Nothing blocking. CI is indeed the first real signal for the fake wiring and the local-server suite, as the description notes. |
… requests FakeGitlabClient returned f.MRsByCommit[commit] directly, which is nil for an absent key. The real client builds its slice up front and returns an empty one, and the difference is not cosmetic: nil serialises as null, and the API rejects null for pull_requests with data_json.pull_requests: Input should be a valid list [input: None] Mirror FakeGitHubClient, which already guards this. Refs #1081
Fixes #1081.
GitLab stopped returning commits for merge requests whose diff predates ~2025-11-26.
GetMergeRequestCommitspassed the empty list through as an empty slice with no error,omitemptythen deletedcommitsfrom the JSON, and the API rejected a payload matching neitherFoundPullRequestV1(which forbids the extended fields) norFoundPullRequestV2(which requirescommits).fix(attest): always serialise commitsDrop
omitemptyfromPREvidence.Commitsand normalise nil to[]inMarshalJSON. A nil slice would otherwise serialise asnull, which fails validation just as surely as a missing field, so removing the tag alone is not enough.commits: []validates againstFoundPullRequestV2, and the server'sset_relevant_commitsvalidator handles an empty list — the merge commit is still recorded.Safe for the V1 shape:
PREvidenceForCommitV1is only reached byassert pullrequest bitbucketandassert pullrequest azure, which count merge requests and post nothing. GitHub's hybrid fallback builds V2-shaped evidence viaPREvidenceByPRNumber. The only serialised shape is V2.test(attest): run the gitlab suite against a fakeThe suite pinned
merkely-gitlab-demo!1, merged 2024-10-10 — a fixture that can never return commits again. AddNewGitlabRetrieverFuncandFakeGitlabClient, mirroring the existing GitHub seam andFakeGitHubClient, and point the suite at a fake seeded with a merge request that has a commit.Without this the suite would pass while asserting a zero-commit attestation, losing the coverage the fix is meant to protect. The git repo is still cloned for real, so commit resolution stays honest; only the merge request API is faked.
Verification
go build ./...,go vet,make lint(0 issues), andinternal/types+internal/gitlaball pass.TestPREvidenceAlwaysSerialisesCommitswas confirmed red before the fix and green after.TestAttestGitlabPRCommandTestSuitehas not been run. It needs the local Kosli server, andCloneGitRepoadditionally fails on macOS withopen .../.git: is a directory— a/varsymlink quirk reproduced on an unmodified tree, so pre-existing and unrelated. CI is the first real signal for the fake wiring.Note the suite is no longer gated on
KOSLI_GITLAB_TOKEN, so it now runs rather than skips when that secret is absent.Not included
GitlabConfighas no logger; worth doing, but separate.Approversnil to[]— cosmetic, the API acceptsnull.gitlab_contract_test.gobehindmake test_contract_gitlab. Blocked on whether GitLab's retention is a rolling window or a one-off deletion: if rolling, a contract test must select a recent merge request dynamically rather than pin a SHA, or it breaks the same way in ~9 months. Re-probinggitlab-org/gitlab!212000 and !214582 in a week answers it (see bug: GitLab PR attestation sends an invalid payload when the MR has no commits (CI red) #1081).assert pullrequest gitlabstill hits live GitLab with the same 2024 fixture. It passes today because assert never needs commits, but it carries the same fragility.