Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 93 additions & 7 deletions internal/forge/bitbucket/checks_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,104 @@ package bitbucket

import (
"context"
"fmt"
"strings"

"go.abhg.dev/gs/internal/forge"
"go.abhg.dev/gs/internal/gateway/bitbucket"
)

// ChecksByChange reports per-change rolled-up and per-run check state
// for each of the given changes.
// rollupFromCommitStatuses collapses a Bitbucket build-status list into
// the forge rollup taxonomy.
//
// TODO: real implementation lands on a follow-up branch.
// This stub returns one nil per id to satisfy the [forge.Repository]
// interface while the schema branch lands standalone.
// Failure wins over pending; pending wins over passed; absence of any
// statuses returns ChecksRollupNone (no CI configured/reported).
// Stopped statuses are operator-cancelled non-failures and roll up as
// passed.
func rollupFromCommitStatuses(
statuses []bitbucket.CommitStatus,
) forge.ChecksRollupState {
if len(statuses) == 0 {
return forge.ChecksRollupNone
}

pending := false
for _, s := range statuses {
switch s.State {
case bitbucket.CommitStatusFailed:
return forge.ChecksRollupFailed
case bitbucket.CommitStatusInProgress:
pending = true
}
}
if pending {
return forge.ChecksRollupPending
}
return forge.ChecksRollupPassed
}

// ChecksByChange reports per-change rolled-up and per-run build state
// for each of the given pull requests.
//
// One getPullRequest + (when the PR has a source commit) a paginated
// CommitStatusList walk per PR. Build statuses are reported in
// Bitbucket's natural order.
func (r *Repository) ChecksByChange(
_ context.Context, ids []forge.ChangeID,
ctx context.Context, ids []forge.ChangeID,
) ([]*forge.ChecksReport, error) {
return make([]*forge.ChecksReport, len(ids)), nil
out := make([]*forge.ChecksReport, len(ids))
for i, id := range ids {
checks, err := r.changeChecks(ctx, id)
if err != nil {
return nil, fmt.Errorf("change %v: %w", id, err)
}
out[i] = checks
}
return out, nil
}

func (r *Repository) changeChecks(
ctx context.Context, id forge.ChangeID,
) (*forge.ChecksReport, error) {
prID := mustPR(id)
pr, err := r.getPullRequest(ctx, prID.Number)
if err != nil {
return nil, fmt.Errorf("get pull request: %w", err)
}

if pr.Source.Commit == nil {
return &forge.ChecksReport{Rollup: forge.ChecksRollupNone}, nil
}

var statuses []bitbucket.CommitStatus
opt := &bitbucket.CommitStatusListOptions{}
for {
page, resp, err := r.client.CommitStatusList(
ctx, r.workspace, r.repo, pr.Source.Commit.Hash, opt,
)
if err != nil {
return nil, fmt.Errorf("get commit statuses: %w", err)
}

statuses = append(statuses, page.Values...)
if resp.NextURL == "" {
break
}
opt.PageURL = resp.NextURL
}

out := &forge.ChecksReport{Rollup: rollupFromCommitStatuses(statuses)}
out.Runs = make([]forge.CheckRun, 0, len(statuses))
for _, s := range statuses {
name := s.Name
if name == "" {
name = s.Key
}
out.Runs = append(out.Runs, forge.CheckRun{
Name: name,
State: strings.ToLower(s.State),
URL: s.URL,
})
}
return out, nil
}
68 changes: 68 additions & 0 deletions internal/forge/bitbucket/checks_report_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package bitbucket

import (
"testing"

"github.com/stretchr/testify/assert"
"go.abhg.dev/gs/internal/forge"
"go.abhg.dev/gs/internal/gateway/bitbucket"
)

func TestRollupFromCommitStatuses(t *testing.T) {
tests := []struct {
name string
statuses []bitbucket.CommitStatus
want forge.ChecksRollupState
}{
{
name: "Empty",
want: forge.ChecksRollupNone,
},
{
name: "AllSuccessful",
statuses: []bitbucket.CommitStatus{
{State: bitbucket.CommitStatusSuccessful},
{State: bitbucket.CommitStatusSuccessful},
},
want: forge.ChecksRollupPassed,
},
{
name: "Failed",
statuses: []bitbucket.CommitStatus{
{State: bitbucket.CommitStatusSuccessful},
{State: bitbucket.CommitStatusFailed},
},
want: forge.ChecksRollupFailed,
},
{
name: "InProgress",
statuses: []bitbucket.CommitStatus{
{State: bitbucket.CommitStatusSuccessful},
{State: bitbucket.CommitStatusInProgress},
},
want: forge.ChecksRollupPending,
},
{
name: "FailedTrumpsInProgress",
statuses: []bitbucket.CommitStatus{
{State: bitbucket.CommitStatusInProgress},
{State: bitbucket.CommitStatusFailed},
},
want: forge.ChecksRollupFailed,
},
{
name: "StoppedIsPassed",
statuses: []bitbucket.CommitStatus{
{State: bitbucket.CommitStatusSuccessful},
{State: bitbucket.CommitStatusStopped},
},
want: forge.ChecksRollupPassed,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, rollupFromCommitStatuses(tt.statuses))
})
}
}
7 changes: 7 additions & 0 deletions internal/gateway/bitbucket/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,13 @@ func (c *Client) PullRequestMerge(
type CommitStatus struct {
Key string `json:"key"`
State string `json:"state"`

// Name is the human-readable label for the build status, if any.
// Falls back to Key when empty.
Name string `json:"name,omitempty"`

// URL links to the build's detail page, if any.
URL string `json:"url,omitempty"`
}

// CommitStatusCreateRequest is the request body for creating a build status.
Expand Down
Loading