Skip to content

fix(repo): Always load local clerk-js when running integration tests #2025

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 3, 2023
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
3 changes: 3 additions & 0 deletions .github/workflows/base-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ jobs:
- name: Install @clerk/backend in /integration
run: |
cd integration && npm init -y && npm install @clerk/backend && cd ..
- name: Install @clerk/clerk-js in os temp
working-directory: ${{runner.temp}}
run: mkdir clerk-js && cd clerk-js && npm init -y && npm install @clerk/clerk-js
- name: Run Playwright tests
run: ${{ inputs.SCRIPT }}
env:
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,12 @@ jobs:
working-directory: ./integration
run: npm init -y && npm install @clerk/backend

- name: Install @clerk/clerk-js in os temp
working-directory: ${{runner.temp}}
run: mkdir clerk-js && cd clerk-js && npm init -y && npm install @clerk/clerk-js

- name: Run Integration Tests
run: npm run test:integration:${{ matrix.test-name }}
run: E2E_APP_CLERK_JS_DIR=$RUNNER_TEMP npm run test:integration:${{ matrix.test-name }}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Define the env var in the env section below that script

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opening a second PR, thanks @LekoArts that's cleaner

env:
E2E_CLERK_VERSION: 'latest'
INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nightly-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
needs: build
uses: ./.github/workflows/base-e2e.yml
with:
SCRIPT: 'E2E_NEXTJS_VERSION=canary npm run test:integration:nextjs'
SCRIPT: 'E2E_APP_CLERK_JS_DIR=$RUNNER_TEMP E2E_NEXTJS_VERSION=canary npm run test:integration:nextjs'
secrets: inherit

notify-slack:
Expand Down
41 changes: 40 additions & 1 deletion integration/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,53 @@ import * as path from 'node:path';
export const constants = {
TMP_DIR: path.join(process.cwd(), '.temp_integration'),
APPS_STATE_FILE: path.join(process.cwd(), '.temp_integration', 'state.json'),
/**
* A URL to a running app that will be used to run the tests against.
* This is usually used when running the app has been started manually,
* outside the test runner.
*/
E2E_APP_URL: process.env.E2E_APP_URL,
/**
* Used to indicate which longRunning apps to start.
* Can also use * to start multiple apps, eg
* E2E_APP_ID=react.vite.*
*/
E2E_APP_ID: process.env.E2E_APP_ID,
/**
* Controls the URL the apps will load clerk.browser.js from.
* This is the same as the clerkJsUrl prop.
* If this is set, clerk-js will not be served automatically from the test runner.
*/
E2E_APP_CLERK_JS: process.env.E2E_APP_CLERK_JS,
/**
* Controls the path where clerk.browser.js is located on the disk.
*/
E2E_APP_CLERK_JS_DIR: process.env.E2E_APP_CLERK_JS_DIR,
/**
* If CLEANUP=0 is used, the .tmp_integration directory will not be deleted.
* This is useful for debugging locally.
*/
CLEANUP: !(process.env.CLEANUP === '0' || process.env.CLEANUP === 'false'),
DEBUG: process.env.DEBUG === 'true' || process.env.DEBUG === '1',
E2E_APP_PK: process.env.E2E_APP_PK,
/**
* Used with E2E_APP_URL if the tests need to access BAPI.
*/
E2E_APP_SK: process.env.E2E_APP_SK,
E2E_APP_PK: process.env.E2E_APP_PK,
/**
* The version of the dependency to use, controlled programmatically.
*/
E2E_NEXTJS_VERSION: process.env.E2E_NEXTJS_VERSION,
/**
* The version of the dependency to use, controlled programmatically.
*/
E2E_REMIX_VERSION: process.env.E2E_REMIX_VERSION,
/**
* The version of the dependency to use, controlled programmatically.
*/
E2E_VITE_VERSION: process.env.E2E_VITE_VERSION,
/**
* The version of the dependency to use, controlled programmatically.
*/
E2E_CLERK_VERSION: process.env.E2E_CLERK_VERSION,
} as const;
9 changes: 4 additions & 5 deletions integration/models/application.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import * as path from 'node:path';

