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:
Anso
2026-05-29 09:43:37 -04:00
committed by GitHub
parent b33a0e8422
commit 2844f606cd
10 changed files with 722 additions and 166 deletions
+15 -1
View File
@@ -9,7 +9,7 @@
*/
import { describe, it, expect, vi } from 'vitest';
import type { Response } from 'express';
import { gitSourceStatus, sendGitSourceError } from '../utils/gitSourceHttp';
import { gitSourceStatus, sendGitSourceError, webhookPullStatus } from '../utils/gitSourceHttp';
import { GitSourceError } from '../services/GitSourceService';
describe('gitSourceStatus', () => {
@@ -32,6 +32,20 @@ describe('gitSourceStatus', () => {
});
});
describe('webhookPullStatus', () => {
it('maps a successful pull/apply to 200', () => {
expect(webhookPullStatus('success')).toBe(200);
});
it('maps a debounced (skipped) pull to 202', () => {
expect(webhookPullStatus('skipped')).toBe(202);
});
it('maps a failed pull/apply to 422, never 200', () => {
expect(webhookPullStatus('error')).toBe(422);
});
});
describe('sendGitSourceError', () => {
function mockRes() {
const res = { status: vi.fn(), json: vi.fn() } as unknown as Response;