Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
59c6629
feat(e2e): use PLAPI to dynamically create with-email-codes instance
dstaley Aug 14, 2026
22e28b9
fix(e2e): encode run keys
dstaley Aug 14, 2026
7a1b10e
fix(e2e): don't use platform api key in hash
dstaley Aug 14, 2026
c298b65
fix(e2e): make instance keys side-effect free
dstaley Aug 14, 2026
b40bb46
fix(e2e): update import
dstaley Aug 14, 2026
9a6bf24
fix: support dynamic client registration
dstaley Aug 17, 2026
b3adc03
fix: always grant oauth consent
dstaley Aug 17, 2026
f42882a
fix: grant oauth consent
dstaley Aug 17, 2026
3100237
fix: run cleanup adter uploading test results
dstaley Aug 17, 2026
c671d24
fix: correct test-results location
dstaley Aug 17, 2026
b111ade
fix: log cf ray
dstaley Aug 17, 2026
3c952bc
fix: configure allowed origins for electron
dstaley Aug 17, 2026
0c2d357
fix: remove default settings
dstaley Aug 18, 2026
dad9633
fix: do not force organization selection
dstaley Aug 18, 2026
e927c74
experiment: log instance name
dstaley Aug 18, 2026
92fd13b
feat: add with-custom-roles to dynamic config
dstaley Aug 19, 2026
07ae7a6
fix: rm invalid fields for new instances
dstaley Aug 19, 2026
888e5d7
fix: prefix roles with "org:"
dstaley Aug 19, 2026
8190c2b
fix: create role with permission IDs
dstaley Aug 19, 2026
f06eb4b
fix: assign to existing admin/member roles
dstaley Aug 19, 2026
87f9776
fix: don't throw on existing permissions
dstaley Aug 19, 2026
5d1d0ed
fix: correctly access underlying error
dstaley Aug 19, 2026
88c6c56
fix: do not force organization selection
dstaley Aug 19, 2026
e321180
fix: rm jwt version assertion
dstaley Aug 19, 2026
da2596f
fix: attribute applications to e2e tests
dstaley Aug 19, 2026
b06eaad
fix: cleanup
dstaley Aug 20, 2026
a091669
Merge branch 'main' into ds.exp/plapi-e2e
dstaley Aug 20, 2026
7df9f7b
Revert "experiment: log instance name"
dstaley Aug 20, 2026
84074e8
docs: improve docs
dstaley Aug 20, 2026
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
2 changes: 2 additions & 0 deletions .changeset/tidy-env-awaits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
22 changes: 13 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,8 @@ jobs:
pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
fi
env:
CLERK_E2E_OAUTH_PROVIDER_CLIENT_SECRET: ${{ secrets.CLERK_E2E_OAUTH_PROVIDER_CLIENT_SECRET }}
CLERK_PLATFORM_API_KEY: ${{ secrets.CLERK_PLATFORM_API_KEY }}
E2E_DEBUG: '1'
E2E_APP_CLERK_JS_DIR: ${{runner.temp}}
E2E_APP_CLERK_UI_DIR: ${{runner.temp}}
Expand All @@ -534,14 +536,6 @@ jobs:
NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/integration/certs/rootCA.pem
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}

- name: Delete integration-test users
if: ${{ always() && steps.integration-tests.outcome != 'skipped' }}
timeout-minutes: 4
run: pnpm test:integration:cleanup
env:
INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }}
NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/integration/certs/rootCA.pem

- name: Sanitize artifact name
if: ${{ cancelled() || failure() }}
id: sanitize
Expand All @@ -556,9 +550,19 @@ jobs:
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: playwright-traces-${{ github.run_id }}-${{ github.run_attempt }}-${{ steps.sanitize.outputs.artifact-suffix }}${{ matrix.next-version && format('-next{0}', matrix.next-version) || '' }}
path: test-results
path: integration/test-results
retention-days: 1

