Skip to content

Commit 74ed5ce

Browse files
wolpertclaude
andcommitted
ci(examples): wire Playwright e2e suites into check and CI
The three demos' Playwright e2e suites (Chrome CDP virtual WebAuthn) were never run by Gradle or CI. Wire them in via a new `pkauth.e2e-conventions` plugin that registers an opt-in `e2eTest` task per demo: - Gated on PK_RUN_E2E=1 (or -PrunE2e); only then does `check` depend on it, so the default local `check` and the existing CI `build` job stay fast and Chrome-free. Resolved to a plain Boolean at configuration time so the wiring is configuration-cache-safe. - A shared BuildService lock (maxParallelUsages=1) serializes the suites across a single build, since every demo's webServer binds :8080 and org.gradle.parallel=true would otherwise let them collide. - Adds a CI `e2e` matrix job (one isolated runner per adapter, fail-fast false) that flips the flag and installs Chrome via PW_INSTALL_DEPS. Also fixes two pre-existing defects the suites surfaced once they actually ran: - playwright.config.ts (all 3): the webServer ran `../../../gradlew` from the e2e/ dir, so Gradle took e2e/ as the project dir and failed. Pass `-p ../../..` to root Gradle at the repo. - micronaut-demo: dev-mode was set only in application.yml, which (like the relying-party keys) does not reach bean conditions reliably — so the @requires(pkauth.dev-mode=true) LoggingEmailSender/LoggingSmsSender beans never activated and the unconditional magicLinkService/otpService singletons failed to inject EmailSender/SmsSender (500 at request time). Pass -Dpkauth.dev-mode=true as a system property in the run task. All three suites now pass 5/5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6ccbc4d commit 74ed5ce

9 files changed

Lines changed: 127 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,53 @@ jobs:
130130
path: clients/passkeys-browser/reports/mutation/
131131
if-no-files-found: ignore
132132

