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).
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -304,6 +305,54 @@ func (f *GiteaContentFetcher) TestConnection(ctx context.Context, owner, repo st
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCommitStatus reports a deploy status on a commit via Gitea's commit-
|
||||
// status API (also serves Forgejo/Gogs). The "context" field is fixed to
|
||||
// "tinyforge" so repeated deploys update one status row.
|
||||
func (f *GiteaContentFetcher) SetCommitStatus(ctx context.Context, owner, repo, sha string, status CommitStatus, targetURL, description string) error {
|
||||
state := giteaState(status)
|
||||
body, err := json.Marshal(map[string]string{
|
||||
"state": state,
|
||||
"target_url": targetURL,
|
||||
"description": truncateDescription(description),
|
||||
"context": commitStatusContext,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal status: %w", err)
|
||||
}
|
||||
// Path-escape each identifier so the URL shape matches the other
|
||||
// provider methods and a hostile owner/repo/sha can't break out of
|
||||
// the intended path. The SSRF-safe client guards the host.
|
||||
apiURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/statuses/%s",
|
||||
f.baseURL, url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(sha))
|
||||
if err := postJSON(ctx, f.httpClient, apiURL, body, f.setAuth); err != nil {
|
||||
return fmt.Errorf("set commit status: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setAuth applies the Gitea token header (no-op when the token is empty).
|
||||
func (f *GiteaContentFetcher) setAuth(req *http.Request) {
|
||||
if f.token != "" {
|
||||
req.Header.Set("Authorization", "token "+f.token)
|
||||
}
|
||||
}
|
||||
|
||||
// giteaState maps a provider-agnostic CommitStatus onto Gitea's API
|
||||
// vocabulary. Gitea accepts the same four words Tinyforge uses, so this is
|
||||
// a 1:1 mapping with a "pending" fallback for any unknown value.
|
||||
func giteaState(status CommitStatus) string {
|
||||
switch status {
|
||||
case CommitStatusSuccess:
|
||||
return "success"
|
||||
case CommitStatusFailure:
|
||||
return "failure"
|
||||
case CommitStatusError:
|
||||
return "error"
|
||||
default:
|
||||
return "pending"
|
||||
}
|
||||
}
|
||||
|
||||
// doGet performs an authenticated GET request and returns the response body.
|
||||
func (f *GiteaContentFetcher) doGet(ctx context.Context, url string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
|
||||
Reference in New Issue
Block a user