Skip to content

Commit c8941e9

Browse files
committed
fix(deploy): throw DeployError on Netlify site repo settings update failure
Make the repository update a required step in deployToNetlify, parse error JSON/text, throw DeployError on failure, and cover those cases in unit tests.
1 parent 647056c commit c8941e9

2 files changed

Lines changed: 168 additions & 2 deletions

File tree

apps/daemon/src/deploy.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,9 @@ export async function deployToNetlify({
780780
}),
781781
});
782782
if (!updateSiteResp.ok) {
783-
console.warn('Failed to update Netlify site repository settings:', await updateSiteResp.text());
783+
const errJson = await readNetlifyJson(updateSiteResp);
784+
const errMsg = errJson?.message || errJson?.error || `Failed to update Netlify site repository settings (${updateSiteResp.status}).`;
785+
throw new DeployError(errMsg, 502, errJson);
784786
}
785787
} else {
786788
// Create a new site linked to the GitHub repo
@@ -843,7 +845,9 @@ export async function deployToNetlify({
843845
}),
844846
});
845847
if (!updateSiteResp.ok) {
846-
console.warn('Failed to update fallback Netlify site repository settings:', await updateSiteResp.text());
848+
const errJson = await readNetlifyJson(updateSiteResp);
849+
const errMsg = errJson?.message || errJson?.error || `Failed to update fallback Netlify site repository settings (${updateSiteResp.status}).`;
850+
throw new DeployError(errMsg, 502, errJson);
847851
}
848852
}
849853
}

apps/daemon/tests/deploy.test.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,168 @@ describe('netlify and railway deploys', () => {
406406
expect(requestedUrls).toContain('GET https://api.netlify.com/api/v1/deploys/deploy-1');
407407
});
408408

