From 7f8febc365c8f1a35fba6ee00941530be3377d11 Mon Sep 17 00:00:00 2001 From: Heat Hamilton Date: Wed, 24 Sep 2025 17:58:50 -0400 Subject: [PATCH 1/4] Add header for CI environment --- .../src/server/keyless-custom-headers.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/nextjs/src/server/keyless-custom-headers.ts b/packages/nextjs/src/server/keyless-custom-headers.ts index ab5b4722738..828c2bcf23e 100644 --- a/packages/nextjs/src/server/keyless-custom-headers.ts +++ b/packages/nextjs/src/server/keyless-custom-headers.ts @@ -13,6 +13,7 @@ interface MetadataHeaders { xPort: string; xProtocol: string; xClerkAuthStatus: string; + isCI: boolean; } /** @@ -32,9 +33,56 @@ export async function collectKeylessMetadata(): Promise { xHost: headerStore.get('x-forwarded-host') ?? 'unknown x-forwarded-host', xProtocol: headerStore.get('x-forwarded-proto') ?? 'unknown x-forwarded-proto', xClerkAuthStatus: headerStore.get('x-clerk-auth-status') ?? 'unknown x-clerk-auth-status', + isCI: detectCIEnvironment(), }; } +/** + * Detects if the application is running in a CI environment + */ +function detectCIEnvironment(): boolean { + // Common CI environment variables + const ciIndicators = [ + 'CI', + 'CONTINUOUS_INTEGRATION', + 'BUILD_NUMBER', + 'BUILD_ID', + 'BUILDKITE', + 'CIRCLECI', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'JENKINS_URL', + 'TRAVIS', + 'APPVEYOR', + 'WERCKER', + 'DRONE', + 'CODESHIP', + 'SEMAPHORE', + 'SHIPPABLE', + 'TEAMCITY_VERSION', + 'BAMBOO_BUILDKEY', + 'GO_PIPELINE_NAME', + 'TF_BUILD', + 'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI', + 'BITBUCKET_BUILD_NUMBER', + 'HEROKU_TEST_RUN_ID', + 'VERCEL', + 'NETLIFY', + ]; + + return ciIndicators.some(indicator => { + const value = process.env[indicator]; + if (value === undefined) { + return false; + } + + const normalizedValue = value.trim().toLowerCase(); + const falsyValues = ['', 'false', '0', 'no']; + + return !falsyValues.includes(normalizedValue); + }); +} + /** * Extracts Next.js version from process title */ @@ -92,5 +140,7 @@ export function formatMetadataHeaders(metadata: MetadataHeaders): Headers { headers.set('Clerk-Auth-Status', metadata.xClerkAuthStatus); } + headers.set('Clerk-Is-CI', metadata.isCI.toString()); + return headers; } From 1c0d64f7fe5a63358a7cae3e0d8af3a91b9a2324 Mon Sep 17 00:00:00 2001 From: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:45:11 -0400 Subject: [PATCH 2/4] Create wild-shoes-divide.md --- .changeset/wild-shoes-divide.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wild-shoes-divide.md diff --git a/.changeset/wild-shoes-divide.md b/.changeset/wild-shoes-divide.md new file mode 100644 index 00000000000..8cdf0600ce1 --- /dev/null +++ b/.changeset/wild-shoes-divide.md @@ -0,0 +1,5 @@ +--- +"@clerk/nextjs": patch +--- + +feat(nextjs): Add CI environment detection header for Next.js keyless app creation From 2a5aa449495e90d65a77b90bd8552616cc2a88b5 Mon Sep 17 00:00:00 2001 From: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:39:31 -0400 Subject: [PATCH 3/4] Update packages/nextjs/src/server/keyless-custom-headers.ts Co-authored-by: Tom Milewski --- packages/nextjs/src/server/keyless-custom-headers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nextjs/src/server/keyless-custom-headers.ts b/packages/nextjs/src/server/keyless-custom-headers.ts index 828c2bcf23e..c8cad5cf407 100644 --- a/packages/nextjs/src/server/keyless-custom-headers.ts +++ b/packages/nextjs/src/server/keyless-custom-headers.ts @@ -70,6 +70,8 @@ function detectCIEnvironment(): boolean { 'NETLIFY', ]; + const falsyValues = new Set(['', 'false', '0', 'no']); + return ciIndicators.some(indicator => { const value = process.env[indicator]; if (value === undefined) { @@ -77,9 +79,7 @@ function detectCIEnvironment(): boolean { } const normalizedValue = value.trim().toLowerCase(); - const falsyValues = ['', 'false', '0', 'no']; - - return !falsyValues.includes(normalizedValue); + return !falsyValues.has(normalizedValue); }); } From 7b1060b34635b497b2bec813fd132e1ce77c99c3 Mon Sep 17 00:00:00 2001 From: Heat Hamilton Date: Thu, 25 Sep 2025 10:20:39 -0400 Subject: [PATCH 4/4] Only add clerk-is-ci header when true --- .../__tests__/keyless-custom-headers.test.ts | 51 ++++++++++++++- .../src/server/keyless-custom-headers.ts | 62 ++++++++++--------- 2 files changed, 83 insertions(+), 30 deletions(-) diff --git a/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts b/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts index 63e01046e3e..48cc8cb896e 100644 --- a/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts +++ b/packages/nextjs/src/__tests__/keyless-custom-headers.test.ts @@ -4,6 +4,40 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { collectKeylessMetadata, formatMetadataHeaders } from '../server/keyless-custom-headers'; +const CI_ENV_VARS = [ + 'CI', + 'CONTINUOUS_INTEGRATION', + 'BUILD_NUMBER', + 'BUILD_ID', + 'BUILDKITE', + 'CIRCLECI', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'JENKINS_URL', + 'TRAVIS', + 'APPVEYOR', + 'WERCKER', + 'DRONE', + 'CODESHIP', + 'SEMAPHORE', + 'SHIPPABLE', + 'TEAMCITY_VERSION', + 'BAMBOO_BUILDKEY', + 'GO_PIPELINE_NAME', + 'TF_BUILD', + 'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI', + 'BITBUCKET_BUILD_NUMBER', + 'HEROKU_TEST_RUN_ID', + 'VERCEL', + 'NETLIFY', +]; +// Helper function to clear all CI environment variables +function clearAllCIEnvironmentVariables(): void { + CI_ENV_VARS.forEach(indicator => { + vi.stubEnv(indicator, undefined); + }); +} + // Default mock headers for keyless-custom-headers.ts const defaultMockHeaders = new Headers({ 'User-Agent': 'Mozilla/5.0 (Test Browser)', @@ -113,11 +147,21 @@ describe('keyless-custom-headers', () => { beforeEach(() => { vi.clearAllMocks(); mockHeaders.mockImplementation(async () => createMockHeaders()); + + // Stub all environment variables that collectKeylessMetadata might access + vi.stubEnv('npm_config_user_agent', undefined); + vi.stubEnv('PORT', undefined); + // Clear all CI environment variables + clearAllCIEnvironmentVariables(); }); afterEach(() => { vi.restoreAllMocks(); - vi.unstubAllEnvs(); + // Don't use vi.unstubAllEnvs() as it restores real environment variables + // Instead, explicitly stub all environment variables to undefined + vi.stubEnv('npm_config_user_agent', undefined); + vi.stubEnv('PORT', undefined); + clearAllCIEnvironmentVariables(); mockHeaders.mockReset(); }); @@ -134,6 +178,7 @@ describe('keyless-custom-headers', () => { xPort: '3000', xProtocol: 'https', xClerkAuthStatus: 'signed-out', + isCI: false, }; const result = formatMetadataHeaders(metadata); @@ -159,6 +204,7 @@ describe('keyless-custom-headers', () => { xPort: '3000', xProtocol: 'https', xClerkAuthStatus: 'signed-out', + isCI: false, // Missing: nodeVersion, nextVersion, npmConfigUserAgent, port }; @@ -191,6 +237,7 @@ describe('keyless-custom-headers', () => { xPort: 'test-x-port', xProtocol: 'test-x-protocol', xClerkAuthStatus: 'test-auth-status', + isCI: false, }; const result = formatMetadataHeaders(metadata); @@ -222,6 +269,7 @@ describe('keyless-custom-headers', () => { xPort: '', xProtocol: '', xClerkAuthStatus: '', + isCI: false, }; const result = formatMetadataHeaders(metadata); @@ -334,6 +382,7 @@ describe('keyless-custom-headers', () => { xHost: 'example.com', xProtocol: 'https', xClerkAuthStatus: 'signed-out', + isCI: false, }); // Restore original values diff --git a/packages/nextjs/src/server/keyless-custom-headers.ts b/packages/nextjs/src/server/keyless-custom-headers.ts index c8cad5cf407..5e216933843 100644 --- a/packages/nextjs/src/server/keyless-custom-headers.ts +++ b/packages/nextjs/src/server/keyless-custom-headers.ts @@ -37,38 +37,40 @@ export async function collectKeylessMetadata(): Promise { }; } +// Common CI environment variables +const CI_ENV_VARS = [ + 'CI', + 'CONTINUOUS_INTEGRATION', + 'BUILD_NUMBER', + 'BUILD_ID', + 'BUILDKITE', + 'CIRCLECI', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'JENKINS_URL', + 'TRAVIS', + 'APPVEYOR', + 'WERCKER', + 'DRONE', + 'CODESHIP', + 'SEMAPHORE', + 'SHIPPABLE', + 'TEAMCITY_VERSION', + 'BAMBOO_BUILDKEY', + 'GO_PIPELINE_NAME', + 'TF_BUILD', + 'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI', + 'BITBUCKET_BUILD_NUMBER', + 'HEROKU_TEST_RUN_ID', + 'VERCEL', + 'NETLIFY', +]; + /** * Detects if the application is running in a CI environment */ function detectCIEnvironment(): boolean { - // Common CI environment variables - const ciIndicators = [ - 'CI', - 'CONTINUOUS_INTEGRATION', - 'BUILD_NUMBER', - 'BUILD_ID', - 'BUILDKITE', - 'CIRCLECI', - 'GITHUB_ACTIONS', - 'GITLAB_CI', - 'JENKINS_URL', - 'TRAVIS', - 'APPVEYOR', - 'WERCKER', - 'DRONE', - 'CODESHIP', - 'SEMAPHORE', - 'SHIPPABLE', - 'TEAMCITY_VERSION', - 'BAMBOO_BUILDKEY', - 'GO_PIPELINE_NAME', - 'TF_BUILD', - 'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI', - 'BITBUCKET_BUILD_NUMBER', - 'HEROKU_TEST_RUN_ID', - 'VERCEL', - 'NETLIFY', - ]; + const ciIndicators = CI_ENV_VARS; const falsyValues = new Set(['', 'false', '0', 'no']); @@ -140,7 +142,9 @@ export function formatMetadataHeaders(metadata: MetadataHeaders): Headers { headers.set('Clerk-Auth-Status', metadata.xClerkAuthStatus); } - headers.set('Clerk-Is-CI', metadata.isCI.toString()); + if (metadata.isCI) { + headers.set('Clerk-Is-CI', 'true'); + } return headers; }