From 021a9ae446857063d2c92fc91e1099f5fe9f0e4e Mon Sep 17 00:00:00 2001 From: James Maes Date: Thu, 5 Nov 2026 22:02:07 +0000 Subject: [PATCH 1/7] ci: use shared ci-workflows Maven workflow Replace the platform-ci release callers with one caller of KofTwentyTwo/ci-workflows maven.yaml (qqq-orb port). Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/build.yaml | 7 +++ .gitea/workflows/qrun-release.yaml | 25 ----------- .gitea/workflows/release-rc.yaml | 72 ------------------------------ 3 files changed, 7 insertions(+), 97 deletions(-) create mode 100644 .gitea/workflows/build.yaml delete mode 100644 .gitea/workflows/qrun-release.yaml delete mode 100644 .gitea/workflows/release-rc.yaml diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..ed42608 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,7 @@ +name: build +on: + push: +jobs: + maven: + uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@main + secrets: inherit diff --git a/.gitea/workflows/qrun-release.yaml b/.gitea/workflows/qrun-release.yaml deleted file mode 100644 index 5ebc530..0000000 --- a/.gitea/workflows/qrun-release.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Standard QRun release caller (v2, thin contract): every push and manual dispatch delegates to the protected qrun-release-v1 channel. Do not edit per repository. -name: qrun-release -on: - push: {} - workflow_dispatch: {} -permissions: - contents: read - actions: read -jobs: - release: - uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/java-docker.yaml@qrun-release-v1 - with: - runner: linux-small - docker-runner: container-build - browsers: true - browser-runner: browser-build - postpublish: - if: ${{ needs.release.outputs.capture == 'true' }} - needs: release - permissions: - contents: write - actions: write - uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-source-version.yaml@qrun-release-v1 - with: - runner: linux-small diff --git a/.gitea/workflows/release-rc.yaml b/.gitea/workflows/release-rc.yaml deleted file mode 100644 index 12ec9fc..0000000 --- a/.gitea/workflows/release-rc.yaml +++ /dev/null @@ -1,72 +0,0 @@ -name: Release candidate -'on': - workflow_dispatch: - inputs: - dry_run: - description: Plan a cut without changing release state - type: boolean - default: false - finalize: - description: Retry finalization of an existing cut; do not cut another RC - type: boolean - default: false - recover: - description: Recover the same published RC after a deployment timeout; never - reupload - type: boolean - default: false - circle_build: - description: Original release run id (recovery only) - type: string - default: '' - merge_sha: - description: Existing cut merge_sha (finalize retry only) - type: string - default: '' - bump_sha: - description: Existing cut bump_sha (finalize retry only) - type: string - default: '' - branch: - description: Existing cut branch (finalize retry only) - type: string - default: '' - version: - description: Existing cut version (finalize retry only) - type: string - default: '' - pull_request: - types: - - closed - branches: - - develop -permissions: - contents: read -jobs: - release-rc: - # The old qbit-rc-published repository_dispatch is replaced by the already-proven workflow_dispatch - # completion: the publisher's own ActionsWRITE job dispatches this workflow with finalize=true plus - # merge_sha/bump_sha/branch/version (same inputs as the manual finalize retry). - permissions: - contents: write - actions: read - pull-requests: write - if: github.event_name == 'workflow_dispatch' - || (github.event_name == 'pull_request' && github.event.pull_request.merged - == true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name, - 'release:rc')) - uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qbit-release-rc.yaml@qrun-release-v1 - with: - dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false - }} - finalize: ${{ github.event_name == 'workflow_dispatch' && inputs.finalize }} - recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false - }} - circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build - || '' }} - merge_sha: ${{ inputs.merge_sha || '' }} - bump_sha: ${{ inputs.bump_sha || '' }} - branch: ${{ inputs.branch || '' }} - version: ${{ inputs.version || '' }} - secrets: - QRUN_TAG_SIGNING_KEY: ${{ secrets.QRUN_TAG_SIGNING_KEY }} -- 2.54.0 From 8f91c08e3e5b965f5a115f5da69299f2b8daacd7 Mon Sep 17 00:00:00 2001 From: CI Date: Thu, 5 Nov 2026 16:15:15 -0600 Subject: [PATCH 2/7] ci: rerun shared workflow -- 2.54.0 From c9806e2b829b3769f6613977a86924f2aa602f3e Mon Sep 17 00:00:00 2001 From: CI Date: Thu, 5 Nov 2026 16:23:54 -0600 Subject: [PATCH 3/7] ci: rerun shared workflow -- 2.54.0 From ba5a688525d9cd134b119e02e682cc8f4fb37720 Mon Sep 17 00:00:00 2001 From: James Maes Date: Thu, 5 Nov 2026 16:52:46 -0600 Subject: [PATCH 4/7] test(authentik): make contract ITs run against a non-loopback Docker host - SchemaDriftIT/SchemaDriftTest: quote OpenAPI path templates before turning {key} into a regex; other placeholders such as {slug} were compiled as regex repetition and threw PatternSyntaxException. - Contract ITs: set allowInsecureHttp on the test qbit config; the Authentik test container is plain http and is reached via the Docker host address (e.g. 172.17.0.1 in CI), not loopback. Production https validation is unchanged. - Postgres ITs: bind the mapped port to 127.0.0.1 only when the Docker daemon is local; with a remote daemon the loopback-only binding made the database unreachable (connection refused). Co-Authored-By: Claude Opus 5.5 --- .../com/kingsrook/qbits/authentik/SchemaDriftTest.java | 2 +- .../contract/InvitationEnrollmentContractIT.java | 4 ++-- .../qbits/authentik/contract/Phase1ReadContractIT.java | 2 +- .../qbits/authentik/contract/QbitReadContractIT.java | 2 +- .../qbits/authentik/contract/QbitWriteContractIT.java | 2 +- .../qbits/authentik/contract/SchemaDriftIT.java | 2 +- .../qbits/authentik/contract/TokenContractIT.java | 2 +- .../qbits/authentik/sync/AuthentikMirrorPostgresIT.java | 9 ++++++++- .../authentik/sync/AuthentikWebhookInboxPostgresIT.java | 9 ++++++++- 9 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/test/java/com/kingsrook/qbits/authentik/SchemaDriftTest.java b/src/test/java/com/kingsrook/qbits/authentik/SchemaDriftTest.java index 016c3ef..383f1d6 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/SchemaDriftTest.java +++ b/src/test/java/com/kingsrook/qbits/authentik/SchemaDriftTest.java @@ -161,7 +161,7 @@ public class SchemaDriftTest { return (surfacePath); } - String regex = surfacePath.replace("{key}", "\\{[^/]+\\}"); + String regex = java.util.regex.Pattern.quote(surfacePath).replace("{key}", "\\E\\{[^/]+\\}\\Q"); Iterator> fields = paths.fields(); while(fields.hasNext()) { diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/InvitationEnrollmentContractIT.java b/src/test/java/com/kingsrook/qbits/authentik/contract/InvitationEnrollmentContractIT.java index 5035cc6..51be1d8 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/InvitationEnrollmentContractIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/InvitationEnrollmentContractIT.java @@ -174,7 +174,7 @@ class InvitationEnrollmentContractIT } assertThat(used).isNotNull(); assertThat(used.getJSONObject("user").optInt("pk", -1)).isNotEqualTo(userPk); - var config = assignment == null ? new com.kingsrook.qbits.authentik.AuthentikQBitConfig().withBaseUrl(stack.baseUrl()) + var config = assignment == null ? new com.kingsrook.qbits.authentik.AuthentikQBitConfig().withBaseUrl(stack.baseUrl()).withAllowInsecureHttp(true) .withApiToken(stack.serviceAccountToken()).withAppKey("kof22-admin").withListCacheTtlSeconds(0) : assignment.config; var instance = assignment == null ? com.kingsrook.qbits.authentik.AuthentikTestSupport.produce(config) : assignment.instance; com.kingsrook.qqq.backend.core.context.QContext.init(instance, @@ -265,7 +265,7 @@ class InvitationEnrollmentContractIT String actor = create(admin, "/core/users/", new JSONObject().put("username", owner + "-operator").put("name", "Invitation operator") .put("email", owner + "-operator@example.test").put("type", "internal").put("path", "users/invitation-contract"), cleanup); assertThat(admin.post("/core/groups/" + admins + "/add_user/", new JSONObject().put("pk", Integer.parseInt(actor))).status()).isEqualTo(204); - config = new AuthentikQBitConfig().withBaseUrl(stack.baseUrl()).withApiToken(stack.serviceAccountToken()).withAppKey("kof22-admin") + config = new AuthentikQBitConfig().withBaseUrl(stack.baseUrl()).withAllowInsecureHttp(true).withApiToken(stack.serviceAccountToken()).withAppKey("kof22-admin") .withListCacheTtlSeconds(0).withGroupCacheTtlSeconds(0).withInvitationFlow(owner).withLocalBackendName("memory") .withRecoveryEmailStage("kof22-recovery-email") .withFeatures(EnumSet.of(AuthentikFeature.READ, AuthentikFeature.USERS, AuthentikFeature.GROUPS, AuthentikFeature.MEMBERSHIP)); diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/Phase1ReadContractIT.java b/src/test/java/com/kingsrook/qbits/authentik/contract/Phase1ReadContractIT.java index ffae214..6095ddd 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/Phase1ReadContractIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/Phase1ReadContractIT.java @@ -96,7 +96,7 @@ class Phase1ReadContractIT void beforeEach() throws QException { config = new AuthentikQBitConfig() - .withBaseUrl(stack.baseUrl()) + .withBaseUrl(stack.baseUrl()).withAllowInsecureHttp(true) .withApiToken(stack.serviceAccountToken()) .withAppKey("kof22-admin") .withApplicationsInScope(List.of(AuthentikTestStack.APP_SLUG)) diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/QbitReadContractIT.java b/src/test/java/com/kingsrook/qbits/authentik/contract/QbitReadContractIT.java index 0b4b564..2f663a0 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/QbitReadContractIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/QbitReadContractIT.java @@ -126,7 +126,7 @@ class QbitReadContractIT void beforeEach() throws QException { config = new AuthentikQBitConfig() - .withBaseUrl(stack.baseUrl()) + .withBaseUrl(stack.baseUrl()).withAllowInsecureHttp(true) .withApiToken(stack.serviceAccountToken()) .withAppKey("kof22-admin") .withListCacheTtlSeconds(0); diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/QbitWriteContractIT.java b/src/test/java/com/kingsrook/qbits/authentik/contract/QbitWriteContractIT.java index e0006a2..08bd6be 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/QbitWriteContractIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/QbitWriteContractIT.java @@ -155,7 +155,7 @@ class QbitWriteContractIT { MemoryRecordStore.getInstance().reset(); config = new AuthentikQBitConfig() - .withBaseUrl(stack.baseUrl()) + .withBaseUrl(stack.baseUrl()).withAllowInsecureHttp(true) .withApiToken(stack.serviceAccountToken()) .withAppKey("kof22-admin") .withListCacheTtlSeconds(0) diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/SchemaDriftIT.java b/src/test/java/com/kingsrook/qbits/authentik/contract/SchemaDriftIT.java index 380511b..ed80929 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/SchemaDriftIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/SchemaDriftIT.java @@ -89,7 +89,7 @@ class SchemaDriftIT for(java.util.Iterator it = paths.fieldNames(); it.hasNext(); ) { String candidate = it.next(); - if(candidate.equals(path) || candidate.matches(path.replace("{key}", "\\{[^/]+\\}"))) + if(candidate.equals(path) || candidate.matches(java.util.regex.Pattern.quote(path).replace("{key}", "\\E\\{[^/]+\\}\\Q"))) { return (SchemaDriftTest.parameterNames(schema, paths.get(candidate).get(operation.method()))); } diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/TokenContractIT.java b/src/test/java/com/kingsrook/qbits/authentik/contract/TokenContractIT.java index d01ad3a..89377ee 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/TokenContractIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/TokenContractIT.java @@ -579,7 +579,7 @@ class TokenContractIT private void configureQbit(int owner, String subject) throws Exception { MemoryRecordStore.getInstance().reset(); - config = new AuthentikQBitConfig().withBaseUrl(stack.baseUrl()).withApiToken(stack.serviceAccountToken()).withAppKey("kof22-admin") + config = new AuthentikQBitConfig().withBaseUrl(stack.baseUrl()).withAllowInsecureHttp(true).withApiToken(stack.serviceAccountToken()).withAppKey("kof22-admin") .withListCacheTtlSeconds(0).withGroupCacheTtlSeconds(0).withBulkPacingMillis(0).withLocalBackendName("memory") .withRecoveryEmailStage("kof22-recovery-email") .withFeatures(EnumSet.of(AuthentikFeature.READ, AuthentikFeature.USERS, AuthentikFeature.TOKENS)) diff --git a/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikMirrorPostgresIT.java b/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikMirrorPostgresIT.java index ca05fbf..ca90f32 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikMirrorPostgresIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikMirrorPostgresIT.java @@ -16,6 +16,7 @@ package com.kingsrook.qbits.authentik.sync; +import java.util.Set; import java.util.UUID; import javax.sql.DataSource; import com.kingsrook.qbits.authentik.RequiresDockerCondition; @@ -27,6 +28,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.ExtendWith; import org.postgresql.ds.PGSimpleDataSource; +import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.PostgreSQLContainer; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.PortBinding; @@ -39,6 +41,7 @@ import com.github.dockerjava.api.model.Ports; @ExtendWith(RequiresDockerCondition.class) class AuthentikMirrorPostgresIT extends AuthentikMirrorSqlTest { + private static final Set LOCAL_DOCKER_HOSTS = Set.of("localhost", "127.0.0.1"); private static PostgreSQLContainer postgres; private String schema; @@ -55,7 +58,11 @@ class AuthentikMirrorPostgresIT extends AuthentikMirrorSqlTest .withCreateContainerCmdModifier(command -> { command.withName(owner); - command.getHostConfig().withPortBindings(new PortBinding(Ports.Binding.bindIp("127.0.0.1"), new ExposedPort(5432))); + // Loopback-only only when the daemon is local; a remote daemon (CI) is reached via its host address. + if(LOCAL_DOCKER_HOSTS.contains(DockerClientFactory.instance().dockerHostIpAddress())) + { + command.getHostConfig().withPortBindings(new PortBinding(Ports.Binding.bindIp("127.0.0.1"), new ExposedPort(5432))); + } }); try { diff --git a/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikWebhookInboxPostgresIT.java b/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikWebhookInboxPostgresIT.java index dec6cc9..8de82cc 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikWebhookInboxPostgresIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikWebhookInboxPostgresIT.java @@ -17,6 +17,7 @@ package com.kingsrook.qbits.authentik.sync; import java.sql.SQLException; +import java.util.Set; import java.util.UUID; import javax.sql.DataSource; import com.kingsrook.qbits.authentik.RequiresDockerCondition; @@ -24,6 +25,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.ExtendWith; import org.postgresql.ds.PGSimpleDataSource; +import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.PostgreSQLContainer; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.PortBinding; @@ -36,6 +38,7 @@ import com.github.dockerjava.api.model.Ports; @ExtendWith(RequiresDockerCondition.class) class AuthentikWebhookInboxPostgresIT extends AuthentikWebhookInboxTest { + private static final Set LOCAL_DOCKER_HOSTS = Set.of("localhost", "127.0.0.1"); private static PostgreSQLContainer postgres; private String schema; @@ -53,7 +56,11 @@ class AuthentikWebhookInboxPostgresIT extends AuthentikWebhookInboxTest .withCreateContainerCmdModifier(command -> { command.withName(owner); - command.getHostConfig().withPortBindings(new PortBinding(Ports.Binding.bindIp("127.0.0.1"), new ExposedPort(5432))); + // Loopback-only only when the daemon is local; a remote daemon (CI) is reached via its host address. + if(LOCAL_DOCKER_HOSTS.contains(DockerClientFactory.instance().dockerHostIpAddress())) + { + command.getHostConfig().withPortBindings(new PortBinding(Ports.Binding.bindIp("127.0.0.1"), new ExposedPort(5432))); + } }); try { -- 2.54.0 From 8b52f583e9f3c7364093f9073b9393bba8d7db29 Mon Sep 17 00:00:00 2001 From: James Maes Date: Thu, 5 Nov 2026 17:11:53 -0600 Subject: [PATCH 5/7] test(authentik): re-apply errored kof22 blueprints during contract stack startup On a cold runner the built-in Authentik blueprints can race our custom ones on first boot, leaving kof22-oauth2-apps in "error" until the 8-minute wait times out (run 5465). The local-review path already re-applied errored blueprints; do the same (throttled to every 10s) in the shared contract-stack wait. Co-Authored-By: Claude Opus 5.5 --- .../contract/AuthentikTestStack.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java b/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java index ea0028c..4160ad0 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java @@ -255,9 +255,35 @@ public final class AuthentikTestStack TestApi admin = admin(); try { + long[] nextRetry = { 0 }; waitFor(Duration.ofMinutes(8), "all three kof22 blueprints applied", () -> - isSuccessful(admin, INFRA_BLUEPRINT) && isSuccessful(admin, BOOTSTRAP_BLUEPRINT) && isSuccessful(admin, TOKEN_BLUEPRINT) + { + //////////////////////////////////////////////////////////////////// + // built-in blueprints may race our custom dependencies on first // + // boot, leaving one of ours in "error"; re-apply it (throttled). // + //////////////////////////////////////////////////////////////////// + if(System.currentTimeMillis() >= nextRetry[0]) + { + nextRetry[0] = System.currentTimeMillis() + 10_000; + for(String name : List.of(INFRA_BLUEPRINT, BOOTSTRAP_BLUEPRINT, TOKEN_BLUEPRINT)) + { + try + { + JSONObject current = blueprint(name); + if(current != null && "error".equals(current.optString("status"))) + { + admin.post("/managed/blueprints/" + current.getString("pk") + "/apply/", new JSONObject()); + } + } + catch(Exception e) + { + // Bootstrap authentication and built-in dependencies settle after HTTP readiness. + } + } + } + return (isSuccessful(admin, INFRA_BLUEPRINT) && isSuccessful(admin, BOOTSTRAP_BLUEPRINT) && isSuccessful(admin, TOKEN_BLUEPRINT) && admin.first("/core/users/?username=" + SERVICE_ACCOUNT) != null); + }); } catch(IllegalStateException e) { -- 2.54.0 From 38660be0f56afd1ab074b1b33e820a613fc6f47a Mon Sep 17 00:00:00 2001 From: James Maes Date: Thu, 5 Nov 2026 17:30:04 -0600 Subject: [PATCH 6/7] test(authentik): also re-apply never-applied kof22 blueprints at startup Run 5488: with errored blueprints re-applied, kof22-oauth2-apps and kof22-qqq-tokens succeeded but kof22-identity-bootstrap stayed "unknown" (never applied) until the wait timed out. Re-apply blueprints in "error" or "unknown" state after a 60s grace period, every 30s. Co-Authored-By: Claude Opus 5.5 --- .../qbits/authentik/contract/AuthentikTestStack.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java b/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java index 4160ad0..fc249ee 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java @@ -255,22 +255,23 @@ public final class AuthentikTestStack TestApi admin = admin(); try { - long[] nextRetry = { 0 }; + long[] nextRetry = { System.currentTimeMillis() + 60_000 }; waitFor(Duration.ofMinutes(8), "all three kof22 blueprints applied", () -> { //////////////////////////////////////////////////////////////////// // built-in blueprints may race our custom dependencies on first // - // boot, leaving one of ours in "error"; re-apply it (throttled). // + // boot, leaving one of ours in "error" or never applied // + // ("unknown"); re-apply it (throttled). // //////////////////////////////////////////////////////////////////// if(System.currentTimeMillis() >= nextRetry[0]) { - nextRetry[0] = System.currentTimeMillis() + 10_000; + nextRetry[0] = System.currentTimeMillis() + 30_000; for(String name : List.of(INFRA_BLUEPRINT, BOOTSTRAP_BLUEPRINT, TOKEN_BLUEPRINT)) { try { JSONObject current = blueprint(name); - if(current != null && "error".equals(current.optString("status"))) + if(current != null && List.of("error", "unknown").contains(current.optString("status"))) { admin.post("/managed/blueprints/" + current.getString("pk") + "/apply/", new JSONObject()); } -- 2.54.0 From ea5c5ab65e66845d47c492e3e720626a00d6bee0 Mon Sep 17 00:00:00 2001 From: James Maes Date: Thu, 5 Nov 2026 18:28:50 -0600 Subject: [PATCH 7/7] test(authentik): surface Authentik error logs when the contract stack fails to start When the blueprint wait times out, also print error-level worker lines and error/warning server lines (API-triggered re-applies run in the server), so the failing blueprint's cause is visible in CI logs. Co-Authored-By: Claude Opus 5.5 --- .../qbits/authentik/contract/AuthentikTestStack.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java b/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java index fc249ee..4ea1659 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java @@ -305,8 +305,16 @@ public final class AuthentikTestStack } try { - worker.getLogs().lines().filter(line -> line.contains("blueprint") && (line.contains("error") || line.contains("warning") || line.contains("fail"))) + worker.getLogs().lines().filter(line -> (line.contains("blueprint") && (line.contains("error") || line.contains("warning") || line.contains("fail"))) + || line.contains("\"level\": \"error\"") || line.contains("Traceback")) + .filter(line -> !line.contains("clear_failed_blueprints")) .limit(60).forEach(line -> System.err.println("worker: " + line)); + //////////////////////////////////////////////////////////////// + // blueprints re-applied through the API run in the server // + //////////////////////////////////////////////////////////////// + server.getLogs().lines().filter(line -> line.contains("\"level\": \"error\"") || line.contains("\"level\": \"warning\"") || line.contains("Traceback")) + .filter(line -> !line.contains("Base URL is not configured")) + .limit(60).forEach(line -> System.err.println("server: " + line)); } catch(RuntimeException unavailable) { -- 2.54.0