mirror of
https://gitea.com/gitea/runner.git
synced 2026-08-25 13:27:46 +00:00
e30c2fed62
- Raised go to 1.27 - Adopted json v2 - Sync lint config from gitea - Fixed all issues Co-authored-by: silverwind <me@silverwind.io> Reviewed-on: https://gitea.com/gitea/runner/pulls/1185 Reviewed-by: bircni <bircni@icloud.com> Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
218 lines
6.1 KiB
Go
218 lines
6.1 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//go:build e2e
|
|
|
|
package e2e
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json/v2"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const pollInterval = 250 * time.Millisecond
|
|
|
|
type GiteaAPI struct {
|
|
baseURL string
|
|
token string
|
|
}
|
|
|
|
type ActionRun struct {
|
|
ID int64 `json:"id"`
|
|
Status string `json:"status"`
|
|
Conclusion string `json:"conclusion"`
|
|
Event string `json:"event"`
|
|
}
|
|
|
|
type ActionJob struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Conclusion string `json:"conclusion"`
|
|
}
|
|
|
|
func (a *GiteaAPI) CreateRepo(ctx context.Context, name string) error {
|
|
body := map[string]any{"name": name, "auto_init": true}
|
|
return a.doJSON(ctx, http.MethodPost, "/api/v1/user/repos", body, nil)
|
|
}
|
|
|
|
func (a *GiteaAPI) CreateFile(ctx context.Context, repo, path, content, message string) error {
|
|
body := map[string]any{
|
|
"content": base64.StdEncoding.EncodeToString([]byte(content)),
|
|
"message": message,
|
|
}
|
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", giteaAdminUser, repo, path)
|
|
return a.doJSON(ctx, http.MethodPost, url, body, nil)
|
|
}
|
|
|
|
func (a *GiteaAPI) CreateSecret(ctx context.Context, repo, name, value string) error {
|
|
body := map[string]any{"data": value}
|
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/actions/secrets/%s", giteaAdminUser, repo, name)
|
|
return a.doJSON(ctx, http.MethodPut, url, body, nil)
|
|
}
|
|
|
|
func (a *GiteaAPI) DefaultBranch(ctx context.Context, repo string) (string, error) {
|
|
var resp struct {
|
|
DefaultBranch string `json:"default_branch"`
|
|
}
|
|
url := fmt.Sprintf("/api/v1/repos/%s/%s", giteaAdminUser, repo)
|
|
if err := a.doJSON(ctx, http.MethodGet, url, nil, &resp); err != nil {
|
|
return "", err
|
|
}
|
|
return resp.DefaultBranch, nil
|
|
}
|
|
|
|
func (a *GiteaAPI) CreateVariable(ctx context.Context, repo, name, value string) error {
|
|
body := map[string]any{"value": value}
|
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables/%s", giteaAdminUser, repo, name)
|
|
return a.doJSON(ctx, http.MethodPost, url, body, nil)
|
|
}
|
|
|
|
func (a *GiteaAPI) DispatchWorkflow(ctx context.Context, repo, workflowID, ref string, inputs map[string]string) error {
|
|
body := map[string]any{"ref": ref}
|
|
if len(inputs) > 0 {
|
|
body["inputs"] = inputs
|
|
}
|
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/actions/workflows/%s/dispatches", giteaAdminUser, repo, workflowID)
|
|
return a.doJSON(ctx, http.MethodPost, url, body, nil)
|
|
}
|
|
|
|
var ErrCancelUnsupported = errors.New("run cancellation is unsupported by this gitea version")
|
|
|
|
func (a *GiteaAPI) CancelRun(ctx context.Context, repo string, runID int64) error {
|
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/cancel", giteaAdminUser, repo, runID)
|
|
err := a.doJSON(ctx, http.MethodPost, url, nil, nil)
|
|
var status statusError
|
|
if errors.As(err, &status) && status.routeAbsent() {
|
|
return ErrCancelUnsupported
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (a *GiteaAPI) Runs(ctx context.Context, repo string) ([]ActionRun, error) {
|
|
var resp struct {
|
|
WorkflowRuns []ActionRun `json:"workflow_runs"`
|
|
}
|
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs?limit=1", giteaAdminUser, repo)
|
|
if err := a.doJSON(ctx, http.MethodGet, url, nil, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.WorkflowRuns, nil
|
|
}
|
|
|
|
func (a *GiteaAPI) WaitForRunConclusion(ctx context.Context, repo string, runID int64, timeout time.Duration) (*ActionRun, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d", giteaAdminUser, repo, runID)
|
|
for {
|
|
var run ActionRun
|
|
if err := a.doJSON(ctx, http.MethodGet, url, nil, &run); err != nil {
|
|
return nil, err
|
|
}
|
|
if run.Status == "completed" {
|
|
return &run, nil
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, fmt.Errorf("run %d did not complete within %s (last status %q): %w", runID, timeout, run.Status, ctx.Err())
|
|
case <-time.After(pollInterval):
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *GiteaAPI) Jobs(ctx context.Context, repo string, runID int64) ([]ActionJob, error) {
|
|
var resp struct {
|
|
Jobs []ActionJob `json:"jobs"`
|
|
}
|
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/jobs", giteaAdminUser, repo, runID)
|
|
if err := a.doJSON(ctx, http.MethodGet, url, nil, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Jobs, nil
|
|
}
|
|
|
|
func (a *GiteaAPI) JobLogs(ctx context.Context, repo string, jobID int64) (string, error) {
|
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", giteaAdminUser, repo, jobID)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+url, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Authorization", "token "+a.token)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if resp.StatusCode >= 300 {
|
|
return "", fmt.Errorf("GET %s: %d: %s", url, resp.StatusCode, body)
|
|
}
|
|
return string(body), nil
|
|
}
|
|
|
|
func (a *GiteaAPI) doJSON(ctx context.Context, method, path string, reqBody, respBody any) error {
|
|
var bodyReader io.Reader
|
|
if reqBody != nil {
|
|
encoded, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bodyReader = bytes.NewReader(encoded)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, a.baseURL+path, bodyReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "token "+a.token)
|
|
if reqBody != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.StatusCode >= 300 {
|
|
return statusError{method: method, path: path, code: resp.StatusCode, body: string(body)}
|
|
}
|
|
if respBody != nil && len(body) > 0 {
|
|
return json.Unmarshal(body, respBody)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type statusError struct {
|
|
method, path string
|
|
code int
|
|
body string
|
|
}
|
|
|
|
func (e statusError) Error() string {
|
|
return fmt.Sprintf("%s %s: %d: %s", e.method, e.path, e.code, e.body)
|
|
}
|
|
|
|
func (e statusError) routeAbsent() bool {
|
|
return e.code == http.StatusNotFound && strings.TrimSpace(e.body) == "404 page not found"
|
|
}
|