Skip to content

fix(bitbucket): never crash on a null field in a Bitbucket reply - #1286

Merged
AlexKantor87 merged 15 commits into
mainfrom
claude/1283-bitbucket
Oct 25, 2026
Merged

AlexKantor87 merged 15 commits into
mainfrom
claude/1283-bitbucket

Conversation

@AlexKantor87

@AlexKantor87 AlexKantor87 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1283.

kosli attest pullrequest bitbucket and kosli assert pullrequest bitbucket crashed when Bitbucket sent a field as null where the code expected a value. A crash prints a Go stack trace that doesn't say which field caused it. For now we treat every field as optional (#1284 will decide which fields are required).

What changed

  • Safe readers. str, obj, boolean and objects read a field and give an empty value when it's missing, null or a different type. They replace every unchecked read in bitbucket.go, on both the V1 path (assert) and the V2 path (attest).
  • Unreadable list entries still fail. A null item in values or participants returns an error that names the list and index. Skipping it could hide a "changes requested" review.
  • Empty timestamps are 0, not an error.
  • Errors where we can't carry on. A PR with no links.self.href (the next request's URL) or no id (needed to fetch commits) returns a readable error. So does a commit with no hash. An unreadable list entry's error names its commit or PR.
  • Commit paging stops. On main it loops forever if Bitbucket's next link points back to a page already read. It now stops after 1,000 pages with an error.
  • Participants with no name (a deleted account) are never approvers. In reviews only a change request is kept, as on main, because dropping it would read as no objection.

Tests

odd_replies_test.go sends realistic replies through V1 and V2 once per field, with that field made null. It checks there's never a crash, and never an error except the cases above. When a participant has no name, it checks that only that person's entries change. Before the fix, 62 cases crashed.

Full CLI suite against a local server: 3,997 tests passed.

🤖 Generated with Claude Code

AlexKantor87 and others added 2 commits October 21, 2026 17:45
Part of #1283. Provider tests use these to send their evidence code a
realistic reply once per field with that field made null, or a list
emptied, and to name the outcome: ok, error or panic, plus which
evidence fields came back changed or missing.
Every field was read with an unchecked type assertion, so a null or
missing value (an approver with no state, a PR with no author) panicked
with a Go stack trace. Fields are now read through one set of safe
getters that give the zero value, and empty timestamps parse to 0.

A null item in a list we read (values, participants) is an error naming
the list and index, not a silent skip. The only null fields that still
fail are the PR's links.self.href and id, which the next request needs.

The test nulls each value of realistic replies in turn through both
PREvidenceForCommitV1 (assert) and V2 (attest) and asserts the rule.

Part of #1283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added go Pull requests that update go code fix labels Oct 21, 2026
@claude

claude Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @AlexKantor87's task in 1m 33s —— View job


Follow-up review: BEFORE 224e2237 → AFTER b52ca3a9

  • Fetch diff between BEFORE and AFTER
  • Fetch previous review findings
  • Check status of each previous finding touched by new commits
  • Review new commits for bugs/regressions in changed lines
  • Post inline comments for new issues (none needed)
  • Post summary comment

New commits (e20143ff, c2eb4701, b52ca3a9) only touch bitbucket.go's objects() helper (added an owner parameter so null-entry errors name the commit/PR/page they belong to) and getPullRequestCommitsFromBitbucket (a commit with no hash now errors instead of being recorded with an empty SHA, numbered across pages), plus matching test updates in odd_replies_test.go.

Status of previous findings (none of their locations were touched by this push — re-check, not re-review):

  • bitbucket.go:354 (empty date → timestamp: 0) — not addressed, by design: deferred to Decide which PR evidence fields are required for each provider #1284. Thread already resolved on GitHub.
  • bitbucket.go (paging continues on missing values instead of breaking) — resolved by 747773f2's page cap. Thread already resolved on GitHub.
  • odd_replies_test.go:367 (regexp compiled per subtest) — declined as a style nit. Thread already resolved on GitHub.

mbevc1's two review comments:

  • "A commit with no hash is recorded with an empty SHA" — addressed by c2eb4701/b52ca3a9: a commit with hash: null or "" now returns fmt.Errorf("commit %d for pull request %d has no hash", ...), where the index counts across pages (confirmed by the new TestOddReplies_CommitWithNoHashFails, which checks index 0 on page 1 and 1 on page 2). oddAllowedErrors was also extended for both commit pages.
  • "Approver and review can disagree when state is null" — not addressed, out of scope here: deferred to Decide which PR evidence fields are required for each provider #1284 as noted elsewhere in this PR's thread.

New issues in the changed lines: none. All three objects() call sites were updated consistently with the new owner parameter, there are no other callers in the repo, and the new tests' expected error substrings (oddListOwners) match the new error format. I confirmed this by static reading only — I don't have permission to run go build/go test in this environment, so this isn't verified by an actual test run.

