From 2179d65d1b090a3f634e9218feca15b45868d93d Mon Sep 17 00:00:00 2001 From: Suzanne Aitchison Date: Mon, 24 Aug 2026 10:50:00 +0100 Subject: [PATCH] feat: forward agent runner ids on deploy creation The agent runner orchestrator sets NETLIFY_AGENT_RUNNER_ID and NETLIFY_AGENT_RUNNER_SESSION_ID when it invokes the CLI. Forward them as agent_runner_id and agent_runner_session_id on the createSiteDeploy body so the API can link the deploy to its runner before the build starts, rather than at deploy completion. Both createSiteDeploy call sites now share a getDeploySourceFields helper alongside the existing deploy_source field. Co-Authored-By: Claude Opus 5 --- src/commands/deploy/deploy.ts | 5 +- src/utils/deploy/deploy-source.ts | 16 ++++ .../commands/deploy/deploy-api-routes.ts | 2 + .../commands/deploy/deploy.test.ts | 74 +++++++++++++++++++ tests/unit/utils/deploy/deploy-source.test.ts | 48 ++++++++++++ 5 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 src/utils/deploy/deploy-source.ts create mode 100644 tests/unit/utils/deploy/deploy-source.test.ts diff --git a/src/commands/deploy/deploy.ts b/src/commands/deploy/deploy.ts index e3c96fcc55c..7828979231d 100644 --- a/src/commands/deploy/deploy.ts +++ b/src/commands/deploy/deploy.ts @@ -40,6 +40,7 @@ import { } from '../../utils/command-helpers.js' import { DEFAULT_CONCURRENT_HASH, DEFAULT_DEPLOY_TIMEOUT } from '../../utils/deploy/constants.js' import { type DeployEvent, deploySite } from '../../utils/deploy/deploy-site.js' +import { getDeploySourceFields } from '../../utils/deploy/deploy-source.js' import { uploadSourceZip } from '../../utils/deploy/upload-source-zip.js' import { getEnvelopeEnv } from '../../utils/env/index.js' import { mergeDeployEnvVars } from '../../utils/env/deploy-env-vars.js' @@ -600,7 +601,7 @@ const runDeploy = async ({ draft, branch: alias, include_upload_url: options.uploadSourceZip, - deploy_source: process.env.NETLIFY_DEPLOY_SOURCE || 'cli', + ...getDeploySourceFields(), } const createDeployResponse = await api.createSiteDeploy({ siteId, title, body: createDeployBody }) @@ -1390,7 +1391,7 @@ export const deploy = async (options: DeployOptionValues, command: BaseCommand) draft, branch: alias, include_upload_url: options.uploadSourceZip, - deploy_source: process.env.NETLIFY_DEPLOY_SOURCE || 'cli', + ...getDeploySourceFields(), } // TODO: Type this properly in `@netlify/api`. diff --git a/src/utils/deploy/deploy-source.ts b/src/utils/deploy/deploy-source.ts new file mode 100644 index 00000000000..81c490eb207 --- /dev/null +++ b/src/utils/deploy/deploy-source.ts @@ -0,0 +1,16 @@ +export interface DeploySourceFields { + deploy_source: string + agent_runner_id?: string + agent_runner_session_id?: string +} + +export const getDeploySourceFields = (): DeploySourceFields => { + const agentRunnerId = process.env.NETLIFY_AGENT_RUNNER_ID + const agentRunnerSessionId = process.env.NETLIFY_AGENT_RUNNER_SESSION_ID + + return { + deploy_source: process.env.NETLIFY_DEPLOY_SOURCE || 'cli', + ...(agentRunnerId ? { agent_runner_id: agentRunnerId } : {}), + ...(agentRunnerSessionId ? { agent_runner_session_id: agentRunnerSessionId } : {}), + } +} diff --git a/tests/integration/commands/deploy/deploy-api-routes.ts b/tests/integration/commands/deploy/deploy-api-routes.ts index 8d5696c4c24..8bde2f00c7d 100644 --- a/tests/integration/commands/deploy/deploy-api-routes.ts +++ b/tests/integration/commands/deploy/deploy-api-routes.ts @@ -53,6 +53,8 @@ interface CreateDeployBody { branch?: string environment?: DeployEnvironmentVariable[] deploy_source?: string + agent_runner_id?: string + agent_runner_session_id?: string } export interface DeployRouteState { diff --git a/tests/integration/commands/deploy/deploy.test.ts b/tests/integration/commands/deploy/deploy.test.ts index efcf72613ab..1e137585460 100644 --- a/tests/integration/commands/deploy/deploy.test.ts +++ b/tests/integration/commands/deploy/deploy.test.ts @@ -1535,6 +1535,80 @@ describe.concurrent('deploy command', () => { }) }) + test('should forward agent runner ids from the environment in create deploy request', async (t) => { + await withMockDeploy(async (mockApi) => { + await withSiteBuilder(t, async (builder) => { + builder.withContentFile({ + path: 'public/index.html', + content: '

