Files
mathiasandClaude Opus 4.8 4ebea7d023
CD / Lint / Test / Vet (push) Successful in 6s
CD / Build & Import (push) Successful in 21s
CD / Deploy via GitOps (push) Has been skipped
fix(workflow_run_trigger): 204 dispatch is success; resolve run via listing (#41)
Gitea's workflow_dispatch endpoint returns 204 No Content with no Location
header. DispatchWorkflow required that header, so every successful dispatch
errored with "missing Location header" and never yielded a run ID — making
the tool unusable for CAD dispatch.

- DispatchWorkflow now returns error-only; 204 = success, no Location needed.
  Body already carried ref+inputs; kept and covered by test.
- Tool snapshots the newest existing workflow_dispatch run before dispatch,
  then polls ListWorkflowRuns after and returns the newest run with ID above
  that baseline (avoids returning a stale prior run). Falls back to an honest
  "dispatched, run not yet registered" result rather than failing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:12:45 +02:00

130 lines
3.9 KiB
Go

package gitea
import (
"context"
"encoding/json"
"fmt"
"net/url"
"strconv"
)
// DispatchWorkflowArgs is the request body for a workflow_dispatch trigger.
type DispatchWorkflowArgs struct {
Ref string `json:"ref"`
Inputs map[string]any `json:"inputs,omitempty"`
}
// DispatchWorkflow triggers a workflow_dispatch event. Gitea returns 204 No
// Content with NO Location header, so the response carries no run ID — callers
// resolve the new run separately via ListWorkflowRuns. Returns nil on success.
func (c *Client) DispatchWorkflow(ctx context.Context, owner, repo, workflow string, args DispatchWorkflowArgs) error {
p := fmt.Sprintf("/api/v1/repos/%s/%s/actions/workflows/%s/dispatches", owner, repo, workflow)
payload, err := json.Marshal(args)
if err != nil {
return err
}
resp, err := c.doRaw(ctx, "POST", p, payload)
if err != nil {
return err
}
if resp.Status != 204 {
if mapErr := MapStatus(resp.Status, resp.Body); mapErr != nil {
return mapErr
}
return fmt.Errorf("unexpected status %d", resp.Status)
}
return nil
}
// WorkflowRun represents a Gitea Actions run.
type WorkflowRun struct {
ID int64 `json:"id"`
DisplayTitle string `json:"display_title,omitempty"`
Status string `json:"status"` // queued | in_progress | completed
Conclusion string `json:"conclusion"` // success | failure | cancelled | skipped (only when completed)
Event string `json:"event,omitempty"`
HeadSHA string `json:"head_sha,omitempty"`
HeadBranch string `json:"head_branch,omitempty"`
WorkflowID string `json:"workflow_id,omitempty"`
RunNumber int64 `json:"run_number,omitempty"`
StartedAt string `json:"started_at"`
UpdatedAt string `json:"updated_at,omitempty"`
HTMLURL string `json:"html_url"`
Actor struct {
Login string `json:"login"`
} `json:"actor,omitempty"`
}
// GetWorkflowRun fetches the status of a specific Actions run.
func (c *Client) GetWorkflowRun(ctx context.Context, owner, repo string, runID int64) (*WorkflowRun, error) {
p := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d", owner, repo, runID)
body, status, err := c.GetJSON(ctx, p)
if err != nil {
return nil, err
}
if err := MapStatus(status, body); err != nil {
return nil, err
}
var run WorkflowRun
if err := json.Unmarshal(body, &run); err != nil {
return nil, err
}
return &run, nil
}
// ListWorkflowRunsArgs captures the optional query params for ListWorkflowRuns.
type ListWorkflowRunsArgs struct {
Branch string
HeadSHA string
Status string // queued | in_progress | completed | all
Event string // push | pull_request | schedule | workflow_dispatch | all
Workflow string
Page int
Limit int
}
type workflowRunsResponse struct {
TotalCount int64 `json:"total_count"`
WorkflowRuns []WorkflowRun `json:"workflow_runs"`
}
// ListWorkflowRuns fetches recent Actions runs for a repo with optional filters.
// Status / Event of "all" or "" are treated as no-filter.
func (c *Client) ListWorkflowRuns(ctx context.Context, owner, repo string, args ListWorkflowRunsArgs) (*workflowRunsResponse, error) {
q := url.Values{}
if args.Branch != "" {
q.Set("branch", args.Branch)
}
if args.HeadSHA != "" {
q.Set("head_sha", args.HeadSHA)
}
if args.Status != "" && args.Status != "all" {
q.Set("status", args.Status)
}
if args.Event != "" && args.Event != "all" {
q.Set("event", args.Event)
}
if args.Workflow != "" {
q.Set("workflow", args.Workflow)
}
if args.Page > 0 {
q.Set("page", strconv.Itoa(args.Page))
}
if args.Limit > 0 {
q.Set("limit", strconv.Itoa(args.Limit))
}
p := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs?%s", owner, repo, q.Encode())
body, status, err := c.GetJSON(ctx, p)
if err != nil {
return nil, err
}
if err := MapStatus(status, body); err != nil {
return nil, err
}
var resp workflowRunsResponse
if err := json.Unmarshal(body, &resp); err != nil {
return nil, err
}
return &resp, nil
}