Skip to content

Commit 5eb3241

Browse files
authored
Always load local clerk-js when running integration tests (#2025)
* chore(repo): Install http-server * chore(repo): Wrap treekill with awaitableTreekill * chore(repo): Server locally-built clerk-js files using http-server * chore(repo): Serve clerk-js from verdaccio on CICD
1 parent 13b16aa commit 5eb3241

File tree

19 files changed

+723
-944
lines changed

19 files changed

+723
-944
lines changed

.github/workflows/base-e2e.yml

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ jobs:
4848
- name: Install @clerk/backend in /integration
4949
run: |
5050
cd integration && npm init -y && npm install @clerk/backend && cd ..
51+
- name: Install @clerk/clerk-js in os temp
52+
working-directory: ${{runner.temp}}
53+
run: mkdir clerk-js && cd clerk-js && npm init -y && npm install @clerk/clerk-js
5154
- name: Run Playwright tests
5255
run: ${{ inputs.SCRIPT }}
5356
env:

.github/workflows/ci.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,12 @@ jobs:
124124
working-directory: ./integration
125125
run: npm init -y && npm install @clerk/backend
126126

127+
- name: Install @clerk/clerk-js in os temp
128+
working-directory: ${{runner.temp}}
129+
run: mkdir clerk-js && cd clerk-js && npm init -y && npm install @clerk/clerk-js
130+
127131
- name: Run Integration Tests
128-
run: npm run test:integration:${{ matrix.test-name }}
132+
run: E2E_APP_CLERK_JS_DIR=$RUNNER_TEMP npm run test:integration:${{ matrix.test-name }}
129133
env:
130134
E2E_CLERK_VERSION: 'latest'
131135
INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }}

.github/workflows/nightly-checks.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
needs: build
1515
uses: ./.github/workflows/base-e2e.yml
1616
with:
17-
SCRIPT: 'E2E_NEXTJS_VERSION=canary npm run test:integration:nextjs'
17+
SCRIPT: 'E2E_APP_CLERK_JS_DIR=$RUNNER_TEMP E2E_NEXTJS_VERSION=canary npm run test:integration:nextjs'
1818
secrets: inherit
1919

2020
notify-slack:

integration/constants.ts

+40-1
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,53 @@ import * as path from 'node:path';
44
export const constants = {
55
TMP_DIR: path.join(process.cwd(), '.temp_integration'),
66
APPS_STATE_FILE: path.join(process.cwd(), '.temp_integration', 'state.json'),
7+
/**
8+
* A URL to a running app that will be used to run the tests against.
9+
* This is usually used when running the app has been started manually,
10+
* outside the test runner.
11+
*/
712
E2E_APP_URL: process.env.E2E_APP_URL,
13+
/**
14+
* Used to indicate which longRunning apps to start.
15+
* Can also use * to start multiple apps, eg
16+
* E2E_APP_ID=react.vite.*
17+
*/
818
E2E_APP_ID: process.env.E2E_APP_ID,
19+
/**
20+
* Controls the URL the apps will load clerk.browser.js from.
21+
* This is the same as the clerkJsUrl prop.
22+
* If this is set, clerk-js will not be served automatically from the test runner.
23+
*/
24+
E2E_APP_CLERK_JS: process.env.E2E_APP_CLERK_JS,
25+
/**
26+
* Controls the path where clerk.browser.js is located on the disk.
27+
*/
28+
E2E_APP_CLERK_JS_DIR: process.env.E2E_APP_CLERK_JS_DIR,
29+
/**
30+
* If CLEANUP=0 is used, the .tmp_integration directory will not be deleted.
31+
* This is useful for debugging locally.
32+
*/
933
CLEANUP: !(process.env.CLEANUP === '0' || process.env.CLEANUP === 'false'),
1034
DEBUG: process.env.DEBUG === 'true' || process.env.DEBUG === '1',
11-
E2E_APP_PK: process.env.E2E_APP_PK,
35+
/**
36+
* Used with E2E_APP_URL if the tests need to access BAPI.
37+
*/
1238
E2E_APP_SK: process.env.E2E_APP_SK,
39+
E2E_APP_PK: process.env.E2E_APP_PK,
40+
/**
41+
* The version of the dependency to use, controlled programmatically.
42+
*/
1343
E2E_NEXTJS_VERSION: process.env.E2E_NEXTJS_VERSION,
44+
/**
45+
* The version of the dependency to use, controlled programmatically.
46+
*/
1447
E2E_REMIX_VERSION: process.env.E2E_REMIX_VERSION,
48+
/**
49+
* The version of the dependency to use, controlled programmatically.
50+
*/
1551
E2E_VITE_VERSION: process.env.E2E_VITE_VERSION,
52+
/**
53+
* The version of the dependency to use, controlled programmatically.
54+
*/
1655
E2E_CLERK_VERSION: process.env.E2E_CLERK_VERSION,
1756
} as const;

