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:
2026-05-29 11:37:56 +03:00
parent 410a131cec
commit 3071cda512
17 changed files with 1051 additions and 10 deletions
+33 -5
View File
@@ -59,6 +59,8 @@ export interface StaticFormState extends GitSourceState {
folderPath: string;
mode: 'static' | 'deno';
renderMarkdown: boolean;
/** Report deploy outcome back to the git provider as a commit status. */
reportCommitStatus: boolean;
}
/** Dockerfile source: build an image from a Dockerfile in a repo. */
@@ -66,6 +68,8 @@ export interface DockerfileFormState extends GitSourceState {
contextPath: string;
dockerfilePath: string;
port: number;
/** Report deploy outcome back to the git provider as a commit status. */
reportCommitStatus: boolean;
}
// ── Defaults ────────────────────────────────────────────────────────
@@ -99,11 +103,23 @@ function emptyGitSourceState(): GitSourceState {
}
export function emptyStaticState(): StaticFormState {
return { ...emptyGitSourceState(), folderPath: '', mode: 'static', renderMarkdown: false };
return {
...emptyGitSourceState(),
folderPath: '',
mode: 'static',
renderMarkdown: false,
reportCommitStatus: false
};
}
export function emptyDockerfileState(): DockerfileFormState {
return { ...emptyGitSourceState(), contextPath: '', dockerfilePath: 'Dockerfile', port: 0 };
return {
...emptyGitSourceState(),
contextPath: '',
dockerfilePath: 'Dockerfile',
port: 0,
reportCommitStatus: false
};
}
// ── Parse helpers ───────────────────────────────────────────────────
@@ -186,7 +202,9 @@ export function seedStaticState(jsonText: string): StaticFormState {
accessToken: strOr(o.access_token, ''),
folderPath: strOr(o.folder_path, ''),
mode: o.mode === 'deno' ? 'deno' : 'static',
renderMarkdown: typeof o.render_markdown === 'boolean' ? o.render_markdown : false
renderMarkdown: typeof o.render_markdown === 'boolean' ? o.render_markdown : false,
reportCommitStatus:
typeof o.report_commit_status === 'boolean' ? o.report_commit_status : false
};
}
@@ -201,7 +219,9 @@ export function seedDockerfileState(jsonText: string): DockerfileFormState {
accessToken: strOr(o.access_token, ''),
contextPath: strOr(o.context_path, ''),
dockerfilePath: strOrTruthy(o.dockerfile_path, 'Dockerfile'),
port: numOr(o.port, 0)
port: numOr(o.port, 0),
reportCommitStatus:
typeof o.report_commit_status === 'boolean' ? o.report_commit_status : false
};
}
@@ -260,7 +280,11 @@ export function staticToConfig(s: StaticFormState, existingJson: string): Record
folder_path: s.folderPath,
access_token: s.accessToken,
mode: s.mode,
render_markdown: s.renderMarkdown
render_markdown: s.renderMarkdown,
// New key appended at the END so existing byte-shape assertions for
// the other keys are minimally affected. Storage_* keys (added below
// only when present in the existing config) trail this on edit.
report_commit_status: s.reportCommitStatus
};
// Preserve storage_* keys set via the raw JSON editor (not yet surfaced
// as form controls) so a form round-trip doesn't silently drop them.
@@ -289,6 +313,7 @@ const DOCKERFILE_OWNED_KEYS: ReadonlySet<string> = new Set([
'context_path',
'dockerfile_path',
'port',
'report_commit_status',
'folder_path',
'mode',
'render_markdown',
@@ -317,6 +342,9 @@ export function dockerfileToConfig(
context_path: s.contextPath,
dockerfile_path: s.dockerfilePath || 'Dockerfile',
port: s.port || 0,
// New owned key appended at the END of the owned block (before any
// preserved unknown keys) so existing byte-shape assertions hold.
report_commit_status: s.reportCommitStatus,
...preserved
};
}