Files
alexei.dolgolyov 3071cda512 feat(deploy): commit-status reporting to Git providers
Report deploy status back to the Git provider as a commit status
(pending/success/failure) for git-sourced workloads (static + dockerfile).

- GitProvider.SetCommitStatus on gitea/github/gitlab over the existing
  SSRF-safe client; fixed "tinyforge" context so redeploys update one row.
  postJSON returns status-code-only errors (never echoes the upstream body,
  which a hostile provider could use to reflect the auth token into the
  best-effort log line).
- Best-effort deploy hook: pending on deploy start, success/failure on
  outcome, gated on a per-workload report_commit_status flag. Never fails or
  blocks a deploy; emits nothing on the unchanged-SHA short-circuit.
- UI ToggleSwitch (create + edit) + reportCommitStatus in sourceForms.ts
  + en/ru i18n.
- Tests: per-provider state mapping + request shape; reporter gating
  (enabled/disabled/empty-SHA/nil/error-swallow).

Reviewed via go-reviewer + security-reviewer (0 CRITICAL/HIGH; one MEDIUM
body-echo log-leak fixed).
2026-05-29 11:37:56 +03:00

95 lines
3.5 KiB
Go

// Package static implements the "static" source: a git-folder-backed
// deployable that can serve plain files or run a Deno backend. Builds
// an image from the cloned folder and runs one container.
//
// The full deploy pipeline is implemented inline in this package
// (deploy.go / teardown.go / reconcile.go). It operates directly on
// plugin.Workload + the containers / workload_env tables — there is no
// longer a synthetic static_sites row backing each workload.
//
// The legacy internal/staticsite package remains alive to serve the
// /api/sites/* HTTP routes and the existing static_sites table; this
// plugin does not depend on it for state, only for git-provider
// helpers and Deno scaffolding generation.
package static
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/alexei/tinyforge/internal/workload/plugin"
)
// Config is the per-workload source config blob. Mirrors the fields
// that used to live on the static_sites table, less anything moved to
// Workload (notification config, webhook secrets, public_face).
type Config struct {
Provider string `json:"provider"` // "gitea" | "github" | "gitlab"; "" = autodetect
BaseURL string `json:"base_url"` // e.g. https://git.example.com
RepoOwner string `json:"repo_owner"`
RepoName string `json:"repo_name"`
Branch string `json:"branch"`
FolderPath string `json:"folder_path"` // path within repo
AccessToken string `json:"access_token"` // encrypted; optional for public repos
Mode string `json:"mode"` // "static" | "deno"
RenderMarkdown bool `json:"render_markdown"`
StorageEnabled bool `json:"storage_enabled"`
StorageLimitMB int `json:"storage_limit_mb"`
// ReportCommitStatus, when true, pushes the deploy outcome back to the
// git provider as a commit status (pending/success/failure) on the
// deployed SHA. Best-effort — a reporting failure never fails a deploy.
ReportCommitStatus bool `json:"report_commit_status"`
}
type source struct{}
// Eager registration — the deploy pipeline lives entirely inside this
// package now, so the kind is usable as soon as init() fires. No more
// "backend not wired" failure mode at deploy time.
func init() { plugin.RegisterSource(&source{}) }
func (*source) Kind() string { return "static" }
func (*source) SchemaSample() any {
return Config{
Provider: "gitea",
BaseURL: "https://git.example.com",
RepoOwner: "owner",
RepoName: "pages",
Branch: "main",
FolderPath: "",
Mode: "static",
}
}
func (*source) Validate(cfg json.RawMessage) error {
var c Config
if len(cfg) == 0 {
return fmt.Errorf("static source: config is required")
}
if err := json.Unmarshal(cfg, &c); err != nil {
return fmt.Errorf("static source: invalid json: %w", err)
}
if strings.TrimSpace(c.RepoOwner) == "" || strings.TrimSpace(c.RepoName) == "" {
return fmt.Errorf("static source: repo_owner and repo_name are required")
}
if c.Mode != "" && c.Mode != "static" && c.Mode != "deno" {
return fmt.Errorf("static source: mode must be \"static\" or \"deno\"")
}
return nil
}
func (*source) Deploy(ctx context.Context, deps plugin.Deps, w plugin.Workload, intent plugin.DeploymentIntent) error {
return deploy(ctx, deps, w, intent)
}
func (*source) Teardown(ctx context.Context, deps plugin.Deps, w plugin.Workload) error {
return teardown(ctx, deps, w)
}
func (*source) Reconcile(ctx context.Context, deps plugin.Deps, w plugin.Workload) error {
return reconcile(ctx, deps, w)
}