import treekill from 'tree-kill';

import { createLogger, fs, getPort, run, waitForIdleProcess, waitForServer } from '../scripts';
import { awaitableTreekill, createLogger, fs, getPort, run, waitForIdleProcess, waitForServer } from '../scripts';
import type { ApplicationConfig } from './applicationConfig.js';
import type { EnvironmentConfig } from './environment.js';

Expand Down Expand Up @@ -63,9 +61,10 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi
stderr: opts.detached ? fs.openSync(stderrFilePath, 'a') : undefined,
log: opts.detached ? undefined : log,
});
// const shouldRetry = () => proc.exitCode !== 0 && proc.exitCode !== null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Dead comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should've been a todo - changing it in the new PR

await waitForServer(serverUrl, { log, maxAttempts: Infinity });
log(`Server started at ${serverUrl}, pid: ${proc.pid}`);
cleanupFns.push(() => treekill(proc.pid, 'SIGKILL'));
cleanupFns.push(() => awaitableTreekill(proc.pid, 'SIGKILL'));
state.serverUrl = serverUrl;
return { port, serverUrl, pid: proc.pid };
},
Expand All @@ -89,7 +88,7 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi
// If this is ever used as a background process, we need to make sure
// it's not using the log function. See the dev() method above
const proc = run(scripts.serve, { cwd: appDirPath, env: { PORT: port.toString() } });
cleanupFns.push(() => treekill(proc.pid, 'SIGKILL'));
cleanupFns.push(() => awaitableTreekill(proc.pid, 'SIGKILL'));
await waitForIdleProcess(proc);
state.serverUrl = serverUrl;
return { port, serverUrl, pid: proc };
Expand Down
8 changes: 3 additions & 5 deletions integration/models/longRunningApplication.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import treekill from 'tree-kill';

import { fs } from '../scripts';
import { awaitableTreekill, fs } from '../scripts';
import type { Application } from './application';
import type { ApplicationConfig } from './applicationConfig';
import type { EnvironmentConfig } from './environment';
Expand Down Expand Up @@ -68,7 +66,8 @@ export const longRunningApplication = (params: LongRunningApplicationParams) =>
destroy: async () => {
readFromStateFile();
console.log(`Destroying ${serverUrl}`);
treekill(pid, 'SIGKILL');
await awaitableTreekill(pid, 'SIGKILL');
// TODO: Test whether this is necessary now that we have awaitableTreekill
await new Promise(res => setTimeout(res, 2000));
await fs.rm(appDir, { recursive: true, force: true });
},
Expand Down Expand Up @@ -110,6 +109,5 @@ export const longRunningApplication = (params: LongRunningApplicationParams) =>
},
);

// eslint-disable-next-line
return self as any as Application & ApplicationConfig & typeof self;
};
25 changes: 25 additions & 0 deletions integration/models/stateFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,21 @@ type StandaloneAppParams = {
};

type StateFile = Partial<{
/**
* This prop describes a running application started manually by the
* e2e suite user by providing the E2E_APP_URL, E2E_APP_ID, E2E_APP_PK, E2E_APP_SK variables
**/
standaloneApp: StandaloneAppParams;
/**
* This prop describes all long-running apps started by the e2e suite itself
**/
longRunningApps: Record<string, AppParams>;
/**
* This prop describes the pid of the http server that serves the clerk-js hotloaded lib.
* The http-server replaces the production clerk-js delivery mechanism.
* The PID is used to teardown the http-server after the tests are done.
*/
clerkJsHttpServerPid: number;
}>;