integration/models/application.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import * as path from 'node:path';
22

3-
import treekill from 'tree-kill';
4-
5-
import { createLogger, fs, getPort, run, waitForIdleProcess, waitForServer } from '../scripts';
3+
import { awaitableTreekill, createLogger, fs, getPort, run, waitForIdleProcess, waitForServer } from '../scripts';
64
import type { ApplicationConfig } from './applicationConfig.js';
75
import type { EnvironmentConfig } from './environment.js';
86

@@ -63,9 +61,10 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi
6361
stderr: opts.detached ? fs.openSync(stderrFilePath, 'a') : undefined,
6462
log: opts.detached ? undefined : log,
6563
});
64+
// const shouldRetry = () => proc.exitCode !== 0 && proc.exitCode !== null;
6665
await waitForServer(serverUrl, { log, maxAttempts: Infinity });
6766
log(`Server started at ${serverUrl}, pid: ${proc.pid}`);
68-
cleanupFns.push(() => treekill(proc.pid, 'SIGKILL'));
67+
cleanupFns.push(() => awaitableTreekill(proc.pid, 'SIGKILL'));
6968
state.serverUrl = serverUrl;
7069
return { port, serverUrl, pid: proc.pid };
7170
},
@@ -89,7 +88,7 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi
8988
// If this is ever used as a background process, we need to make sure
9089
// it's not using the log function. See the dev() method above
9190
const proc = run(scripts.serve, { cwd: appDirPath, env: { PORT: port.toString() } });
92-
cleanupFns.push(() => treekill(proc.pid, 'SIGKILL'));
91+
cleanupFns.push(() => awaitableTreekill(proc.pid, 'SIGKILL'));
9392
await waitForIdleProcess(proc);
9493
state.serverUrl = serverUrl;
9594
return { port, serverUrl, pid: proc };

integration/models/longRunningApplication.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import treekill from 'tree-kill';
2-
3-
import { fs } from '../scripts';
1+
import { awaitableTreekill, fs } from '../scripts';
42
import type { Application } from './application';
53
import type { ApplicationConfig } from './applicationConfig';
64
import type { EnvironmentConfig } from './environment';
@@ -68,7 +66,8 @@ export const longRunningApplication = (params: LongRunningApplicationParams) =>
6866
destroy: async () => {
6967
readFromStateFile();
7068
console.log(`Destroying ${serverUrl}`);
71-
treekill(pid, 'SIGKILL');
69+
await awaitableTreekill(pid, 'SIGKILL');
70+
// TODO: Test whether this is necessary now that we have awaitableTreekill
7271
await new Promise(res => setTimeout(res, 2000));
7372
await fs.rm(appDir, { recursive: true, force: true });
7473
},
@@ -110,6 +109,5 @@ export const longRunningApplication = (params: LongRunningApplicationParams) =>
110109
},
111110
);
112111

113-
// eslint-disable-next-line
114112
return self as any as Application & ApplicationConfig & typeof self;
115113
};

integration/models/stateFile.ts

+25
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,21 @@ type StandaloneAppParams = {
1717
};
1818

1919
type StateFile = Partial<{
20+
/**
21+
* This prop describes a running application started manually by the
22+
* e2e suite user by providing the E2E_APP_URL, E2E_APP_ID, E2E_APP_PK, E2E_APP_SK variables
23+
**/
2024
standaloneApp: StandaloneAppParams;
25+
/**
26+
* This prop describes all long-running apps started by the e2e suite itself
27+
**/
2128
longRunningApps: Record<string, AppParams>;
29+
/**
30+
* This prop describes the pid of the http server that serves the clerk-js hotloaded lib.
31+
* The http-server replaces the production clerk-js delivery mechanism.
32+
* The PID is used to teardown the http-server after the tests are done.
33+
*/
34+
clerkJsHttpServerPid: number;
2235
}>;
2336