133+
# Playwright end-to-end suites for the three example demos. Each demo carries a sibling `e2e/`
134+
# Playwright project that drives the full passkey ceremony through Chrome's CDP virtual WebAuthn
135+
# authenticator. These are wired into Gradle `check` as an OPT-IN task (skipped by default so the
136+
# `build` job above stays fast and Chrome-free) — here we flip the switch with PK_RUN_E2E=1 and
137+
# let the `e2eTest` task provision Chrome (PW_INSTALL_DEPS=1 → playwright install --with-deps) and
138+
# boot the demo via its Gradle `run` task. One job per adapter (all three bind :8080).
139+
e2e:
140+
runs-on: ubuntu-latest
141+
strategy:
142+
fail-fast: false
143+
matrix:
144+
demo: [ spring-boot-demo, dropwizard-demo, micronaut-demo ]
145+
steps:
146+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
147+
148+
- name: Set up JDK 21
149+
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
150+
with:
151+
java-version: '21'
152+
distribution: 'temurin'
153+
154+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
155+
with:
156+
node-version: '20'
157+
158+
# The e2eTest task executes the wrapper jar (directly and again nested, when Playwright boots
159+
# the demo via the Gradle `run` task) — validate it for the same reason the build job does.
160+
- name: Validate Gradle wrapper
161+
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
162+
163+
- name: Set up Gradle
164+
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
165+
166+
- name: Run ${{ matrix.demo }} e2e
167+
run: ./gradlew :examples:${{ matrix.demo }}:e2eTest --stacktrace
168+
env:
169+
PK_RUN_E2E: '1' # flip the opt-in e2eTest task on
170+
PW_INSTALL_DEPS: '1' # playwright install --with-deps chrome (needs the runner's apt deps)
171+
172+
- name: Upload Playwright traces
173+
if: always()
174+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
175+
with:
176+
name: playwright-traces-${{ matrix.demo }}
177+
path: examples/${{ matrix.demo }}/e2e/test-results/
178+
if-no-files-found: ignore
179+
133180
# Block PRs that introduce dependencies with known vulnerabilities or incompatible
134181
# licenses, before they merge. Uses the GitHub dependency graph; PR-only.
135182
dependency-review:

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ JDK 21 is required (Gradle's toolchain fetches one if absent). Node ≥ 20 + npm
2323
```
2424

2525
- **Integration tests are ordinary `test` tasks** — the JDBI/DynamoDB persistence modules use Testcontainers (Postgres / DynamoDB Local), so `./gradlew :pk-auth-persistence-jdbi:test` needs **Docker** running. There is no separate `integrationTest` task or JUnit tag.
26-
- Playwright end-to-end suites live under the `examples/` demos and need **Chrome** (drives a CDP virtual WebAuthn authenticator).
26+
- Playwright end-to-end suites live under the `examples/` demos and need **Chrome** (drives a CDP virtual WebAuthn authenticator). They are wired into each demo's `check` via the `e2eTest` task (`pkauth.e2e-conventions`) but are **opt-in** — they only run when `PK_RUN_E2E=1` (or `-PrunE2e`) is set, so a default `./gradlew check` and the CI `build` job stay fast and Chrome-free. Run one with `PK_RUN_E2E=1 ./gradlew :examples:spring-boot-demo:e2eTest`; the task itself does `npm ci` + `npx playwright install chrome` and boots the demo via its Gradle `run` task. CI runs all three in the `e2e` matrix job (`.github/workflows/ci.yml`, with `PW_INSTALL_DEPS=1` so Chrome's OS deps are installed too).
2727
- The Gradle **build cache is disabled** on purpose (Spotless 8.x classloader bug) — see the comment in `gradle.properties`. Don't re-enable it.
2828

2929
## Architecture: three concentric rings
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// E2E (Playwright) conventions for the example demos. Each demo carries a sibling `e2e/`
2+
// Playwright project that drives the full passkey ceremony through Chrome's CDP virtual WebAuthn
3+
// authenticator (see examples/<demo>/e2e/). This wires that suite into `check` as an OPT-IN task:
4+
// it runs only when PK_RUN_E2E=1 (or -PrunE2e) is set, so the default local `check` and the
5+
// existing CI `build` job stay fast and Chrome-free. The dedicated CI e2e jobs
6+
// (.github/workflows/ci.yml) set the flag; the task itself provisions the Chrome channel via
7+
// `npx playwright install` (adding --with-deps when PW_INSTALL_DEPS is set, as CI does).
8+
plugins {
9+
java
10+
}
11+
12+
// Every demo's webServer binds the same port (:8080), so two e2eTest tasks must never run at once.
13+
// With org.gradle.parallel=true a root-level `PK_RUN_E2E=1 ./gradlew check` would otherwise run the
14+
// sibling demos' suites concurrently and collide on the port. A shared build service with
15+
// maxParallelUsages=1 serializes them across the whole build (registerIfAbsent dedupes by name, so
16+
// all three demos share one lock). In CI each demo runs on its own isolated runner, so this lock is
17+
// a no-op there — it only matters for single-machine multi-demo runs.
18+
abstract class E2eServerLock : BuildService<BuildServiceParameters.None>
19+
20+
val e2eServerLock = gradle.sharedServices.registerIfAbsent(
21+
"pkauthE2eServerLock",
22+
E2eServerLock::class,
23+
) {
24+
maxParallelUsages.set(1)
25+
}
26+
27+
// Opt-in switch: PK_RUN_E2E=1 in the environment, or -PrunE2e on the command line. An explicitly
28+
// set PK_RUN_E2E (even to a non-"1" value) wins over the property, so `PK_RUN_E2E=0` forces off.
29+
// Resolved to a plain Boolean at configuration time (the env var / property become configuration
30+
// cache inputs) so the wiring below captures no script reference — an `onlyIf { provider.get() }`
31+
// closure would not be configuration-cache-serializable.
32+
val runE2e: Boolean = providers.environmentVariable("PK_RUN_E2E").orNull
33+
?.let { it == "1" }
34+
?: providers.gradleProperty("runE2e").isPresent
35+
36+
val e2eDir = layout.projectDirectory.dir("e2e")
37+
38+
val e2eTest = tasks.register<Exec>("e2eTest") {
39+
group = "verification"
40+
description = "Runs the Playwright end-to-end suite for this demo (opt-in: set PK_RUN_E2E=1)."
41+
usesService(e2eServerLock) // serialize across demos — they share port :8080
42+
workingDir = e2eDir.asFile
43+
// `npm ci` pins deps to the committed lockfile. `playwright install` provisions the Chrome
44+
// channel the config pins (CDP virtual WebAuthn needs Google Chrome); --with-deps is added in
45+
// CI (PW_INSTALL_DEPS) to also install the OS libraries. The webServer in playwright.config.ts
46+
// boots this demo via the Gradle `run` task, so no separately-started server is needed.
47+
commandLine(
48+
"sh", "-c",
49+
"npm ci --no-audit --no-fund && " +
50+
"npx playwright install ${'$'}{PW_INSTALL_DEPS:+--with-deps} chrome && " +
51+
"npx playwright test",
52+
)
53+
}
54+
55+
// Wire into `check` only when opted in, so a default `check` (local or the CI `build` job) neither
56+
// depends on nor runs the suite. `./gradlew :examples:<demo>:e2eTest` still runs it on demand.
57+
if (runE2e) {
58+
tasks.named("check") {
59+
dependsOn(e2eTest)
60+
}
61+
}

examples/dropwizard-demo/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins {
22
id("pkauth.java-conventions")
33
id("pkauth.test-conventions")
4+
id("pkauth.e2e-conventions")
45
application
56
}
67

examples/dropwizard-demo/e2e/playwright.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ export default defineConfig({
2727
: {
2828
// Dropwizard's run task starts with `server` arg; the persistence flavor is read from
2929
// the `pkauth.persistence` system property (see DemoApplication#persistenceFlavor).
30-
command: `../../../gradlew :examples:dropwizard-demo:run -Dpkauth.persistence=${PERSISTENCE}`,
30+
// `-p ../../..` roots Gradle at the repo (the webServer runs from this config's dir, so a
31+
// bare `../../../gradlew` would otherwise take the e2e/ dir as the project dir and fail).
32+
command: `../../../gradlew -p ../../.. :examples:dropwizard-demo:run -Dpkauth.persistence=${PERSISTENCE}`,
3133
url: BASE_URL,
3234
reuseExistingServer: !process.env.CI,
3335
timeout: 180_000,

examples/micronaut-demo/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins {
22
application
33
id("pkauth.java-conventions")
4+
id("pkauth.e2e-conventions")
45
}
56

67
description = "pk-auth Micronaut demo app."
@@ -62,6 +63,12 @@ tasks.named<JavaExec>("run") {
6263
systemProperty("pkauth.jwt.issuer", "pk-auth-micronaut-demo")
6364
systemProperty("pkauth.jwt.audience", "pk-auth-micronaut-demo-clients")
6465
systemProperty("pkauth.jwt.secret", "change-me-in-prod-use-a-32-byte-secret!!")
66+
// dev-mode is also set in application.yml, but — like the relying-party keys above — it must be
67+
// passed as a system property to reliably reach the bean conditions: the LoggingEmailSender /
68+
// LoggingSmsSender beans in PkAuthFactory are @Requires(property = "pkauth.dev-mode", value =
69+
// "true"), and without them the unconditional magicLinkService / otpService singletons fail to
70+
// inject EmailSender / SmsSender at request time (a 500 the e2e suite exercises).
71+
systemProperty("pkauth.dev-mode", "true")
6572
}
6673

6774
tasks.named<Test>("test") {

examples/micronaut-demo/e2e/playwright.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ export default defineConfig({
2525
webServer: externallyManaged
2626
? undefined
2727
: {
28-
command: `../../../gradlew :examples:micronaut-demo:run -Dpkauth.persistence=${PERSISTENCE}`,
28+
// `-p ../../..` roots Gradle at the repo (the webServer runs from this config's dir, so a
29+
// bare `../../../gradlew` would otherwise take the e2e/ dir as the project dir and fail).
30+
command: `../../../gradlew -p ../../.. :examples:micronaut-demo:run -Dpkauth.persistence=${PERSISTENCE}`,
2931
url: BASE_URL,
3032
reuseExistingServer: !process.env.CI,
3133
timeout: 180_000,

examples/spring-boot-demo/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins {
22
id("pkauth.java-conventions")
33
id("pkauth.test-conventions")
4+
id("pkauth.e2e-conventions")
45
application
56
}
67

examples/spring-boot-demo/e2e/playwright.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ export default defineConfig({
2929
webServer: externallyManaged
3030
? undefined
3131
: {
32-
command: `../../../gradlew :examples:spring-boot-demo:run --args='--demo.persistence=${PERSISTENCE}'`,
32+
// `-p ../../..` roots Gradle at the repo (the webServer runs from this config's dir, so a
33+
// bare `../../../gradlew` would otherwise take the e2e/ dir as the project dir and fail).
34+
command: `../../../gradlew -p ../../.. :examples:spring-boot-demo:run --args='--demo.persistence=${PERSISTENCE}'`,
3335
url: BASE_URL,
3436
reuseExistingServer: !process.env.CI,
3537
timeout: 180_000,

0 commit comments

Comments
 (0)