0 new inline comments.
(branch)

Comment thread internal/bitbucket/bitbucket.go
Comment thread internal/bitbucket/bitbucket.go Outdated
Comment thread internal/bitbucket/odd_replies_test.go
Base automatically changed from claude/odd-reply-helper to main October 22, 2026 08:29
AlexKantor87 and others added 7 commits October 22, 2026 10:09
# Conflicts:
#	internal/testHelpers/oddReplies.go
#	internal/testHelpers/oddReplies_test.go
Snyk fails every branch on CVE-2026-111943, a high-severity flaw in Go's
crypto/x509 fixed in 1.26.10; go.mod declared 1.26.7.

The Dockerfile built from golang:1.26-alpine3.23, so each build could
pull a different toolchain. It now names golang:1.26.10-alpine3.23 by
digest. Dependabot can't update a FROM built from ARGs, so the line is
literal, and a docker ecosystem entry (golang only) keeps the digest
current.
… advance

An approved participant with no user or display_name was counted as an
approval by nobody, which records approval evidence that fails open. It is
now an entry we can't read and errors, naming participants[i], for both
assert (V1) and attest (V2).

Commit paging followed next even when it pointed at the page just
fetched, so such a reply looped forever. It now errors instead, matching
the gitlab and github "did not advance" guards.

Part of #1283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dependabot bumps the pinned golang image on its own, and none of the
gomod updates touch go.mod's go directive. Snyk reads go.mod, so an
image-only bump would leave the next Go stdlib CVE flagged even though
the image is fixed. The test makes that bump red until go.mod moves too.
…in reviews

The previous commit made an approval with no user or display_name an
error. A deleted account is a legal reply with no identity, so any pull
request it ever approved could never be attested again, with no
workaround.

Now an approval with no identity is left out of the approvers, so it
never counts as an approval, and is kept in the reviews with an empty
name and its state, so the record still shows an unknown account
reviewed. One helper, approvedBy, decides it for V1 and V2.

Part of #1283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The earlier guard only stopped a next link that was the page just
fetched. Two pages pointing at each other still looped forever, and the
earlier commit was wrong to say it matched GitLab: GitLab also caps the
number of pages. Bitbucket's next is an opaque URL, so there is no page
number to check for going up; a cap is the guard that ends every cycle.

Commit paging now stops with an error after defaultMaxPages (1000)
pages. The same-page check is dropped: the cap covers it.

Part of #1283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AlexKantor87

Copy link
Copy Markdown
Contributor Author

This branch now includes #1291's branch, so the Go 1.26.10 change clears the Snyk check here. That's why go.mod, Dockerfile, .github/dependabot.yml and internal/version/toolchain_test.go show in this diff. They're identical to #1291 and drop out of the diff once it merges. Please review those files on #1291, not here.

no search: answers no review finding; it brings in #1291's reviewed files unchanged (git diff origin/claude/go-1.26.10 HEAD on those four files is empty).

…283-bitbucket

# Conflicts:
#	.github/dependabot.yml
@claude

claude Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Follow-up review: BEFORE 5b6d96b..AFTER 507500b

The new commits (9a1287f8, 507500b3) only merge in origin/main, which already includes #1291. The only line-level change in git diff 5b6d96b..507500b is .github/dependabot.yml's schedule tweak — identical to #1291 and out of scope here per the PR description's note to review that on #1291 instead. No bitbucket-related files were touched by this push.

Status of previous findings (none of these locations were touched by the new commits, so this is a re-check, not a re-review):

  • bitbucket.go:338 (possible infinite loop if values is missing/non-list) — resolved, fixed by 747773f2 (defaultMaxPages cap, confirmed present in the code). Thread already resolved.
  • bitbucket.go:345 (empty date → timestamp: 0) — not addressed, by design: deferred to Decide which PR evidence fields are required for each provider #1284 per author reply. Thread already resolved (acknowledged, no code change expected here).
  • odd_replies_test.go:313 (regexp compiled per subtest) — not addressed, declined as a style nit per author reply. Thread already resolved.

New issues in the changed lines: none. The only diff is the dependabot schedule change, which is trivial and already covered by #1291.

No new inline comments needed.

View job run

AlexKantor87 and others added 2 commits October 25, 2026 14:00
b49f8b9 kept a nameless approval in Reviews with state APPROVED. Policies
read Reviews, so one that counts approvals there would count an approval
by nobody: the same fail-open the approvers fix closed.

This restores main's rule in bitbucketReviews exactly: a review with no
name is kept only when it requests changes, since dropping that would read
as none given. Nameless approvals stay out of Approvers. Never an error.