test

', + }) + + await builder.build() + + await callCli( + ['deploy', '--json', '--no-build', '--dir', 'public'], + getCLIOptions({ + apiUrl: mockApi.apiUrl, + builder, + env: { + NETLIFY_DEPLOY_SOURCE: 'agent_runner', + NETLIFY_AGENT_RUNNER_ID: 'runner-123', + NETLIFY_AGENT_RUNNER_SESSION_ID: 'session-456', + }, + }), + ).then(parseDeploy) + + const createDeployRequest = mockApi.requests.find( + (req) => req.method === 'POST' && req.path === '/api/v1/sites/site_id/deploys', + ) + expect(createDeployRequest).toBeDefined() + expect(createDeployRequest!.body as Record).toMatchObject({ + deploy_source: 'agent_runner', + agent_runner_id: 'runner-123', + agent_runner_session_id: 'session-456', + }) + }) + }) + }) + + test('should forward agent runner ids in create deploy request when building', async (t) => { + await withMockDeploy(async (mockApi) => { + await withSiteBuilder(t, async (builder) => { + builder + .withContentFile({ + path: 'public/index.html', + content: '

test

', + }) + .withNetlifyToml({ config: { build: { publish: 'public' } } }) + + await builder.build() + + await callCli( + ['deploy', '--json'], + getCLIOptions({ + apiUrl: mockApi.apiUrl, + builder, + env: { + NETLIFY_DEPLOY_SOURCE: 'agent_runner', + NETLIFY_AGENT_RUNNER_ID: 'runner-123', + NETLIFY_AGENT_RUNNER_SESSION_ID: 'session-456', + }, + }), + ).then(parseDeploy) + + const createDeployRequest = mockApi.requests.find( + (req) => req.method === 'POST' && req.path === '/api/v1/sites/site_id/deploys', + ) + expect(createDeployRequest).toBeDefined() + expect(createDeployRequest!.body as Record).toMatchObject({ + deploy_source: 'agent_runner', + agent_runner_id: 'runner-123', + agent_runner_session_id: 'session-456', + }) + }) + }) + }) + test('should include build_version in deploy body', async (t) => { await withMockDeploy(async (mockApi, deployState) => { await withSiteBuilder(t, async (builder) => { diff --git a/tests/unit/utils/deploy/deploy-source.test.ts b/tests/unit/utils/deploy/deploy-source.test.ts new file mode 100644 index 00000000000..d36d4e363fc --- /dev/null +++ b/tests/unit/utils/deploy/deploy-source.test.ts @@ -0,0 +1,48 @@ +import { afterEach, beforeEach, expect, test, vi } from 'vitest' + +import { getDeploySourceFields } from '../../../../src/utils/deploy/deploy-source.js' + +beforeEach(() => { + vi.stubEnv('NETLIFY_DEPLOY_SOURCE', undefined) + vi.stubEnv('NETLIFY_AGENT_RUNNER_ID', undefined) + vi.stubEnv('NETLIFY_AGENT_RUNNER_SESSION_ID', undefined) +}) + +afterEach(() => { + vi.unstubAllEnvs() +}) + +test('defaults the deploy source to `cli`', () => { + expect(getDeploySourceFields()).toEqual({ deploy_source: 'cli' }) +}) + +test('honors NETLIFY_DEPLOY_SOURCE', () => { + vi.stubEnv('NETLIFY_DEPLOY_SOURCE', 'agent_runner') + + expect(getDeploySourceFields()).toEqual({ deploy_source: 'agent_runner' }) +}) + +test('forwards the agent runner ids', () => { + vi.stubEnv('NETLIFY_DEPLOY_SOURCE', 'agent_runner') + vi.stubEnv('NETLIFY_AGENT_RUNNER_ID', 'runner-123') + vi.stubEnv('NETLIFY_AGENT_RUNNER_SESSION_ID', 'session-456') + + expect(getDeploySourceFields()).toEqual({ + deploy_source: 'agent_runner', + agent_runner_id: 'runner-123', + agent_runner_session_id: 'session-456', + }) +}) + +test('omits the session id when only the runner id is set', () => { + vi.stubEnv('NETLIFY_AGENT_RUNNER_ID', 'runner-123') + + expect(getDeploySourceFields()).toEqual({ deploy_source: 'cli', agent_runner_id: 'runner-123' }) +}) + +test('omits empty agent runner ids', () => { + vi.stubEnv('NETLIFY_AGENT_RUNNER_ID', '') + vi.stubEnv('NETLIFY_AGENT_RUNNER_SESSION_ID', '') + + expect(getDeploySourceFields()).toEqual({ deploy_source: 'cli' }) +})