fix(git-sources): harden validation, RBAC, concurrency, and deploy recovery (#603)

* fix(git-sources): harden validation, RBAC, concurrency, and deploy recovery

Tightens the surface area around the Git source feature:

- Enforce HTTPS-only repo URLs server-side (regex was permissive).
- Add stack:read permission check on git-source reads and filter the
  list endpoint by callable permission.
- Validate stack names before permission checks on mutation routes so
  scoped lookups never see unvalidated input.
- Cap repo_url / branch / compose_path / env_path / token lengths and
  require the stack directory to exist before upsert.
- Wrap pull() in the per-stack mutex to eliminate the pull/delete race
  that could orphan pending data.
- Block .git/ path components in compose_path / env_path so a
  misconfigured clone cannot leak repo metadata.
- Return {applied, deployed, deployError?} on deploy failure instead of
  throwing, and surface deployError as a warning toast so the user can
  retry deploy without re-pulling.
- Always clean the stack_git_sources row on stack delete even when the
  file deletion step fails.
- Add shadow-card-bevel to the pending alert and metadata card per the
  design system.
- Handle the new 403 response on the panel fetch gracefully.
- Add diagnostic logging gated on developer_mode (isDebugEnabled) across
  fetch / pull / apply / webhook paths with credential scrubbing.

* test(git-sources): expand coverage for hardening and route validation

- New route-level suite covers HTTPS enforcement, required fields,
  max-length caps on repo_url / branch / compose_path / env_path /
  token, the stack-existence 404 guard, and GET authz.
- Service tests cover the .git metadata guard on compose and env
  paths (including nested and substring-containing "git"), pull and
  apply rejections when no source is configured or pending is
  cleared, the sha-mismatch branch, and the deploy-failure return
  shape that now carries deployError.
- E2E adds three server-side contract assertions: PUT against a
  missing stack returns 404, http:// is rejected with 400, and
  .git/config is rejected as compose_path.

* docs(git-sources): document deploy-failure recovery path

Adds a Troubleshooting entry explaining that when apply succeeds but
the subsequent deploy fails, the compose content is already on disk
and the user can retry deploy from the stack editor without
re-pulling.

* docs(git-sources): add configuration, diff, pending, and webhook screenshots
This commit is contained in:
Anso
2026-04-14 22:32:42 -04:00
committed by GitHub
parent 789437cc46
commit 00901cf5bf
11 changed files with 575 additions and 59 deletions
@@ -112,6 +112,9 @@ export function GitSourcePanel({
setAuthType('none');
setToken('');
setApplyModeOverride(null);
} else if (res.status === 403) {
setSource(null);
toast.error('You do not have permission to view this stack\'s Git source.');
} else {
const err = await res.json().catch(() => ({}));
toast.error(err?.error || 'Failed to load Git source.');
@@ -134,7 +137,7 @@ export function GitSourcePanel({
toast.error('Repository URL, branch, and compose path are required.');
return;
}
if (!/^https?:\/\//i.test(repoUrl.trim())) {
if (!/^https:\/\//i.test(repoUrl.trim())) {
toast.error('Only HTTPS repository URLs are supported.');
return;
}
@@ -235,8 +238,12 @@ export function GitSourcePanel({
body: JSON.stringify({ commitSha, deploy }),
});
if (res.ok) {
const data: { applied: boolean; deployed: boolean } = await res.json();
toast.success(data.deployed ? 'Applied and deployed.' : 'Applied successfully.');
const data: { applied: boolean; deployed: boolean; deployError?: string } = await res.json();
if (data.deployError) {
toast.warning(`Applied, but deploy failed: ${data.deployError}`);
} else {
toast.success(data.deployed ? 'Applied and deployed.' : 'Applied successfully.');
}
setDiffOpen(false);
setPull(null);
await load();
@@ -323,7 +330,7 @@ export function GitSourcePanel({
) : (
<>
{source?.pending_commit_sha && (
<div className="flex items-start gap-2 rounded-md border border-brand/30 bg-brand/5 px-3 py-2 text-xs">
<div className="flex items-start gap-2 rounded-md border border-brand/30 bg-brand/5 px-3 py-2 text-xs shadow-card-bevel">
<AlertCircle className="w-4 h-4 shrink-0 mt-0.5 text-brand" strokeWidth={1.5} />
<div className="flex-1">
<p className="font-medium">Pending update</p>
@@ -450,7 +457,7 @@ export function GitSourcePanel({
</div>
{source && (
<div className="rounded-md border border-glass-border bg-muted/30 px-3 py-2 text-[11px] text-stat-subtitle space-y-0.5">
<div className="rounded-md border border-glass-border bg-muted/30 px-3 py-2 text-[11px] text-stat-subtitle space-y-0.5 shadow-card-bevel">
<div className="flex justify-between gap-2">
<span>Last applied commit</span>
<span className="font-mono tabular-nums">