const createStateFile = () => {
Expand Down Expand Up @@ -60,10 +73,22 @@ const createStateFile = () => {
return json.longRunningApps;
};

const setClerkJsHttpServerPid = (pid: number) => {
const json = read();
json.clerkJsHttpServerPid = pid;
write(json);
};

const getClerkJsHttpServerPid = () => {
return read().clerkJsHttpServerPid;
};

return {
remove,
setStandAloneApp,
getStandAloneApp,
setClerkJsHttpServerPid,
getClerkJsHttpServerPid,
addLongRunningApp,
getLongRunningApps,
};
Expand Down
6 changes: 4 additions & 2 deletions integration/presets/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ const withEmailCodes = environmentConfig()
.setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['with-email-codes'].sk)
.setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk)
.setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in')
.setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up');
.setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up')
.setEnvVariable('public', 'CLERK_JS', process.env.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js');

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

export const envs = {
withEmailCodes,
Expand Down
14 changes: 14 additions & 0 deletions integration/scripts/awaitableTreekill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// @ts-ignore
import treekill from 'tree-kill';

export const awaitableTreekill = (pid: number, signal: string) => {
return new Promise<void>((resolve, reject) => {
treekill(pid, signal, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
67 changes: 67 additions & 0 deletions integration/scripts/clerkJsServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* eslint-disable turbo/no-undeclared-env-vars */

import os from 'node:os';
import path from 'node:path';

import { stateFile } from '../models/stateFile';
import { awaitableTreekill, fs, waitForServer } from './index';
import { run } from './run';

export const startClerkJsHttpServer = async () => {
if (process.env.E2E_APP_CLERK_JS) {
return;
}
if (!process.env.CI) {
await copyClerkJsToTempDir();
}
return serveFromTempDir();
};

export const killClerkJsHttpServer = async () => {
const clerkJsHttpServerPid = stateFile.getClerkJsHttpServerPid();
if (clerkJsHttpServerPid) {
console.log('Killing clerkJsHttpServer', clerkJsHttpServerPid);
await awaitableTreekill(clerkJsHttpServerPid, 'SIGKILL');
}
};

// If we are running the tests locally, then clerk.browser.js should be built already
// so we simply copy it from packages/clerk to the same location as CICD would install it
const copyClerkJsToTempDir = async () => {
const clerkJsTempDir = getClerkJsTempDir();
await fs.remove(clerkJsTempDir);
await fs.ensureDir(clerkJsTempDir);
const packagesClerkJsDistPath = path.join(process.cwd(), 'packages/clerk-js/dist');
fs.copySync(packagesClerkJsDistPath, clerkJsTempDir);
};

const serveFromTempDir = async () => {
console.log('Serving clerkJs from temp dir');
const port = 18211;
const serverUrl = `http://localhost:${port}`;
const now = Date.now();
const TMP_DIR = path.join(process.cwd(), '.temp_integration');
const stdoutFilePath = path.resolve(TMP_DIR, `clerkJsHttpServer.${now}.log`);
const stderrFilePath = path.resolve(TMP_DIR, `clerkJsHttpServer.${now}.err.log`);
const clerkJsTempDir = getClerkJsTempDir();
const proc = run(`node_modules/.bin/http-server ${clerkJsTempDir} -d --gzip --cors -a localhost`, {
cwd: process.cwd(),
env: { PORT: port.toString() },
detached: true,
stdout: fs.openSync(stdoutFilePath, 'a'),
stderr: fs.openSync(stderrFilePath, 'a'),
});
stateFile.setClerkJsHttpServerPid(proc.pid);
await waitForServer(serverUrl, { log: console.log, maxAttempts: Infinity });
console.log('clerk.browser.js is being served from', serverUrl);
};

// The location where the clerk.browser.js is served from
// For simplicity, on CICD we install `@clerk/clerk-js` on osTemp
// so the actual clerk.browser.file is at osTemp/clerk-js/node_modules/@clerk/clerk-js/dist
// Locally, it's the osTemp/clerk-js/node_modules/@clerk/clerk-js/dist
// You can override it by setting the `E2E_APP_CLERK_JS_DIR` env variable
const getClerkJsTempDir = () => {
const osTempDir = process.env.E2E_APP_CLERK_JS_DIR || os.tmpdir();
return path.join(osTempDir, ...'clerk-js/node_modules/@clerk/clerk-js/dist'.split('/'));
};
2 changes: 2 additions & 0 deletions integration/scripts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export { chunkLogger, run } from './run';

export * from './setup';
export * from './waitForServer';
export { awaitableTreekill } from './awaitableTreekill';
export { startClerkJsHttpServer, killClerkJsHttpServer } from './clerkJsServer';
Loading