Skip to content
Merged
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
142 changes: 133 additions & 9 deletions gitea/ci.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,140 @@
package gitea

import (
"bytes"
"context"
forge "github.com/git-pkgs/forge"
"io"
"net/http"

forge "github.com/git-pkgs/forge"

"code.gitea.io/sdk/gitea"
)

type giteaCIService struct{}
type giteaCIService struct {
client *gitea.Client
}

func (f *giteaForge) CI() forge.CIService {
return &giteaCIService{}
return &giteaCIService{client: f.client}
}

func (s *giteaCIService) ListRuns(_ context.Context, _, _ string, _ forge.ListCIRunOpts) ([]forge.CIRun, error) {
return nil, forge.ErrNotSupported
func convertGiteaWorkflowRun(r *gitea.ActionWorkflowRun) forge.CIRun {
result := forge.CIRun{
ID: r.ID,
Title: r.DisplayTitle,
Status: r.Status,
Branch: r.HeadBranch,
SHA: r.HeadSha,
Event: r.Event,
HTMLURL: r.HTMLURL,
CreatedAt: r.StartedAt,
}

if r.Conclusion != "" {
result.Conclusion = r.Conclusion
}

if r.Actor != nil {
result.Author = forge.User{
Login: r.Actor.UserName,
AvatarURL: r.Actor.AvatarURL,
}
}

if !r.CompletedAt.IsZero() {
t := r.CompletedAt
result.FinishedAt = &t
}

return result
}

func (s *giteaCIService) GetRun(_ context.Context, _, _ string, _ int64) (*forge.CIRun, error) {
return nil, forge.ErrNotSupported
func convertGiteaWorkflowJob(j *gitea.ActionWorkflowJob) forge.CIJob {
job := forge.CIJob{
ID: j.ID,
Name: j.Name,
Status: j.Status,
Conclusion: j.Conclusion,
HTMLURL: j.HTMLURL,
}
if !j.StartedAt.IsZero() {
t := j.StartedAt
job.StartedAt = &t
}
if !j.CompletedAt.IsZero() {
t := j.CompletedAt
job.FinishedAt = &t
}
return job
}

func (s *giteaCIService) ListRuns(_ context.Context, owner, repo string, opts forge.ListCIRunOpts) ([]forge.CIRun, error) {
perPage := opts.PerPage
if perPage <= 0 {
perPage = 20
}
page := opts.Page
if page <= 0 {
page = 1
}

gOpts := gitea.ListRepoActionRunsOptions{
ListOptions: gitea.ListOptions{Page: page, PageSize: perPage},
}
if opts.Branch != "" {
gOpts.Branch = opts.Branch
}
if opts.Status != "" {
gOpts.Status = opts.Status
}
if opts.User != "" {
gOpts.Actor = opts.User
}

var all []forge.CIRun
for {
resp, httpResp, err := s.client.ListRepoActionRuns(owner, repo, gOpts)
if err != nil {
if httpResp != nil && httpResp.StatusCode == http.StatusNotFound {
return nil, forge.ErrNotFound
}
return nil, err
}
for _, r := range resp.WorkflowRuns {
all = append(all, convertGiteaWorkflowRun(r))
}
if len(resp.WorkflowRuns) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) {
break
}
gOpts.Page++
}

if opts.Limit > 0 && len(all) > opts.Limit {
all = all[:opts.Limit]
}

return all, nil
}

func (s *giteaCIService) GetRun(_ context.Context, owner, repo string, runID int64) (*forge.CIRun, error) {
r, resp, err := s.client.GetRepoActionRun(owner, repo, runID)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
return nil, forge.ErrNotFound
}
return nil, err
}
result := convertGiteaWorkflowRun(r)

jobs, _, err := s.client.ListRepoActionRunJobs(owner, repo, runID, gitea.ListRepoActionJobsOptions{})
if err == nil {
for _, j := range jobs.Jobs {
result.Jobs = append(result.Jobs, convertGiteaWorkflowJob(j))
}
}

return &result, nil
}

func (s *giteaCIService) TriggerRun(_ context.Context, _, _ string, _ forge.TriggerCIRunOpts) error {
Expand All @@ -32,6 +149,13 @@ func (s *giteaCIService) RetryRun(_ context.Context, _, _ string, _ int64) error
return forge.ErrNotSupported
}

func (s *giteaCIService) GetJobLog(_ context.Context, _, _ string, _ int64) (io.ReadCloser, error) {
return nil, forge.ErrNotSupported
func (s *giteaCIService) GetJobLog(_ context.Context, owner, repo string, jobID int64) (io.ReadCloser, error) {
data, resp, err := s.client.GetRepoActionJobLogs(owner, repo, jobID)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
return nil, forge.ErrNotFound
}
return nil, err
}
return io.NopCloser(bytes.NewReader(data)), nil
}
234 changes: 234 additions & 0 deletions gitea/ci_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package gitea

import (
"context"
"encoding/json"
"fmt"
forge "github.com/git-pkgs/forge"
"io"
"net/http"
"net/http/httptest"
"testing"
)

func giteaVersionHandler125(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, `{"version":"1.25.0"}`)
}

func TestGiteaCIListRuns(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/actions/runs", func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{
"total_count": 1,
"workflow_runs": []map[string]any{
{
"id": 42,
"display_title": "CI Pipeline",
"status": "completed",
"conclusion": "success",
"head_branch": "main",
"head_sha": "abc123",
"event": "push",
"html_url": "https://codeberg.org/testorg/testrepo/actions/runs/42",
"actor": map[string]any{
"login": "testuser",
"avatar_url": "https://codeberg.org/avatars/1",
},
},
},
})
})