- name: Delete integration-test users
if: ${{ always() && steps.integration-tests.outcome != 'skipped' }}
timeout-minutes: 4
run: pnpm test:integration:cleanup
env:
CLERK_E2E_OAUTH_PROVIDER_CLIENT_SECRET: ${{ secrets.CLERK_E2E_OAUTH_PROVIDER_CLIENT_SECRET }}
CLERK_PLATFORM_API_KEY: ${{ secrets.CLERK_PLATFORM_API_KEY }}
INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }}
NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/integration/certs/rootCA.pem

pkg-pr-new:
name: Publish with pkg-pr-new
needs: [check-permissions, build-packages]
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/e2e-cleanups.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ jobs:
- name: Run cleanup
run: pnpm test:integration:cleanup
env:
CLERK_PLATFORM_API_KEY: ${{ secrets.CLERK_PLATFORM_API_KEY }}
INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }}
INTEGRATION_STAGING_INSTANCE_KEYS: ${{ secrets.INTEGRATION_STAGING_INSTANCE_KEYS }}
17 changes: 17 additions & 0 deletions .github/workflows/nightly-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ jobs:
pnpm init
pkglab add @clerk/ui

- name: Configure test run cleanup
run: echo "INTEGRATION_TEST_RUN_KEY=${RUN_ID}-${RUN_ATTEMPT}-nightly-${TEST_NAME}" >> "$GITHUB_ENV"
env:
RUN_ID: ${{ github.run_id }}
RUN_ATTEMPT: ${{ github.run_attempt }}
TEST_NAME: ${{ matrix.test-name }}

- name: Run Integration Tests
id: integration_tests
continue-on-error: true
Expand All @@ -71,6 +78,8 @@ jobs:
fi
echo "exit_code=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT
env:
CLERK_E2E_OAUTH_PROVIDER_CLIENT_SECRET: ${{ secrets.CLERK_E2E_OAUTH_PROVIDER_CLIENT_SECRET }}
CLERK_PLATFORM_API_KEY: ${{ secrets.CLERK_PLATFORM_API_KEY }}
E2E_APP_CLERK_JS_DIR: ${{runner.temp}}
E2E_APP_CLERK_UI_DIR: ${{runner.temp}}
# Don't set E2E_CLERK_JS_VERSION - let it use '*' (via linkPackage)
Expand Down Expand Up @@ -118,6 +127,14 @@ jobs:
test-results/
retention-days: 7

- name: Delete integration-test resources
if: ${{ always() && steps.integration_tests.outcome != 'skipped' }}
timeout-minutes: 4
run: pnpm test:integration:cleanup
env:
CLERK_PLATFORM_API_KEY: ${{ secrets.CLERK_PLATFORM_API_KEY }}
INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }}

- name: Report Status
if: always()
uses: ravsamhq/notify-slack-action@4ed28566c2bdcdaee6dca2b46b9666d01b4ed8a4 # v1
Expand Down
52 changes: 47 additions & 5 deletions integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,24 @@ If you need a fake user to login to the test site, use `createTestUtils`.

