mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-10 18:56:53 +00:00
fix(git-sources): harden webhook delivery, transport errors, and clone limits (#1249)
* fix(git-sources): harden webhook delivery, transport errors, and clone limits Map webhook-pull outcomes to real HTTP status codes (200 success, 202 debounced, 404 no source, 422 failure) instead of always returning 200, so a Git provider and any monitoring on it can tell when a delivery actually failed. Close a concurrent webhook fan-out gap: the debounce window is now re-checked inside the per-stack lock, so simultaneous deliveries for one push run a single clone instead of one per request. The whole pull/apply critical section runs under a single lock acquisition. Unwrap fetch transport causes (ENOTFOUND, ECONNREFUSED, ECONNRESET, TLS) so a clone failure surfaces an actionable, host-qualified message instead of a bare "fetch failed". Cap how many bytes a single clone may download to protect the host disk; operators can tune it with GITSOURCE_MAX_CLONE_BYTES (default 100 MB). Log webhook pull failures server-side, since the webhook path is unattended. * test(git-sources): assert surfaced host via toContain to satisfy CodeQL * fix(git-sources): bound per-file read, treat debounced webhooks as non-failure, correct clone-cap docs * docs(git-sources): correct clone-cap comment to describe a download bound, not disk
This commit is contained in:
@@ -183,15 +183,18 @@ export class WebhookService {
|
||||
}
|
||||
|
||||
const skipped = result.status === 'skipped';
|
||||
// A debounced pull is rate-limited, not failed (the route answers 202
|
||||
// Accepted). Record it as a success carrying the debounce note so it
|
||||
// does not pollute the webhook's failure history.
|
||||
this.recordExecution(
|
||||
webhookId,
|
||||
action,
|
||||
skipped ? 'failure' : 'success',
|
||||
'success',
|
||||
triggerSource,
|
||||
durationMs,
|
||||
skipped ? result.message : null,
|
||||
);
|
||||
return { success: !skipped, error: skipped ? result.message : undefined, duration_ms: durationMs };
|
||||
return { success: true, error: undefined, duration_ms: durationMs };
|
||||
}
|
||||
|
||||
private async executeRemote(
|
||||
@@ -214,13 +217,17 @@ export class WebhookService {
|
||||
const durationMs = Date.now() - startTime;
|
||||
const payload = await response.json().catch(() => ({})) as { error?: string; message?: string; status?: string };
|
||||
|
||||
if (!response.ok || payload.status === 'error' || payload.status === 'skipped') {
|
||||
if (!response.ok || payload.status === 'error') {
|
||||
const error = payload.error || payload.message || `Remote ${action} failed with status ${response.status}`;
|
||||
this.recordExecution(webhookId, action, 'failure', triggerSource, durationMs, error);
|
||||
return { success: false, error, duration_ms: durationMs };
|
||||
}
|
||||
|
||||
this.recordExecution(webhookId, action, 'success', triggerSource, durationMs, null);
|
||||
// A debounced remote pull comes back 202 with status "skipped": it was
|
||||
// accepted and rate-limited, not failed. Record it as a success with
|
||||
// the debounce note rather than failure noise.
|
||||
const skipped = payload.status === 'skipped';
|
||||
this.recordExecution(webhookId, action, 'success', triggerSource, durationMs, skipped ? (payload.message ?? null) : null);
|
||||
return { success: true, duration_ms: durationMs };
|
||||
} catch (err) {
|
||||
const durationMs = Date.now() - startTime;
|
||||
|
||||
Reference in New Issue
Block a user