srv := httptest.NewServer(mux)
defer srv.Close()

f := New(srv.URL, "test-token", nil)
runs, err := f.CI().ListRuns(context.Background(), "testorg", "testrepo", forge.ListCIRunOpts{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(runs) != 1 {
t.Fatalf("expected 1 run, got %d", len(runs))
}

r := runs[0]
if r.ID != 42 {
t.Errorf("ID: want 42, got %d", r.ID)
}
assertEqual(t, "Title", "CI Pipeline", r.Title)
assertEqual(t, "Status", "completed", r.Status)
assertEqual(t, "Conclusion", "success", r.Conclusion)
assertEqual(t, "Branch", "main", r.Branch)
assertEqual(t, "SHA", "abc123", r.SHA)
assertEqual(t, "Event", "push", r.Event)
assertEqual(t, "Author.Login", "testuser", r.Author.Login)
}

func TestGiteaCIListRunsWithFilters(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/actions/runs", func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
if q.Get("branch") != "develop" {
t.Errorf("expected branch=develop, got %q", q.Get("branch"))
}
if q.Get("status") != "running" {
t.Errorf("expected status=running, got %q", q.Get("status"))
}
if q.Get("actor") != "testuser" {
t.Errorf("expected actor=testuser, got %q", q.Get("actor"))
}
_ = json.NewEncoder(w).Encode(map[string]any{
"total_count": 0,
"workflow_runs": []map[string]any{},
})
})

srv := httptest.NewServer(mux)
defer srv.Close()

f := New(srv.URL, "test-token", nil)
runs, err := f.CI().ListRuns(context.Background(), "testorg", "testrepo", forge.ListCIRunOpts{
Branch: "develop",
Status: "running",
User: "testuser",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(runs) != 0 {
t.Fatalf("expected 0 runs, got %d", len(runs))
}
}

func TestGiteaCIGetRun(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/actions/runs/42", func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{
"id": 42,
"display_title": "CI Pipeline",
"status": "completed",
"conclusion": "success",
"head_branch": "main",
"head_sha": "abc123",
})
})
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/actions/runs/42/jobs", func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{
"total_count": 1,
"jobs": []map[string]any{
{
"id": 100,
"name": "build",
"status": "completed",
"conclusion": "success",
"html_url": "https://codeberg.org/testorg/testrepo/actions/runs/42/jobs/100",
},
},
})
})

srv := httptest.NewServer(mux)
defer srv.Close()

f := New(srv.URL, "test-token", nil)
run, err := f.CI().GetRun(context.Background(), "testorg", "testrepo", 42)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if run.ID != 42 {
t.Errorf("ID: want 42, got %d", run.ID)
}
assertEqual(t, "Title", "CI Pipeline", run.Title)
assertEqual(t, "Conclusion", "success", run.Conclusion)
if len(run.Jobs) != 1 {
t.Fatalf("expected 1 job, got %d", len(run.Jobs))
}
assertEqual(t, "Jobs[0].Name", "build", run.Jobs[0].Name)
assertEqual(t, "Jobs[0].Status", "completed", run.Jobs[0].Status)
}

func TestGiteaCIGetRunNotFound(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/actions/runs/999", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
})

srv := httptest.NewServer(mux)
defer srv.Close()

f := New(srv.URL, "test-token", nil)
_, err := f.CI().GetRun(context.Background(), "testorg", "testrepo", 999)
if err != forge.ErrNotFound {
t.Fatalf("expected forge.ErrNotFound, got %v", err)
}
}

func TestGiteaCIGetJobLog(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/actions/jobs/100/logs", func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprint(w, "Build started\nStep 1: compile\nBuild finished")
})

srv := httptest.NewServer(mux)
defer srv.Close()

f := New(srv.URL, "test-token", nil)
rc, err := f.CI().GetJobLog(context.Background(), "testorg", "testrepo", 100)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer func() { _ = rc.Close() }()

data, err := io.ReadAll(rc)
if err != nil {
t.Fatalf("failed to read log: %v", err)
}
assertEqual(t, "log content", "Build started\nStep 1: compile\nBuild finished", string(data))
}

func TestGiteaCITriggerRunNotSupported(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)

srv := httptest.NewServer(mux)
defer srv.Close()

f := New(srv.URL, "test-token", nil)
err := f.CI().TriggerRun(context.Background(), "testorg", "testrepo", forge.TriggerCIRunOpts{})
if err != forge.ErrNotSupported {
t.Fatalf("expected forge.ErrNotSupported, got %v", err)
}
}

func TestGiteaCICancelRunNotSupported(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)

srv := httptest.NewServer(mux)
defer srv.Close()

f := New(srv.URL, "test-token", nil)
err := f.CI().CancelRun(context.Background(), "testorg", "testrepo", 42)
if err != forge.ErrNotSupported {
t.Fatalf("expected forge.ErrNotSupported, got %v", err)
}
}

func TestGiteaCIRetryRunNotSupported(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)

srv := httptest.NewServer(mux)
defer srv.Close()

f := New(srv.URL, "test-token", nil)
err := f.CI().RetryRun(context.Background(), "testorg", "testrepo", 42)
if err != forge.ErrNotSupported {
t.Fatalf("expected forge.ErrNotSupported, got %v", err)
}
}