2437
const createStateFile = () => {
@@ -60,10 +73,22 @@ const createStateFile = () => {
6073
return json.longRunningApps;
6174
};
6275

76+
const setClerkJsHttpServerPid = (pid: number) => {
77+
const json = read();
78+
json.clerkJsHttpServerPid = pid;
79+
write(json);
80+
};
81+
82+
const getClerkJsHttpServerPid = () => {
83+
return read().clerkJsHttpServerPid;
84+
};
85+
6386
return {
6487
remove,
6588
setStandAloneApp,
6689
getStandAloneApp,
90+
setClerkJsHttpServerPid,
91+
getClerkJsHttpServerPid,
6792
addLongRunningApp,
6893
getLongRunningApps,
6994
};

integration/presets/envs.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ const withEmailCodes = environmentConfig()
2727
.setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['with-email-codes'].sk)
2828
.setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk)
2929
.setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in')
30-
.setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up');
30+
.setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up')
31+
.setEnvVariable('public', 'CLERK_JS', process.env.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js');
3132

3233
const withEmailLinks = environmentConfig()
3334
.setId('withEmailLinks')
3435
.setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['with-email-links'].sk)
3536
.setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-links'].pk)
3637
.setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in')
37-
.setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up');
38+
.setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up')
39+
.setEnvVariable('public', 'CLERK_JS', process.env.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js');
3840

3941
export const envs = {
4042
withEmailCodes,
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// @ts-ignore
2+
import treekill from 'tree-kill';
3+
4+
export const awaitableTreekill = (pid: number, signal: string) => {
5+
return new Promise<void>((resolve, reject) => {
6+
treekill(pid, signal, err => {
7+
if (err) {
8+
reject(err);
9+
} else {
10+
resolve();
11+
}
12+
});
13+
});
14+
};

integration/scripts/clerkJsServer.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* eslint-disable turbo/no-undeclared-env-vars */
2+
3+
import os from 'node:os';
4+
import path from 'node:path';
5+
6+
import { stateFile } from '../models/stateFile';
7+
import { awaitableTreekill, fs, waitForServer } from './index';
8+
import { run } from './run';
9+
10+
export const startClerkJsHttpServer = async () => {
11+
if (process.env.E2E_APP_CLERK_JS) {
12+
return;
13+
}
14+
if (!process.env.CI) {
15+
await copyClerkJsToTempDir();
16+
}
17+
return serveFromTempDir();
18+
};
19+
20+
export const killClerkJsHttpServer = async () => {
21+
const clerkJsHttpServerPid = stateFile.getClerkJsHttpServerPid();
22+
if (clerkJsHttpServerPid) {
23+
console.log('Killing clerkJsHttpServer', clerkJsHttpServerPid);
24+
await awaitableTreekill(clerkJsHttpServerPid, 'SIGKILL');
25+
}
26+
};
27+
28+
// If we are running the tests locally, then clerk.browser.js should be built already
29+
// so we simply copy it from packages/clerk to the same location as CICD would install it
30+
const copyClerkJsToTempDir = async () => {
31+
const clerkJsTempDir = getClerkJsTempDir();
32+
await fs.remove(clerkJsTempDir);
33+
await fs.ensureDir(clerkJsTempDir);
34+
const packagesClerkJsDistPath = path.join(process.cwd(), 'packages/clerk-js/dist');
35+
fs.copySync(packagesClerkJsDistPath, clerkJsTempDir);
36+
};
37+
38+
const serveFromTempDir = async () => {
39+
console.log('Serving clerkJs from temp dir');
40+
const port = 18211;
41+
const serverUrl = `http://localhost:${port}`;
42+
const now = Date.now();
43+
const TMP_DIR = path.join(process.cwd(), '.temp_integration');
44+
const stdoutFilePath = path.resolve(TMP_DIR, `clerkJsHttpServer.${now}.log`);
45+
const stderrFilePath = path.resolve(TMP_DIR, `clerkJsHttpServer.${now}.err.log`);
46+
const clerkJsTempDir = getClerkJsTempDir();
47+
const proc = run(`node_modules/.bin/http-server ${clerkJsTempDir} -d --gzip --cors -a localhost`, {
48+
cwd: process.cwd(),
49+
env: { PORT: port.toString() },
50+
detached: true,
51+
stdout: fs.openSync(stdoutFilePath, 'a'),
52+
stderr: fs.openSync(stderrFilePath, 'a'),
53+
});
54+
stateFile.setClerkJsHttpServerPid(proc.pid);
55+
await waitForServer(serverUrl, { log: console.log, maxAttempts: Infinity });
56+
console.log('clerk.browser.js is being served from', serverUrl);
57+
};
58+
59+
// The location where the clerk.browser.js is served from
60+
// For simplicity, on CICD we install `@clerk/clerk-js` on osTemp
61+
// so the actual clerk.browser.file is at osTemp/clerk-js/node_modules/@clerk/clerk-js/dist
62+
// Locally, it's the osTemp/clerk-js/node_modules/@clerk/clerk-js/dist
63+
// You can override it by setting the `E2E_APP_CLERK_JS_DIR` env variable
64+
const getClerkJsTempDir = () => {
65+
const osTempDir = process.env.E2E_APP_CLERK_JS_DIR || os.tmpdir();
66+
return path.join(osTempDir, ...'clerk-js/node_modules/@clerk/clerk-js/dist'.split('/'));
67+
};

integration/scripts/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ export { chunkLogger, run } from './run';
1313

1414
export * from './setup';
1515
export * from './waitForServer';
16+
export { awaitableTreekill } from './awaitableTreekill';
17+
export { startClerkJsHttpServer, killClerkJsHttpServer } from './clerkJsServer';

0 commit comments

Comments
 (0)