If you need to run a test suite inside a different environment (e.g. a different first factor or optional/new features) you can create a new [environment config](#environment-configs) inside [`presets/envs.ts`](../integration/presets/envs.ts).

#### Using the Platform API (recommended)

1. Create a new JavaScript file inside the `integration/configs` folder with a descriptive name such as `with-new-feature.js`.
1. Import the `defineConfig` helper, and provide a valid PLAPI configuration object. (You can export a complete configuration object with the Clerk CLI by running `clerk config pull`)

```js
import { defineConfig } from '../presets/platformApplication';
export default defineConfig({
config: {
/* ... */
},
});
```

#### Using a manually created instance

1. Create a new instance inside the **Integration testing** organization on Clerk
1. Add its secret and publishable key to the 1Password note with the name **JS SDKs integration tests**
1. Add its secret and publishable key to the 1Password note with the name **JS SDKs integration tests**. Also ensure that your new keys are added to the `INTEGRATION_INSTANCE_KEYS` environment variable inside the repository so that GitHub actions can successfully run. If you also have staging keys, add them to `INTEGRATION_STAGING_INSTANCE_KEYS`.
1. Add a new key to `.keys.json` (with a concise name) and add your keys to `sk` and `pk` respectively. Also add a placeholder to `.keys.json.sample`. For example:

```json
Expand All @@ -328,10 +344,12 @@ If you need to run a test suite inside a different environment (e.g. a different
}
```

#### Configuring the test suite

1. Inside `presets/envs.ts`, create a new environment config wrapped with `withInstanceKeys`:

```ts
const yourConciseName = withInstanceKeys('your-concise-name', base.clone().setId('yourConciseName'));
const yourConciseName = await withInstanceKeys('your-concise-name', base.clone().setId('yourConciseName'));
```

The `withInstanceKeys` wrapper sets PK/SK from the instance keys map and automatically swaps to staging keys when `E2E_STAGING=1` is set. The first argument is the production key name — the staging key is looked up as `clerkstage-your-concise-name`. See [Running tests against staging](#running-tests-against-staging) for more details.
Expand All @@ -345,8 +363,6 @@ If you need to run a test suite inside a different environment (e.g. a different
} as const;
```

1. Ensure that your new keys are added to the `INTEGRATION_INSTANCE_KEYS` environment variable inside the repository so that GitHub actions can successfully run. If you also have staging keys, add them to `INTEGRATION_STAGING_INSTANCE_KEYS`.

## Debugging tests

Sometimes tests are passing locally but not in CI 😢 But there are ways to dig into the root cause. Inside the PR with the failing tests apply these changes:
Expand Down Expand Up @@ -562,7 +578,7 @@ await app.withEnv(appConfigs.envs.withEmailCodes);
Inside [`presets/envs.ts`](../integration/presets/envs.ts) you can also create a completely new environment config. All new configs should be wrapped with `withInstanceKeys` to enable staging environment swapping:

```ts
const withCustomRoles = withInstanceKeys(
const withCustomRoles = await withInstanceKeys(
'with-custom-roles',
base
.clone()
Expand All @@ -572,6 +588,32 @@ const withCustomRoles = withInstanceKeys(
);
```

For non-staging tests, when `CLERK_PLATFORM_API_KEY` is set and `integration/configs/<key-name>.js` exists, `withInstanceKeys` creates a development application through the Platform API. It applies the exported configuration and uses the keys from the new application. If the file does not exist, the wrapper uses the existing instance keys map.

Use `defineConfig` for configuration files. JavaScript configuration files can read environment variables. The optional `setup` function receives a `ClerkClient` after the instance configuration is applied, along with a `patchConfig` method that can be used to make subsequent PATCH requests to the Platform API. Omit `setup` when no additional operations are required.

```js
import { defineConfig } from '../presets/platformApplication.js';

export default defineConfig({
config: {
auth_attack_protection: {
user_lockout: {
max_attempts: Number(process.env.CLERK_E2E_USER_LOCKOUT_MAX_ATTEMPTS || 10),
},
},
},
setup: async ({ clerkClient, patchConfig }) => {
await clerkClient.instance.update({ allowedOrigins: ['clerk://app'] });
await patchConfig({
organization_settings: {
domains_default_role: 'org:viewer',
},
});
},
});
```

When `E2E_STAGING=1`, this will automatically look up `clerkstage-with-custom-roles` from the staging keys and swap the PK, SK, and API URL. If the staging key doesn't exist, the config will not be staging-ready and any long running apps using it will be gracefully skipped.

Read [creating a new environment config](#creating-a-new-environment-config) and [running tests against staging](#running-tests-against-staging) to learn more.
Expand Down
53 changes: 48 additions & 5 deletions integration/cleanup/cleanup.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { parsePublishableKey } from '@clerk/shared/keys';
import { isStaging } from '@clerk/shared/utils';
import { test as setup } from '@playwright/test';

import { appConfigs } from '../presets/';
import { findE2ERunUsers, getE2ERunMarker } from '../testUtils/e2eRun';
import { constants } from '../constants';
import { instanceKeys } from '../presets/instanceKeys';
import { deleteApplication, listApplications } from '../presets/platformApplication';
import { findE2ERunUsers, getE2EApplicationRunMarker, getE2ERunMarker } from '../testUtils/e2eRun';
import { withRetry } from '../testUtils/retryableClerkClient';

setup('cleanup instances ', async () => {
const runMarker = getE2ERunMarker();
const entries = Array.from(appConfigs.secrets.instanceKeys.values())
const entries = Array.from(instanceKeys.values())
.map(({ pk, sk }) => {
const secretKey = sk;
if (!secretKey) {
Expand Down Expand Up @@ -193,13 +195,49 @@ setup('cleanup instances ', async () => {
cleanupSummary.push(instanceSummary);
}

const applicationCleanupErrors: string[] = [];
let applicationsDeleted = 0;
if (constants.CLERK_PLATFORM_API_KEY) {
try {
const applications = await listApplications(constants.CLERK_PLATFORM_API_KEY);
console.log(`Found ${applications.length} Platform API applications.`);

const applicationRunMarker = getE2EApplicationRunMarker(constants.INTEGRATION_TEST_RUN_KEY);
if (applicationRunMarker) {
const applicationNameSuffix = `-${applicationRunMarker}`;
const applicationsToDelete = applications.filter(application =>
application.name.endsWith(applicationNameSuffix),
);

for (const application of applicationsToDelete) {
try {
await deleteApplication(constants.CLERK_PLATFORM_API_KEY, application.application_id);
applicationsDeleted++;
console.log(`Deleted Platform API application ${application.application_id} (${application.name}).`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
applicationCleanupErrors.push(`${application.application_id}: ${message}`);
}
}
} else {
console.log('INTEGRATION_TEST_RUN_KEY is not set. Skipping Platform API application deletion.');
}
} catch (error) {
applicationCleanupErrors.push(error instanceof Error ? error.message : String(error));
}
} else {
console.log('CLERK_PLATFORM_API_KEY is not set. Skipping Platform API application cleanup.');
}

// Final summary
const totalUsersDeleted = cleanupSummary.reduce((sum, instance) => sum + instance.usersDeleted, 0);
const totalOrgsDeleted = cleanupSummary.reduce((sum, instance) => sum + instance.orgsDeleted, 0);
const errorInstances = cleanupSummary.filter(instance => instance.status === 'error').length;
const unauthorizedInstances = cleanupSummary.filter(instance => instance.status === 'unauthorized').length;

console.log(`\n📊 Summary: ${totalUsersDeleted} users, ${totalOrgsDeleted} orgs deleted`);
console.log(
`\n📊 Summary: ${totalUsersDeleted} users, ${totalOrgsDeleted} orgs, ${applicationsDeleted} applications deleted`,
);
if (errorInstances > 0 || unauthorizedInstances > 0) {
console.log(` ${errorInstances} errors, ${unauthorizedInstances} unauthorized`);
}
Expand All @@ -214,7 +252,12 @@ setup('cleanup instances ', async () => {
});
}

if (errorInstances === 0 && unauthorizedInstances === 0) {
if (applicationCleanupErrors.length > 0) {
console.log('\nPlatform application cleanup errors:');
applicationCleanupErrors.forEach(error => console.log(` - ${error}`));
}

if (errorInstances === 0 && unauthorizedInstances === 0 && applicationCleanupErrors.length === 0) {
console.log('\n✅ Cleanup completed successfully with no errors');
}
});
Expand Down
Loading
Loading