409+
it('throws DeployError when existing site repository settings update fails', async () => {
410+
const fetchMock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
411+
const url =
412+
typeof input === 'string'
413+
? input
414+
: input instanceof Request
415+
? input.url
416+
: String(input);
417+
const method = init?.method || (input instanceof Request ? input.method : 'GET');
418+
419+
if (url === 'https://api.github.com/user' && method === 'GET') {
420+
return new Response(JSON.stringify({ login: 'testuser' }), {
421+
status: 200,
422+
headers: { 'content-type': 'application/json' },
423+
});
424+
}
425+
if (url === 'https://api.github.com/repos/testuser/od-p1' && method === 'GET') {
426+
return new Response(JSON.stringify({ id: 123, name: 'od-p1' }), {
427+
status: 200,
428+
headers: { 'content-type': 'application/json' },
429+
});
430+
}
431+
if (url === 'https://api.github.com/repos/testuser/od-p1/keys' && method === 'POST') {
432+
return new Response(JSON.stringify({ id: 789 }), {
433+
status: 201,
434+
headers: { 'content-type': 'application/json' },
435+
});
436+
}
437+
if (url.startsWith('https://api.github.com/repos/testuser/od-p1/contents/') && method === 'GET') {
438+
return new Response('', { status: 404 });
439+
}
440+
if (url.startsWith('https://api.github.com/repos/testuser/od-p1/contents/') && method === 'PUT') {
441+
return new Response(JSON.stringify({ content: { sha: 'abc123' } }), {
442+
status: 201,
443+
headers: { 'content-type': 'application/json' },
444+
});
445+
}
446+
if (url.endsWith('/deploy_keys') && method === 'POST') {
447+
return new Response(JSON.stringify({ id: 'deploy-key-1', public_key: 'ssh-rsa AAAAB3NzaC1...' }), {
448+
status: 200,
449+
headers: { 'content-type': 'application/json' },
450+
});
451+
}
452+
if (url.endsWith('/sites/site-1') && method === 'PUT') {
453+
return new Response(JSON.stringify({ message: 'Repository settings update rejected' }), {
454+
status: 422,
455+
headers: { 'content-type': 'application/json' },
456+
});
457+
}
458+
459+
throw new Error(`Unexpected fetch: ${method} ${url}`);
460+
});
461+
vi.stubGlobal('fetch', fetchMock);
462+
463+
await expect(
464+
deployToNetlify({
465+
config: { token: 'netlify-token-secret', githubToken: 'ghp-test-token' },
466+
projectId: 'p1',
467+
projectsRoot: '/tmp/test-projects',
468+
files: [
469+
{
470+
file: 'index.html',
471+
data: Buffer.from('<!doctype html><h1>Hello</h1>'),
472+
contentType: 'text/html',
473+
},
474+
],
475+
priorMetadata: { siteId: 'site-1' },
476+
})
477+
).rejects.toThrowError(/Repository settings update rejected/);
478+
});
479+
480+
it('throws DeployError when fallback site repository settings update fails', async () => {
481+
const fetchMock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
482+
const url =
483+
typeof input === 'string'
484+
? input
485+
: input instanceof Request
486+
? input.url
487+
: String(input);
488+
const method = init?.method || (input instanceof Request ? input.method : 'GET');
489+
490+
if (url === 'https://api.github.com/user' && method === 'GET') {
491+
return new Response(JSON.stringify({ login: 'testuser' }), {
492+
status: 200,
493+
headers: { 'content-type': 'application/json' },
494+
});
495+
}
496+
if (url === 'https://api.github.com/repos/testuser/od-p1' && method === 'GET') {
497+
return new Response(JSON.stringify({ id: 123, name: 'od-p1' }), {
498+
status: 200,
499+
headers: { 'content-type': 'application/json' },
500+
});
501+
}
502+
if (url === 'https://api.github.com/repos/testuser/od-p1/keys' && method === 'POST') {
503+
return new Response(JSON.stringify({ id: 789 }), {
504+
status: 201,
505+
headers: { 'content-type': 'application/json' },
506+
});
507+
}
508+
if (url.startsWith('https://api.github.com/repos/testuser/od-p1/contents/') && method === 'GET') {
509+
return new Response('', { status: 404 });
510+
}
511+
if (url.startsWith('https://api.github.com/repos/testuser/od-p1/contents/') && method === 'PUT') {
512+
return new Response(JSON.stringify({ content: { sha: 'abc123' } }), {
513+
status: 201,
514+
headers: { 'content-type': 'application/json' },
515+
});
516+
}
517+
if (url.includes('/sites?name=od-p1') && method === 'GET') {
518+
return new Response(JSON.stringify([]), {
519+
status: 200,
520+
headers: { 'content-type': 'application/json' },
521+
});
522+
}
523+
if (url.endsWith('/deploy_keys') && method === 'POST') {
524+
return new Response(JSON.stringify({ id: 'deploy-key-1', public_key: 'ssh-rsa AAAAB3NzaC1...' }), {
525+
status: 200,
526+
headers: { 'content-type': 'application/json' },
527+
});
528+
}
529+
if (url.endsWith('/sites') && method === 'POST') {
530+
const parsed = JSON.parse(init?.body ? String(init.body) : '{}');
531+
if (parsed.repo) {
532+
return new Response(JSON.stringify({ message: 'Direct create failed' }), {
533+
status: 400,
534+
headers: { 'content-type': 'application/json' },
535+
});
536+
} else {
537+
return new Response(JSON.stringify({ id: 'site-fallback', site_id: 'site-fallback' }), {
538+
status: 200,
539+
headers: { 'content-type': 'application/json' },
540+
});
541+
}
542+
}
543+
if (url.endsWith('/sites/site-fallback') && method === 'PUT') {
544+
return new Response(JSON.stringify({ message: 'Fallback update rejected' }), {
545+
status: 422,
546+
headers: { 'content-type': 'application/json' },
547+
});
548+
}
549+
550+
throw new Error(`Unexpected fetch: ${method} ${url}`);
551+
});
552+
vi.stubGlobal('fetch', fetchMock);
553+
554+
await expect(
555+
deployToNetlify({
556+
config: { token: 'netlify-token-secret', githubToken: 'ghp-test-token' },
557+
projectId: 'p1',
558+
projectsRoot: '/tmp/test-projects',
559+
files: [
560+
{
561+
file: 'index.html',
562+
data: Buffer.from('<!doctype html><h1>Hello</h1>'),
563+
contentType: 'text/html',
564+
},
565+
],
566+
})
567+
).rejects.toThrowError(/Fallback update rejected/);
568+
});
569+
570+
409571
it('creates a Railway project, service, deployment, and service domain from the UI-backed file set', async () => {
410572
const graphQlCalls: Array<{ query: string; variables: Record<string, unknown> }> = [];
411573
const uploadedPaths: string[] = [];

0 commit comments

Comments
 (0)