forgejo-runner-optimiser/internal/summary/push.go
Martin McCaffery d0dd209bc9
All checks were successful
ci / build (push) Successful in 28s
Add token-based authentication for receiver
2026-02-11 15:18:03 +01:00

112 lines
3 KiB
Go

// ABOUTME: HTTP client for pushing run summaries to the metrics receiver.
// ABOUTME: Reads execution context from GitHub Actions style environment variables.
package summary
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
// ExecutionContext holds GitHub Actions style identifiers for a workflow run
type ExecutionContext struct {
Organization string `json:"organization"`
Repository string `json:"repository"`
Workflow string `json:"workflow"`
Job string `json:"job"`
RunID string `json:"run_id"`
}
// MetricsPayload is the complete payload sent to the receiver
type MetricsPayload struct {
Execution ExecutionContext `json:"execution"`
Summary RunSummary `json:"run_summary"`
}
// PushClient sends metrics to the receiver service
type PushClient struct {
endpoint string
token string
client *http.Client
ctx ExecutionContext
}
// NewPushClient creates a new push client configured from environment variables.
// If token is non-empty, it is sent as a Bearer token on each push request.
func NewPushClient(endpoint, token string) *PushClient {
return &PushClient{
endpoint: endpoint,
token: token,
client: &http.Client{
Timeout: 30 * time.Second,
},
ctx: ExecutionContextFromEnv(),
}
}
// ExecutionContextFromEnv reads execution context from GitHub Actions environment variables
func ExecutionContextFromEnv() ExecutionContext {
return ExecutionContext{
Organization: getEnvWithFallback("GITHUB_REPOSITORY_OWNER", "GITEA_REPO_OWNER"),
Repository: getEnvWithFallback("GITHUB_REPOSITORY", "GITEA_REPO"),
Workflow: getEnvWithFallback("GITHUB_WORKFLOW", "GITEA_WORKFLOW"),
Job: getEnvWithFallback("GITHUB_JOB", "GITEA_JOB"),
RunID: getEnvWithFallback("GITHUB_RUN_ID", "GITEA_RUN_ID"),
}
}
func getEnvWithFallback(keys ...string) string {
for _, key := range keys {
if val := os.Getenv(key); val != "" {
return val
}
}
return ""
}
// Push sends the run summary to the receiver
func (p *PushClient) Push(ctx context.Context, summary *RunSummary) error {
if summary == nil {
return nil
}
payload := MetricsPayload{
Execution: p.ctx,
Summary: *summary,
}
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshaling payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.endpoint, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if p.token != "" {
req.Header.Set("Authorization", "Bearer "+p.token)
}
resp, err := p.client.Do(req)
if err != nil {
return fmt.Errorf("sending request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return nil
}
// ExecutionContext returns the current execution context
func (p *PushClient) ExecutionContext() ExecutionContext {
return p.ctx
}