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
89 changes: 82 additions & 7 deletions internal/forge/gitlab/checks_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,93 @@ package gitlab

import (
"context"
"fmt"

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

// ChecksByChange reports per-change rolled-up and per-run check state
// for each of the given changes.
// rollupFromPipelineStatus collapses a GitLab pipeline status into the
// forge rollup taxonomy. Canceled/manual/skipped pipelines are
// user-initiated non-failures and roll up as passed. Unknown statuses
// default to pending — GitLab adds new pipeline states over time and
// reporting them as pending is safer than reporting them as failed.
func rollupFromPipelineStatus(status string) forge.ChecksRollupState {
switch status {
case gitlab.PipelineStatusSuccess,
gitlab.PipelineStatusSkipped,
gitlab.PipelineStatusCanceled,
gitlab.PipelineStatusManual:
return forge.ChecksRollupPassed
case gitlab.PipelineStatusFailed:
return forge.ChecksRollupFailed
case gitlab.PipelineStatusCreated,
gitlab.PipelineStatusWaitingForResource,
gitlab.PipelineStatusPreparing,
gitlab.PipelineStatusPending,
gitlab.PipelineStatusRunning,
gitlab.PipelineStatusScheduled:
return forge.ChecksRollupPending
default:
return forge.ChecksRollupPending
}
}

// ChecksByChange reports per-change rolled-up and per-job pipeline
// state for each of the given merge requests.
//
// 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.
// One MergeRequestGet + (when a pipeline exists) one PipelineJobsList
// call per MR. The job list is reported in pipeline order (stages
// first, then job order within stages) which is the most useful order
// for both UI surfaces and tooltips.
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) {
mrID := mustMR(id)
mr, _, err := r.client.MergeRequestGet(ctx, r.repoID, mrID.Number, nil)
if err != nil {
return nil, fmt.Errorf("get merge request: %w", err)
}

if mr.HeadPipeline == nil {
return &forge.ChecksReport{Rollup: forge.ChecksRollupNone}, nil
}

out := &forge.ChecksReport{
Rollup: rollupFromPipelineStatus(mr.HeadPipeline.Status),
URL: mr.HeadPipeline.WebURL,
}

if mr.HeadPipeline.ID == 0 {
return out, nil
}

jobs, _, err := r.client.PipelineJobsList(ctx, r.repoID, mr.HeadPipeline.ID)
if err != nil {
return nil, fmt.Errorf("list pipeline jobs: %w", err)
}

out.Runs = make([]forge.CheckRun, 0, len(jobs))
for _, j := range jobs {
out.Runs = append(out.Runs, forge.CheckRun{
Name: j.Name,
State: j.Status, // already lowercase per GitLab
URL: j.WebURL,
})
}
return out, nil
}
36 changes: 36 additions & 0 deletions internal/forge/gitlab/checks_report_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package gitlab

import (
"testing"

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

func TestRollupFromPipelineStatus(t *testing.T) {
tests := []struct {
status string
want forge.ChecksRollupState
}{
{gitlab.PipelineStatusSuccess, forge.ChecksRollupPassed},
{gitlab.PipelineStatusSkipped, forge.ChecksRollupPassed},
// Canceled and manual are user-initiated non-failures.
{gitlab.PipelineStatusCanceled, forge.ChecksRollupPassed},
{gitlab.PipelineStatusManual, forge.ChecksRollupPassed},
{gitlab.PipelineStatusFailed, forge.ChecksRollupFailed},
{gitlab.PipelineStatusPending, forge.ChecksRollupPending},
{gitlab.PipelineStatusRunning, forge.ChecksRollupPending},
{gitlab.PipelineStatusCreated, forge.ChecksRollupPending},
{gitlab.PipelineStatusPreparing, forge.ChecksRollupPending},
{gitlab.PipelineStatusWaitingForResource, forge.ChecksRollupPending},
{gitlab.PipelineStatusScheduled, forge.ChecksRollupPending},
{"unknown", forge.ChecksRollupPending},
}

for _, tt := range tests {
t.Run(tt.status, func(t *testing.T) {
assert.Equal(t, tt.want, rollupFromPipelineStatus(tt.status))
})
}
}
36 changes: 36 additions & 0 deletions internal/gateway/gitlab/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,31 @@ func (c *Client) CommitStatusList(
return response, resp, nil
}

// PipelineJobsList lists the jobs of a pipeline, in pipeline order.
//
// GitLab API:
// https://docs.gitlab.com/api/jobs/#list-pipeline-jobs
func (c *Client) PipelineJobsList(
ctx context.Context,
projectID int64,
pipelineID int64,
) ([]*Job, *Response, error) {
var response []*Job
resp, err := c.get(
ctx,
fmt.Sprintf(
"projects/%d/pipelines/%d/jobs",
projectID, pipelineID,
),
nil,
&response,
)
if err != nil {
return nil, resp, err
}
return response, resp, nil
}

// MergeRequestNoteCreate creates a merge request note.
//
// GitLab API:
Expand Down Expand Up @@ -545,6 +570,17 @@ type MergeRequest struct {
type Pipeline struct {
ID int64 `json:"id"`
Status string `json:"status"`
WebURL string `json:"web_url,omitempty"`
}

// Job is a single CI job within a pipeline.
//
// GitLab jobs API:
// https://docs.gitlab.com/api/jobs/
type Job struct {
Name string `json:"name"`
Status string `json:"status"`
WebURL string `json:"web_url,omitempty"`
}

// CommitStatus matches the subset of GitLab commit status fields
Expand Down
Loading