Part of #1283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The nameless-participant check only looked at nameless entries, so a bug
that put a placeholder name on an approver, or dropped another person's
named review, still passed. Compare both lists with the evidence for the
same replies with the name present: only that person's entries may change.

Part of #1283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Follow-up review: BEFORE 507500b → AFTER 224e223

New commits 423653d5 (fix) and 224e2237 (test) touch only bitbucketReviews() in internal/bitbucket/bitbucket.go (lines ~280–301) and its tests — a different location from all three previously-raised findings.

Status of previous findings (none of their locations were touched by this push, so this is a re-check, not a re-review):

  • bitbucket.go:350 (empty date → timestamp: 0) — already resolved/acknowledged (deferred to Decide which PR evidence fields are required for each provider #1284). Thread confirmed resolved.
  • bitbucket.go:343 (paging continues on missing values instead of breaking) — already resolved by 747773f2's page cap. Thread confirmed resolved.
  • odd_replies_test.go:357 (regexp compiled per subtest) — already declined as a style nit by the author. Thread confirmed resolved.

New issues in the changed lines: none. The fix correctly keeps nameless changes_requested participants in Reviews (so a hidden "changes requested" from a deleted account is never dropped) while excluding nameless approvals and nameless comments from both Approvers and Reviews. odd_replies_test.go's extended oddNameless cases and reviews_test.go's updated fixture correctly exercise all three nameless states (approval, changes_requested, comment) and match the implementation.

Note: I wasn't able to run go build/go test in this environment (commands beyond git require approval not available here) — review is based on static reading of the diff and existing test fixtures.

0 new inline comments.

View job run

@mbevc1

mbevc1 commented Oct 25, 2026

Copy link
Copy Markdown
Contributor
  1. A commit with no hash is recorded with an empty SHA.
    "hash": null gives no error, and the payload carries "sha1": "". main panics here, so erroring breaks nothing that works today. GitLab and Azure both error on a commit without an id.
    Suggestion: error when hash is missing, and add pullrequest_commits_* values[0].hash to oddAllowedErrors.

  2. Approver and review can disagree when state is null (for Decide which PR evidence fields are required for each provider #1284).
    A participant with approved: true, state: null is an approver (V2 with an empty State), but COMMENTED in reviews. Approvers come from approved, reviews from state. On main, V1 already counts this participant as an approver, and V2 panicked.

An error for an unreadable list entry named only the list and index, such
as values[0], so a user couldn't tell which commit or pull request it was
about. It now says what the list belongs to: the commit for its pull
requests, or the pull request for its participants and commits.

Part of #1283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A commit page entry with a null hash was recorded with an empty SHA. A
commit with no SHA can't be attested, so it now returns an error naming
the commit's index and the pull request.

Part of #1283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A commit's index restarted at 0 on each page of commits, so the no-hash
error could name the same index for two different commits. It now counts
across pages, and an unreadable entry on a commit page names its page. An
unreadable participant's error named the pull request by its web link,
which is empty when the reply has no links.html. It now uses the API URL,
which every request needs.

Part of #1283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AlexKantor87

Copy link
Copy Markdown
Contributor Author

Thanks @mbevc1.

1. Commit with no hash. Fixed in c2eb470. It now returns an error naming the commit's index and the pull request. b52ca3a counts that index across pages, so one index always means one commit. I checked the other providers for the same gap. GitLab (on main since #1287) and Azure (#1288) already fail on a commit with no id. GitHub's schema says a commit's oid is never null: GitHub nulls the whole commit instead, which already fails.

2. Approver and review disagreeing. Added to #1284. The same thing happens on Azure (a vote of 5 is APPROVED in reviews but not an approver) and on GitLab (an approver who also requested changes).

Same sweep, after your #1287 nit: errors for an unreadable list entry now name their commit or pull request, plus the page for commit pages (e20143f).

search: grep -nE 'objects(|has no hash' internal/bitbucket/bitbucket.go

141: objects(responseData, "values", "commit "+commit)
187: objects(responseData, "participants", "pull request "+prApiUrl)
344: objects(responseData, "values", fmt.Sprintf("pull request %d, page %d", prID, pages+1))
351: fmt.Errorf("commit %d for pull request %d has no hash", len(allCommits), prID)

verified: sed -n '349,352p' internal/bitbucket/bitbucket.go

		for _, commit := range commits {
			if str(commit, "hash") == "" {
				return nil, fmt.Errorf("commit %d for pull request %d has no hash", len(allCommits), prID)
			}

mutation: drop the hash check -> TestOddReplies_CommitWithNoHashFails red (2 cases)
mutation: drop the owner from the objects() error -> TestOddReplies red (8 cases)

@AlexKantor87
AlexKantor87 merged commit 62128d7 into main Oct 25, 2026
23 of 25 checks passed
@AlexKantor87
AlexKantor87 deleted the claude/1283-bitbucket branch October 25, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants