Skip to content

Commit 7e369aa

Browse files
authored
Merge pull request #16 from git-pkgs/implement-gitea-ci-service
Implement CI service for Gitea/Forgejo
2 parents ea26276 + ca34bfa commit 7e369aa

2 files changed

Lines changed: 367 additions & 9 deletions

File tree

gitea/ci.go

Lines changed: 133 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,140 @@
11
package gitea
22

33
import (
4+
"bytes"
45
"context"
5-
forge "github.com/git-pkgs/forge"
66
"io"
7+
"net/http"
8+
9+
forge "github.com/git-pkgs/forge"
10+
11+
"code.gitea.io/sdk/gitea"
712
)
813

9-
type giteaCIService struct{}
14+
type giteaCIService struct {
15+
client *gitea.Client
16+
}
1017

1118
func (f *giteaForge) CI() forge.CIService {
12-
return &giteaCIService{}
19+
return &giteaCIService{client: f.client}
1320
}
1421

15-
func (s *giteaCIService) ListRuns(_ context.Context, _, _ string, _ forge.ListCIRunOpts) ([]forge.CIRun, error) {
16-
return nil, forge.ErrNotSupported
22+
func convertGiteaWorkflowRun(r *gitea.ActionWorkflowRun) forge.CIRun {
23+
result := forge.CIRun{
24+
ID: r.ID,
25+
Title: r.DisplayTitle,
26+
Status: r.Status,
27+
Branch: r.HeadBranch,
28+
SHA: r.HeadSha,
29+
Event: r.Event,
30+
HTMLURL: r.HTMLURL,
31+
CreatedAt: r.StartedAt,
32+
}
33+
34+
if r.Conclusion != "" {
35+
result.Conclusion = r.Conclusion
36+
}
37+
38+
if r.Actor != nil {
39+
result.Author = forge.User{
40+
Login: r.Actor.UserName,
41+
AvatarURL: r.Actor.AvatarURL,
42+
}
43+
}
44+
45+
if !r.CompletedAt.IsZero() {
46+
t := r.CompletedAt
47+
result.FinishedAt = &t
48+
}
49+
50+
return result
1751
}
1852

19-
func (s *giteaCIService) GetRun(_ context.Context, _, _ string, _ int64) (*forge.CIRun, error) {
20-
return nil, forge.ErrNotSupported
53+
func convertGiteaWorkflowJob(j *gitea.ActionWorkflowJob) forge.CIJob {
54+
job := forge.CIJob{
55+
ID: j.ID,
56+
Name: j.Name,
57+
Status: j.Status,
58+
Conclusion: j.Conclusion,
59+
HTMLURL: j.HTMLURL,
60+
}
61+
if !j.StartedAt.IsZero() {
62+
t := j.StartedAt
63+
job.StartedAt = &t
64+
}
65+
if !j.CompletedAt.IsZero() {
66+
t := j.CompletedAt
67+
job.FinishedAt = &t
68+
}
69+
return job
70+
}
71+
72+
func (s *giteaCIService) ListRuns(_ context.Context, owner, repo string, opts forge.ListCIRunOpts) ([]forge.CIRun, error) {
73+
perPage := opts.PerPage
74+
if perPage <= 0 {
75+
perPage = 20
76+
}
77+
page := opts.Page
78+
if page <= 0 {
79+
page = 1
80+
}
81+
82+
gOpts := gitea.ListRepoActionRunsOptions{
83+
ListOptions: gitea.ListOptions{Page: page, PageSize: perPage},
84+
}
85+
if opts.Branch != "" {
86+
gOpts.Branch = opts.Branch
87+
}
88+
if opts.Status != "" {
89+
gOpts.Status = opts.Status
90+
}
91+
if opts.User != "" {
92+
gOpts.Actor = opts.User
93+
}
94+
95+
var all []forge.CIRun
96+
for {
97+
resp, httpResp, err := s.client.ListRepoActionRuns(owner, repo, gOpts)
98+
if err != nil {
99+
if httpResp != nil && httpResp.StatusCode == http.StatusNotFound {
100+
return nil, forge.ErrNotFound
101+
}
102+
return nil, err
103+
}
104+
for _, r := range resp.WorkflowRuns {
105+
all = append(all, convertGiteaWorkflowRun(r))
106+
}
107+
if len(resp.WorkflowRuns) < perPage || (opts.Limit > 0 && len(all) >= opts.Limit) {
108+
break
109+
}
110+
gOpts.Page++
111+
}
112+
113+
if opts.Limit > 0 && len(all) > opts.Limit {
114+
all = all[:opts.Limit]
115+
}
116+
117+
return all, nil
118+
}
119+
120+
func (s *giteaCIService) GetRun(_ context.Context, owner, repo string, runID int64) (*forge.CIRun, error) {
121+
r, resp, err := s.client.GetRepoActionRun(owner, repo, runID)
122+
if err != nil {
123+
if resp != nil && resp.StatusCode == http.StatusNotFound {
124+
return nil, forge.ErrNotFound
125+
}
126+
return nil, err
127+
}
128+
result := convertGiteaWorkflowRun(r)
129+
130+
jobs, _, err := s.client.ListRepoActionRunJobs(owner, repo, runID, gitea.ListRepoActionJobsOptions{})
131+
if err == nil {
132+
for _, j := range jobs.Jobs {
133+
result.Jobs = append(result.Jobs, convertGiteaWorkflowJob(j))
134+
}
135+
}
136+
137+
return &result, nil
21138
}
22139

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

35-
func (s *giteaCIService) GetJobLog(_ context.Context, _, _ string, _ int64) (io.ReadCloser, error) {
36-
return nil, forge.ErrNotSupported
152+
func (s *giteaCIService) GetJobLog(_ context.Context, owner, repo string, jobID int64) (io.ReadCloser, error) {
153+
data, resp, err := s.client.GetRepoActionJobLogs(owner, repo, jobID)
154+
if err != nil {
155+
if resp != nil && resp.StatusCode == http.StatusNotFound {
156+
return nil, forge.ErrNotFound
157+
}
158+
return nil, err
159+
}
160+
return io.NopCloser(bytes.NewReader(data)), nil
37161
}

gitea/ci_test.go

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package gitea
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
forge "github.com/git-pkgs/forge"
8+
"io"
9+
"net/http"
10+
"net/http/httptest"
11+
"testing"
12+
)
13+
14+
func giteaVersionHandler125(w http.ResponseWriter, r *http.Request) {
15+
_, _ = fmt.Fprintf(w, `{"version":"1.25.0"}`)
16+
}
17+
18+
func TestGiteaCIListRuns(t *testing.T) {
19+
mux := http.NewServeMux()
20+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
21+
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/actions/runs", func(w http.ResponseWriter, r *http.Request) {
22+
_ = json.NewEncoder(w).Encode(map[string]any{
23+
"total_count": 1,
24+
"workflow_runs": []map[string]any{
25+
{
26+
"id": 42,
27+
"display_title": "CI Pipeline",
28+
"status": "completed",
29+
"conclusion": "success",
30+
"head_branch": "main",
31+
"head_sha": "abc123",
32+
"event": "push",
33+
"html_url": "https://codeberg.org/testorg/testrepo/actions/runs/42",
34+
"actor": map[string]any{
35+
"login": "testuser",
36+
"avatar_url": "https://codeberg.org/avatars/1",
37+
},
38+
},
39+
},
40+
})
41+
})
42+
43+
srv := httptest.NewServer(mux)
44+
defer srv.Close()
45+
46+
f := New(srv.URL, "test-token", nil)
47+
runs, err := f.CI().ListRuns(context.Background(), "testorg", "testrepo", forge.ListCIRunOpts{})
48+
if err != nil {
49+
t.Fatalf("unexpected error: %v", err)
50+
}
51+
if len(runs) != 1 {
52+
t.Fatalf("expected 1 run, got %d", len(runs))
53+
}
54+
55+
r := runs[0]
56+
if r.ID != 42 {
57+
t.Errorf("ID: want 42, got %d", r.ID)
58+
}
59+
assertEqual(t, "Title", "CI Pipeline", r.Title)
60+
assertEqual(t, "Status", "completed", r.Status)
61+
assertEqual(t, "Conclusion", "success", r.Conclusion)
62+
assertEqual(t, "Branch", "main", r.Branch)
63+
assertEqual(t, "SHA", "abc123", r.SHA)
64+
assertEqual(t, "Event", "push", r.Event)
65+
assertEqual(t, "Author.Login", "testuser", r.Author.Login)
66+
}
67+
68+
func TestGiteaCIListRunsWithFilters(t *testing.T) {
69+
mux := http.NewServeMux()
70+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
71+
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/actions/runs", func(w http.ResponseWriter, r *http.Request) {
72+
q := r.URL.Query()
73+
if q.Get("branch") != "develop" {
74+
t.Errorf("expected branch=develop, got %q", q.Get("branch"))
75+
}
76+
if q.Get("status") != "running" {
77+
t.Errorf("expected status=running, got %q", q.Get("status"))
78+
}
79+
if q.Get("actor") != "testuser" {
80+
t.Errorf("expected actor=testuser, got %q", q.Get("actor"))
81+
}
82+
_ = json.NewEncoder(w).Encode(map[string]any{
83+
"total_count": 0,
84+
"workflow_runs": []map[string]any{},
85+
})
86+
})
87+
88+
srv := httptest.NewServer(mux)
89+
defer srv.Close()
90+
91+
f := New(srv.URL, "test-token", nil)
92+
runs, err := f.CI().ListRuns(context.Background(), "testorg", "testrepo", forge.ListCIRunOpts{
93+
Branch: "develop",
94+
Status: "running",
95+
User: "testuser",
96+
})
97+
if err != nil {
98+
t.Fatalf("unexpected error: %v", err)
99+
}
100+
if len(runs) != 0 {
101+
t.Fatalf("expected 0 runs, got %d", len(runs))
102+
}
103+
}
104+
105+
func TestGiteaCIGetRun(t *testing.T) {
106+
mux := http.NewServeMux()
107+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
108+
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/actions/runs/42", func(w http.ResponseWriter, r *http.Request) {
109+
_ = json.NewEncoder(w).Encode(map[string]any{
110+
"id": 42,
111+
"display_title": "CI Pipeline",
112+
"status": "completed",
113+
"conclusion": "success",
114+
"head_branch": "main",
115+
"head_sha": "abc123",
116+
})
117+
})
118+
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/actions/runs/42/jobs", func(w http.ResponseWriter, r *http.Request) {
119+
_ = json.NewEncoder(w).Encode(map[string]any{
120+
"total_count": 1,
121+
"jobs": []map[string]any{
122+
{
123+
"id": 100,
124+
"name": "build",
125+
"status": "completed",
126+
"conclusion": "success",
127+
"html_url": "https://codeberg.org/testorg/testrepo/actions/runs/42/jobs/100",
128+
},
129+
},
130+
})
131+
})
132+
133+
srv := httptest.NewServer(mux)
134+
defer srv.Close()
135+
136+
f := New(srv.URL, "test-token", nil)
137+
run, err := f.CI().GetRun(context.Background(), "testorg", "testrepo", 42)
138+
if err != nil {
139+
t.Fatalf("unexpected error: %v", err)
140+
}
141+
if run.ID != 42 {
142+
t.Errorf("ID: want 42, got %d", run.ID)
143+
}
144+
assertEqual(t, "Title", "CI Pipeline", run.Title)
145+
assertEqual(t, "Conclusion", "success", run.Conclusion)
146+
if len(run.Jobs) != 1 {
147+
t.Fatalf("expected 1 job, got %d", len(run.Jobs))
148+
}
149+
assertEqual(t, "Jobs[0].Name", "build", run.Jobs[0].Name)
150+
assertEqual(t, "Jobs[0].Status", "completed", run.Jobs[0].Status)
151+
}
152+
153+
func TestGiteaCIGetRunNotFound(t *testing.T) {
154+
mux := http.NewServeMux()
155+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
156+
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/actions/runs/999", func(w http.ResponseWriter, r *http.Request) {
157+
w.WriteHeader(http.StatusNotFound)
158+
})
159+
160+
srv := httptest.NewServer(mux)
161+
defer srv.Close()
162+
163+
f := New(srv.URL, "test-token", nil)
164+
_, err := f.CI().GetRun(context.Background(), "testorg", "testrepo", 999)
165+
if err != forge.ErrNotFound {
166+
t.Fatalf("expected forge.ErrNotFound, got %v", err)
167+
}
168+
}
169+
170+
func TestGiteaCIGetJobLog(t *testing.T) {
171+
mux := http.NewServeMux()
172+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
173+
mux.HandleFunc("GET /api/v1/repos/testorg/testrepo/actions/jobs/100/logs", func(w http.ResponseWriter, r *http.Request) {
174+
_, _ = fmt.Fprint(w, "Build started\nStep 1: compile\nBuild finished")
175+
})
176+
177+
srv := httptest.NewServer(mux)
178+
defer srv.Close()
179+
180+
f := New(srv.URL, "test-token", nil)
181+
rc, err := f.CI().GetJobLog(context.Background(), "testorg", "testrepo", 100)
182+
if err != nil {
183+
t.Fatalf("unexpected error: %v", err)
184+
}
185+
defer func() { _ = rc.Close() }()
186+
187+
data, err := io.ReadAll(rc)
188+
if err != nil {
189+
t.Fatalf("failed to read log: %v", err)
190+
}
191+
assertEqual(t, "log content", "Build started\nStep 1: compile\nBuild finished", string(data))
192+
}
193+
194+
func TestGiteaCITriggerRunNotSupported(t *testing.T) {
195+
mux := http.NewServeMux()
196+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
197+
198+
srv := httptest.NewServer(mux)
199+
defer srv.Close()
200+
201+
f := New(srv.URL, "test-token", nil)
202+
err := f.CI().TriggerRun(context.Background(), "testorg", "testrepo", forge.TriggerCIRunOpts{})
203+
if err != forge.ErrNotSupported {
204+
t.Fatalf("expected forge.ErrNotSupported, got %v", err)
205+
}
206+
}
207+
208+
func TestGiteaCICancelRunNotSupported(t *testing.T) {
209+
mux := http.NewServeMux()
210+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
211+
212+
srv := httptest.NewServer(mux)
213+
defer srv.Close()
214+
215+
f := New(srv.URL, "test-token", nil)
216+
err := f.CI().CancelRun(context.Background(), "testorg", "testrepo", 42)
217+
if err != forge.ErrNotSupported {
218+
t.Fatalf("expected forge.ErrNotSupported, got %v", err)
219+
}
220+
}
221+
222+
func TestGiteaCIRetryRunNotSupported(t *testing.T) {
223+
mux := http.NewServeMux()
224+
mux.HandleFunc("GET /api/v1/version", giteaVersionHandler125)
225+
226+
srv := httptest.NewServer(mux)
227+
defer srv.Close()
228+
229+
f := New(srv.URL, "test-token", nil)
230+
err := f.CI().RetryRun(context.Background(), "testorg", "testrepo", 42)
231+
if err != forge.ErrNotSupported {
232+
t.Fatalf("expected forge.ErrNotSupported, got %v", err)
233+
}
234+
}

0 commit comments

Comments
 (0)