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
kosli-cli 2.36.6 Created-by: HarmonybrewBot Commit-by: HarmonybrewBot Merged-by: HarmonybrewBot Description: Created by `brew bump` --- Created with `brew bump-formula-pr`.<details> <summary>release notes</summary> <pre># Bug fixes - Empty `--attachments` or `--template` elements (e.g. from unset shell variables) are now rejected with a clear error instead of being silently dropped. - An empty `KOSLI_CONFIG_FILE` environment variable no longer suppresses config file loading; it is treated as unset and falls back to the default. - An empty `KOSLI_API_TOKEN` environment variable no longer skips decryption of a config-file token. - A boolean flag given an empty value (e.g. `--compliant ""` or `--new-compliance-status ""`) is now rejected with a clear error naming the flag, preventing the opposite compliance verdict from being recorded silently. - Config file or environment values that cannot be applied to a flag now produce an error naming the flag and its source, instead of silently failing. ## What's Changed * fix(attest): always serialise commits in pull request attestations by @dangrondahl in kosli-dev/cli#1083 * fix(docs): correct invalid regex in snapshot ecs help examples by @dangrondahl in kosli-dev/cli#1080 * fix(config): treat empty KOSLI_* env vars as unset, not as set by @JonJagger in kosli-dev/cli#1089 * fix(config): report config values that cannot be applied to their flag by @JonJagger in kosli-dev/cli#1090 * fix(flags): reject an empty element in a multi-value flag by @JonJagger in kosli-dev/cli#1092 * fix(flags): reject an empty value written after a boolean flag by @JonJagger in kosli-dev/cli#1091 * chore(deps): bump the github-actions-dependencies group with 3 updates by @dependabot[bot] in kosli-dev/cli#1084 * chore(deps): bump the go-dependencies group with 9 updates by @dependabot[bot] in kosli-dev/cli#1085 * chore(deps): bump anthropics/claude-code-action from 1.0.189 to 1.0.191 in the github-actions-dependencies group by @dependabot[bot] in kosli-dev/cli#1094 * chore(deps): bump the go-dependencies group with 7 updates by @dependabot[bot] in kosli-dev/cli#1095 * chore(deps): bump google.golang.org/protobuf from 1.36.12-0.20260120151049-f2248ac996af to 1.36.12 by @dependabot[bot] in kosli-dev/cli#1096 **Full Changelog**: https://github.com/kosli-dev/cli/compare/v2.36.5...v2.36.6</pre> <p>View the full release notes at <a href="https://github.com/kosli-dev/cli/releases/tag/v2.36.6">https://github.com/kosli-dev/cli/releases/tag/v2.36.6</a>.</p> </details> <hr> See merge request: Harmonybrew/homebrew-core!16566
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.