Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wild-shoes-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/nextjs": patch
---

feat(nextjs): Add CI environment detection header for Next.js keyless app creation
51 changes: 50 additions & 1 deletion packages/nextjs/src/__tests__/keyless-custom-headers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down Expand Up @@ -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();
});

Expand All @@ -134,6 +178,7 @@ describe('keyless-custom-headers', () => {
xPort: '3000',
xProtocol: 'https',
xClerkAuthStatus: 'signed-out',
isCI: false,
};

const result = formatMetadataHeaders(metadata);
Expand All @@ -159,6 +204,7 @@ describe('keyless-custom-headers', () => {
xPort: '3000',
xProtocol: 'https',
xClerkAuthStatus: 'signed-out',
isCI: false,
// Missing: nodeVersion, nextVersion, npmConfigUserAgent, port
};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -222,6 +269,7 @@ describe('keyless-custom-headers', () => {
xPort: '',
xProtocol: '',
xClerkAuthStatus: '',
isCI: false,
};

const result = formatMetadataHeaders(metadata);
Expand Down Expand Up @@ -334,6 +382,7 @@ describe('keyless-custom-headers', () => {
xHost: 'example.com',
xProtocol: 'https',
xClerkAuthStatus: 'signed-out',
isCI: false,
});

// Restore original values
Expand Down
54 changes: 54 additions & 0 deletions packages/nextjs/src/server/keyless-custom-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface MetadataHeaders {
xPort: string;
xProtocol: string;
xClerkAuthStatus: string;
isCI: boolean;
}

/**
Expand All @@ -32,9 +33,58 @@ export async function collectKeylessMetadata(): Promise<MetadataHeaders> {
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(),
};
}

// 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 {
const ciIndicators = CI_ENV_VARS;

const falsyValues = new Set<string>(['', 'false', '0', 'no']);

return ciIndicators.some(indicator => {
const value = process.env[indicator];
if (value === undefined) {
return false;
}

const normalizedValue = value.trim().toLowerCase();
return !falsyValues.has(normalizedValue);
});
}

/**
* Extracts Next.js version from process title
*/
Expand Down Expand Up @@ -92,5 +142,9 @@ export function formatMetadataHeaders(metadata: MetadataHeaders): Headers {
headers.set('Clerk-Auth-Status', metadata.xClerkAuthStatus);
}

if (metadata.isCI) {
headers.set('Clerk-Is-CI', 'true');
}

return headers;
}
Loading