Compare commits

..
Author SHA1 Message Date
james.maes 191b165593 Merge pull request 'build(deps): align with Gitea registry (parent 2.2.1, QQQ 4.1.0-RC.26)' (#28) from deps/align-gitea into develop
build / maven (push) Successful in 3m10s
maven / build (push) Successful in 3m10s
2026-11-07 18:03:41 +00:00
james.maesandClaude Opus 5.5 9d7214912a build(deps): align with Gitea registry (parent 2.2.1, QQQ 4.1.0-RC.26)
build / maven (push) Successful in 3m5s
maven / build (push) Successful in 3m6s
pom.xml: parent -> 2.2.1
pom.xml: qqq.snapshot.version: 4.1.0-RC.21 -> 4.1.0-RC.26
pom.xml: qqq-frontend-next: 1.0.0-RC.14 -> 1.0.0-RC.38
pom.xml: added qqq.version=4.1.0-RC.26 (parent BOM import)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 17:58:48 +00:00
james.maes 716cccc30d Merge pull request 'ci: pin shared workflow to v1' (#27) from ci/pin-v1-develop into develop
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 13s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Successful in 12s
build / maven (push) Successful in 3m55s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (push) Successful in 15s
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 3m55s
2026-11-06 16:04:41 +00:00
james.maesandClaude Opus 5.5 c42195cd61 ci: pin shared workflow to v1
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Waiting to run
build / maven (push) Successful in 3m21s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 7s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
maven / build (push) Successful in 3m21s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:06:12 +00:00
james.maes 6a1edfae3c Merge pull request 'ci: use shared ci-workflows Maven workflow' (#26) from ci/ci-workflows into develop
build / maven (push) Successful in 3m4s
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Successful in 5s
maven / build (push) Successful in 3m5s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow d20a578c7d818bc7ca32a2ac2c675da4e4f27385] (push) Successful in 9s
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
2026-11-05 23:15:16 +00:00
james.maesandClaude Opus 5.5 ad2edb6769 ci: use shared ci-workflows Maven workflow
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1b3648d9421543350067e95e54959dfb471b26d5] (pull_request) Successful in 4s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
build / maven (push) Successful in 1m58s
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 2m4s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Failing after 1s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Failing after 1s
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Failing after 17m12s
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 2s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
maven / build (push) Successful in 1m58s
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 <noreply@anthropic.com>
2026-11-05 22:03:09 +00:00
james.maes 629cfb287f Merge canonical CMS CORE and PostgreSQL runtime integration (#25)
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 10s
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
Shared Java Maven CI / secrets (push) Successful in 47s
Shared Java Maven CI / verify (push) Successful in 2m42s
QRun-IO/ci-policy: Shared Java Maven CI / verify (push) Successful in 1m28s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 14s
qrun-release / postpublish (push) Successful in 7m26s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 60e5a80ed7a5a67877bfb3e129b1929c83464b6e] (push) Successful in 10s
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (push) Successful in 43s
Shared Java Maven CI / gate (push) Successful in 23s
qrun-release / release (push) Successful in 51m53s
QRun-IO/ci-policy: Shared Java Maven CI / gate (push) Successful in 27s
QRun-IO/ci-policy: ci / java-maven-6 (push) Successful in 53m0s
Shared QRun source version / version (push) Successful in 7m26s
2026-11-05 12:47:10 +00:00
James MaesandClaude Sonnet 5.5 3fb67aac99 feat(cms): implement the CMS CORE feature on QQQ 4.1
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
Shared Java Maven CI / secrets (push) Successful in 33s
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 30s
Shared Java Maven CI / verify (push) Successful in 1m31s
qrun-release / postpublish (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 1m33s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Successful in 26s
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Successful in 58m32s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 10s
Release candidate / release-rc (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow f1d46718955078331817c6206b822a94bc7d769f] (pull_request) Successful in 12s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
Shared Java Maven CI / gate (push) Successful in 20s
qrun-release / release (push) Successful in 56m59s
Implement the CORE feature area as standard QQQ tables: site, domain,
redirect, redirect type, page, page type, page section, page section type,
snippet, menu and menu item. Add the CMS app with Sites, Pages and
Navigation homes, CmsFeature flags with dependency validation, per-table
permissions with CmsRole bundles and openReads, a startup schema check
(FAIL_FAST, DEGRADED, SKIP), a prefix-parameterized Liquibase changelog and
the Postgres DDL.

Add unit tests, Postgres-backed Testcontainers install and runtime-identity
tests, and a local demo server used to produce the docs screenshots.

Build against the published QQQ 4.1 BOM (4.1.0-RC.21), add the test-scope
Postgres, Javalin, frontend-next and Testcontainers dependencies, and
enforce checkstyle at validate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 05:28:20 -06:00
forge-recovery e0bb637277 ci: enroll shared Gitea lifecycle
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
Shared Java Maven CI / secrets (push) Successful in 33s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 11s
QRun-IO/ci-policy: Shared Java Maven CI / verify (push) Successful in 1m1s
Shared Java Maven CI / verify (push) Successful in 1m36s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 9s
qrun-release / postpublish (push) Failing after 10m53s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (push) Successful in 36s
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / gate (push) Successful in 20s
QRun-IO/ci-policy: ci / java-maven-6 (push) Successful in 8m7s
Shared Java Maven CI / gate (push) Successful in 22s
qrun-release / release (push) Successful in 9m28s
Shared QRun source version / version (push) Failing after 10m53s
2026-11-02 21:30:55 +00:00
forge-recoveryandCodex d2af100c99 ci: enroll shared Gitea release-candidate lifecycle
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
Shared Java Maven CI / secrets (push) Successful in 16s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 15s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
Shared Java Maven CI / verify (push) Successful in 19s
qrun-release / postpublish (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 1m3s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Successful in 14s
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Successful in 55m56s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 6s
Release candidate / release-rc (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
Shared Java Maven CI / gate (push) Successful in 12s
qrun-release / release (push) Successful in 55m12s
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 20:14:14 +00:00
forge-recovery cfe41b4807 ci: enroll shared Gitea lifecycle
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (push) Successful in 52s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 4s
Shared Java Maven CI / verify (push) Successful in 19s
Shared Java Maven CI / secrets (push) Successful in 15s
qrun-release / postpublish (push) Skipped
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (push) Successful in 19s
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / gate (push) Successful in 15s
QRun-IO/ci-policy: ci / java-maven-6 (push) Successful in 1h44m43s
Shared QRun source version / version (push) Successful in 2m31s
Shared Java Maven CI / gate (push) Successful in 14s
qrun-release / release (push) Successful in 54m57s
2026-11-02 19:15:40 +00:00
d3e39fd6cb ci: add the isolated postpublish job to the QRun release caller
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
Shared Java Maven CI / secrets (push) Successful in 14s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 13s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
Shared Java Maven CI / verify (push) Successful in 49s
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 54s
qrun-release / postpublish (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Successful in 2h38m19s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 4s
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
Shared Java Maven CI / gate (push) Successful in 12s
qrun-release / release (push) Successful in 2h38m13s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 15:47:28 +00:00
forge-recovery dd43ebbaae ci: add the QRun release caller (#19)
QRun-IO/ci-policy: ci / ci-result (push) Failing after 0s
QRun-IO/ci-policy: ci / java-docker-2 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-3 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-7 (push) Skipped
QRun-IO/ci-policy: ci / source-config-8 (push) Skipped
Shared Java Maven CI / verify (push) Failing after 1s
Shared Java Maven CI / secrets (push) Failing after 0s
QRun-IO/ci-policy: Shared Java Maven CI / verify (push) Failing after 0s
QRun-IO/ci-policy: ci / java-maven-4 (push) Failing after 1m4s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / node-5 (push) Skipped
QRun-IO/ci-policy: ci / node-6 (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (push) Failing after 0s
Shared Java Maven CI / gate (push) Failing after 0s
qrun-release / release (push) Failing after 1m5s
QRun-IO/ci-policy: Shared Java Maven CI / gate (push) Failing after 0s
2026-11-02 04:17:48 +00:00
james.maesandCodex 6c9bf5bbea ci: adopt managed orb 0.8 patch track (#16)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 15:29:32 -05:00
james.maesandCodex debbbeb571 ci: adopt shared orb 0.7.18 (#15)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 14:47:23 -05:00
james.maesandCodex 900915ee0e ci: adopt shared orb 0.7.17 (#14)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 14:17:15 -05:00
james.maesandCodex bb1e6527a1 ci: centralize QBit source scanning (#12)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 10:45:12 -05:00
james.maesandCodex d38ba3f9db ci: adopt central orb 0.7.12 and stable gates (#11)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 08:54:04 -05:00
james.maesandCodex c7a84b1146 ci: centralize rc release completion in qqq orb (#10)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 02:25:37 -05:00
james.maesandCodex f0d72e0e4b ci: adopt shared orb 0.7.11 (#9)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 00:57:40 -05:00
james.maes 37d4dc9ef1 Merge pull request #8 from QRun-IO/feature/central-ci
ci: adopt shared QQQ orb standard
2026-10-20 19:04:49 -05:00
james.maesandCodex a3c7c91c50 ci: adopt verified central QQQ orb 0.7.3
Co-Authored-By: Codex <noreply@openai.com>
2026-10-20 19:02:37 -05:00
james.maes f441a6992c Merge pull request #6 from QRun-IO/ci/release-rc-secrets-inherit
ci(release-rc): pass secrets so the shared workflow can sign the RC tag
2026-10-16 08:03:41 -05:00
james.maesandClaude Opus 5.5 4a8b78b846 ci(release-rc): pass secrets to the shared workflow so it can sign the RC tag
QRun-IO/.github#2 adds a tag-and-release job that signs vX with the
QRUN_TAG_SIGNING_KEY secret after Maven Central confirms the artifact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:21:21 -05:00
james.maes 91b28925a9 Merge pull request #5 from QRun-IO/feature/orb-0.6.9
ci: qqq-orb 0.6.9 (refresh SNAPSHOT dependencies)
2026-10-14 10:56:50 -05:00
james.maesandClaude Opus 5.5 f9320e595f ci: qqq-orb 0.6.9 (refresh SNAPSHOT dependencies)
Moves qqq-orb@0.6.8 to qqq-orb@0.6.9, which passes --update-snapshots so cached SNAPSHOT dependencies are re-checked on every build (QRun-IO/qqq-orb#22).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:06:27 -05:00
james.maes c0405acd0c Merge pull request #4 from QRun-IO/feature/release-rc-automation
ci: automate RC releases; hotfix/* pushes no longer publish
2026-10-13 14:43:54 -05:00
james.maesandClaude Opus 5.5 33d8539f5a ci: automate RC releases; hotfix/* pushes no longer publish
Removes publish_hotfix_release: a hotfix/* branch push published a GA
release to Maven Central with no tag or review (QRun-IO/qbit-cdn#12,
QRun-IO/qqq#1121). hotfix/* branches now run test_only. Adds
.github/workflows/release-rc.yml (reusable QRun-IO/.github
qbit-release-rc). Also writes the GA tag filter without brackets
(standard C5): [0-9] inside a YAML flow sequence does not parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:38:54 -05:00
10 changed files with 532 additions and 72 deletions

No files matched your search

+11 -68
View File
@@ -1,71 +1,14 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.6.8
commands:
configure-package-install:
steps:
- run:
name: Keep package installation unattended
command: |
# These build machines are discarded after the job.
sudo install -d /etc/needrestart/conf.d
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
qqq-orb: kingsrook/qqq-orb@0.8
workflows:
test_only:
jobs:
- qqq-orb/mvn_test_only:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
filters:
branches:
ignore: /(develop|release\/.*|hotfix\/.*)/
publish_snapshot:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: snapshot
filters:
branches:
only: [develop]
publish_release_candidate:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release_candidate
filters:
branches:
only: [/release\/.*/]
publish_release:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release
filters:
branches:
ignore: /.*/
tags:
only: [/^v[0-9]+\.[0-9]+\.[0-9]+$/]
publish_hotfix_release:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: hotfix
filters:
branches:
only: [/hotfix\/.*/]
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: '0.8'
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
only: /^v\d+\.\d+\.\d+$/
+7
View File
@@ -0,0 +1,7 @@
name: build
on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
+23
View File
@@ -0,0 +1,23 @@
name: CodeQL
'on':
push:
branches:
- main
- develop
- release/**
- hotfix/**
pull_request: {}
schedule:
- cron: 41 16 * * 0
workflow_dispatch: {}
permissions:
contents: read
actions: read
security-events: write
packages: read
concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
+74
View File
@@ -0,0 +1,74 @@
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 Circle publisher build number (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
repository_dispatch:
types:
- qbit-rc-published
permissions:
contents: write
actions: read
checks: read
jobs:
release-rc:
if: github.event_name == 'workflow_dispatch' || github.event_name == 'repository_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: QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml@cc30f599e63c8e41629c99d23395bac1adb8bc44
with:
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
}}
finalize: ${{ github.event_name == 'repository_dispatch' || (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: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.merge_sha
|| inputs.merge_sha || '' }}
bump_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.bump_sha
|| inputs.bump_sha || '' }}
branch: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch
|| inputs.branch || '' }}
version: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version
|| inputs.version || '' }}
secrets: inherit
+16
View File
@@ -0,0 +1,16 @@
profile: qbit
orb_version: '0.8'
default_branch: develop
java_version: '21'
browsers: true
api_checks: false
docs_only: false
release_on_main: false
release_tag_policy: stable
test_branch_policy: non-release
publish:
snapshot: true
release_candidate: true
release: true
asciidoc: false
release_completion: true
+6 -4
View File
@@ -18,6 +18,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<qqq.version>4.1.0-RC.26</qqq.version> <!-- feeds qbit-build-parent's qqq-bom-pom import -->
<revision>0.1.0-SNAPSHOT</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
@@ -29,7 +30,7 @@
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.0.0</version>
<version>2.2.1</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
@@ -109,7 +110,7 @@
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-frontend-next</artifactId>
<version>1.0.0-RC.14</version>
<version>1.0.0-RC.38</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -216,14 +217,15 @@
<profiles>
<!-- Built against qqq 4.1 (QBitMetaDataProducer). Until qbit-build-parent -->
<!-- imports the released 4.1 BOM, this profile is active by default and -->
<!-- imports qqq-bom-pom 4.1.0-RC.4 ahead of the parent's 4.0.0 BOM. -->
<!-- imports qqq-bom-pom 4.1.0-RC.21 ahead of the parent's 4.0.0 BOM. -->
<profile>
<id>qqq-snapshot</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<qqq.snapshot.version>4.1.0-RC.4</qqq.snapshot.version>
<!-- The published QQQ 4.1 core (Central JAR/POM), the one BOM version shared by the four app-QBit patches. -->
<qqq.snapshot.version>4.1.0-RC.26</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -65,6 +65,7 @@ public class CmsQBitConfig implements QBitConfig
private String parentAppName;
private Boolean openReads = false;
private CmsStartupMode startupMode = CmsStartupMode.FAIL_FAST;
private CmsSchemaVerification schemaVerification = CmsSchemaVerification.CHANGELOG_HISTORY;
private Map<String, CmsRole> roleMapping = new LinkedHashMap<>();
private CmsRuntime runtime;
@@ -98,6 +99,7 @@ public class CmsQBitConfig implements QBitConfig
}
assertCondition(roleMapping != null, "roleMapping may be empty but not null", errors);
assertCondition(startupMode != null, "startupMode is required", errors);
assertCondition(schemaVerification != null, "schemaVerification is required", errors);
}
@@ -465,6 +467,37 @@ public class CmsQBitConfig implements QBitConfig
/*******************************************************************************
** Getter for schemaVerification
*******************************************************************************/
public CmsSchemaVerification getSchemaVerification()
{
return (this.schemaVerification);
}
/*******************************************************************************
** Setter for schemaVerification
*******************************************************************************/
public void setSchemaVerification(CmsSchemaVerification schemaVerification)
{
this.schemaVerification = schemaVerification;
}
/*******************************************************************************
** Fluent setter for schemaVerification
*******************************************************************************/
public CmsQBitConfig withSchemaVerification(CmsSchemaVerification schemaVerification)
{
this.schemaVerification = schemaVerification;
return (this);
}
/*******************************************************************************
** Getter for startupMode
*******************************************************************************/
@@ -29,6 +29,8 @@ import java.util.Locale;
import java.util.Set;
import com.kingsrook.qbits.cms.metadata.CmsTableDefinition;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
@@ -68,6 +70,10 @@ public final class CmsSchemaCheck
List<String> problems = new ArrayList<>();
try(Connection connection = ConnectionManager.getConnection((RDBMSBackendMetaData) qInstance.getBackend(config.getBackendName())))
{
if(config.getSchemaVerification() == CmsSchemaVerification.CATALOG)
{
return (checkCatalog(qInstance, config, connection));
}
Set<String> recorded = recordedChangesets(connection);
if(recorded == null)
{
@@ -99,6 +105,65 @@ public final class CmsSchemaCheck
/*******************************************************************************
** Verify from the database catalog alone: each table of the installed features
** exists and has every column the qbit maps. Never reads Liquibase tables, so it
** works for an identity that has only table privileges.
*******************************************************************************/
static List<String> checkCatalog(QInstance qInstance, CmsQBitConfig config, Connection connection) throws SQLException
{
List<String> problems = new ArrayList<>();
for(CmsTableDefinition definition : CmsTables.definitions(config))
{
String table = config.applyPrefix(definition.logicalName());
if(!tableExists(connection, table))
{
problems.add("Table " + table + " is missing: run the qbit-cms changelog with cms.tablePrefix=" + config.getTableNamePrefix()
+ " and the " + definition.feature() + " feature in cms.features.");
continue;
}
QTableMetaData metaData = qInstance.getTable(table);
if(metaData == null)
{
continue;
}
Set<String> columns = columnsOf(connection, table);
for(QFieldMetaData field : metaData.getFields().values())
{
String column = field.getBackendName() != null ? field.getBackendName() : field.getName();
if(!columns.contains(column.toLowerCase(Locale.ROOT)))
{
problems.add("Column " + table + "." + column + " is missing: the database is behind this version of qbit-cms.");
}
}
}
return (problems);
}
/*******************************************************************************
** The lower-cased column names of a table, from the catalog.
*******************************************************************************/
static Set<String> columnsOf(Connection connection, String table) throws SQLException
{
Set<String> columns = new HashSet<>();
DatabaseMetaData metaData = connection.getMetaData();
for(String candidate : List.of(table, table.toUpperCase(Locale.ROOT), table.toLowerCase(Locale.ROOT)))
{
try(ResultSet resultSet = metaData.getColumns(connection.getCatalog(), null, candidate, null))
{
while(resultSet.next())
{
columns.add(resultSet.getString("COLUMN_NAME").toLowerCase(Locale.ROOT));
}
}
}
return (columns);
}
/*******************************************************************************
** The qbit's recorded changeset ids, or null when Liquibase has never run.
*******************************************************************************/
@@ -0,0 +1,39 @@
/*
* Copyright 2026 Kingsrook, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.cms;
/*******************************************************************************
** What the startup schema check reads to decide the database is up to date.
*******************************************************************************/
public enum CmsSchemaVerification
{
/**
* The Liquibase history (DATABASECHANGELOG) records every changeset and the tables exist
* (the original check). Needs read access to the history table, which a DML-only
* runtime identity does not have.
*/
CHANGELOG_HISTORY,
/**
* Only the database catalog: every table of the installed features exists and has every
* column the qbit's metadata maps. Needs no Liquibase tables and no privilege beyond
* connecting, so it is the check for a runtime identity that owns nothing and may not
* read migration history.
*/
CATALOG
}
@@ -0,0 +1,258 @@
/*
* Copyright 2025 Kingsrook, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.cms;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
import java.util.Set;
import com.kingsrook.qbits.cms.security.CmsRole;
import com.kingsrook.qbits.cms.security.CmsSecurity;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.module.postgres.model.metadata.PostgreSQLBackendMetaData;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/*******************************************************************************
** The install as production runs it on PostgreSQL 17: a schema OWNER runs the
** changelog (Liquibase 4.33.0) with a prefix that cannot collide with a host's own
** CMS tables, and the application runs as a DML-only role that owns nothing, cannot
** read migration history and cannot run DDL. The startup check must work for that
** role (catalog verification), and the QQQ permission ceilings must hold on top of
** the database grants.
*******************************************************************************/
@Testcontainers(disabledWithoutDocker = true)
class CmsPostgresRuntimeIdentityTest
{
private static final String OWNER = "qcms_owner";
private static final String RUNTIME = "qcms_runtime";
private static final String PREFIX = "qcms_";
@Container
private static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:17-alpine");
/*******************************************************************************
** Owner, runtime role, owner-run migration (twice), DML-only grants.
*******************************************************************************/
@BeforeAll
static void provision() throws Exception
{
try(Connection admin = connect(POSTGRES.getUsername(), POSTGRES.getPassword(), "postgres"); Statement statement = admin.createStatement())
{
statement.execute("CREATE ROLE " + OWNER + " LOGIN PASSWORD 'owner-synthetic'");
statement.execute("CREATE ROLE " + RUNTIME + " LOGIN PASSWORD 'runtime-synthetic'");
statement.execute("CREATE DATABASE cmsdb OWNER " + OWNER);
statement.execute("REVOKE ALL ON DATABASE cmsdb FROM PUBLIC");
statement.execute("GRANT CONNECT ON DATABASE cmsdb TO " + RUNTIME);
}
try(Connection owner = connect(OWNER, "owner-synthetic", "cmsdb"); Statement statement = owner.createStatement())
{
statement.execute("REVOKE ALL ON SCHEMA public FROM PUBLIC");
statement.execute("GRANT USAGE ON SCHEMA public TO " + RUNTIME);
}
ConnectionManager.resetConnectionProviders();
PostgreSQLBackendMetaData ownerBackend = backend(OWNER, "owner-synthetic");
CmsTestSupport.migrate(ownerBackend, Map.of("cms.tablePrefix", PREFIX));
CmsTestSupport.migrate(ownerBackend, Map.of("cms.tablePrefix", PREFIX));
try(Connection owner = connect(OWNER, "owner-synthetic", "cmsdb"); Statement statement = owner.createStatement())
{
statement.execute("DO $$ DECLARE t text; BEGIN FOR t IN SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE '" + PREFIX
+ "%' LOOP EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON %I TO " + RUNTIME + "', t); END LOOP; END $$");
}
}
/*******************************************************************************
** Clear the context.
*******************************************************************************/
@AfterEach
void afterEach()
{
QContext.clear();
}
/*******************************************************************************
** The original history check cannot run as the DML-only role and must fail
** closed; the catalog check starts it.
*******************************************************************************/
@Test
void testStartupCheckAsRuntimeRole() throws Exception
{
CmsQBitConfig history = config().withSchemaVerification(CmsSchemaVerification.CHANGELOG_HISTORY);
assertThatThrownBy(() -> new CmsQBitProducer().withQBitConfig(history).produce(runtimeInstance()))
.isInstanceOf(QException.class).hasMessageContaining("not up to date");
CmsQBitConfig catalog = config().withSchemaVerification(CmsSchemaVerification.CATALOG);
new CmsQBitProducer().withQBitConfig(catalog).produce(runtimeInstance());
assertThat(catalog.getRuntime().getSchemaProblems()).isEmpty();
}
/*******************************************************************************
** The catalog check names a missing table and a missing column, and refuses to
** start in FAIL_FAST mode.
*******************************************************************************/
@Test
void testCatalogCheckFindsDrift() throws Exception
{
CmsQBitConfig unmigrated = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND).withTableNamePrefix("zcms_").withNamePrefix("zcms")
.withPermissionBaseName("zcms").withSchemaVerification(CmsSchemaVerification.CATALOG);
assertThatThrownBy(() -> new CmsQBitProducer().withQBitConfig(unmigrated).produce(runtimeInstance()))
.isInstanceOf(QException.class).hasMessageContaining("zcms_page is missing");
try(Connection owner = connect(OWNER, "owner-synthetic", "cmsdb"); Statement statement = owner.createStatement())
{
statement.execute("ALTER TABLE " + PREFIX + "page RENAME COLUMN title TO title_old");
try
{
CmsQBitConfig drifted = config().withSchemaVerification(CmsSchemaVerification.CATALOG);
assertThatThrownBy(() -> new CmsQBitProducer().withQBitConfig(drifted).produce(runtimeInstance()))
.isInstanceOf(QException.class).hasMessageContaining(PREFIX + "page.title is missing");
}
finally
{
statement.execute("ALTER TABLE " + PREFIX + "page RENAME COLUMN title_old TO title");
}
}
}
/*******************************************************************************
** The runtime role can do DML and nothing else: no DDL, no migration history.
*******************************************************************************/
@Test
void testRuntimeRoleIsDmlOnly() throws Exception
{
try(Connection runtime = connect(RUNTIME, "runtime-synthetic", "cmsdb"); Statement statement = runtime.createStatement())
{
assertThatThrownBy(() -> statement.execute("CREATE TABLE intruder (id int)")).isInstanceOf(SQLException.class).hasMessageContaining("permission denied");
assertThatThrownBy(() -> statement.execute("ALTER TABLE " + PREFIX + "site ADD COLUMN x int")).isInstanceOf(SQLException.class);
assertThatThrownBy(() -> statement.execute("DROP TABLE " + PREFIX + "site")).isInstanceOf(SQLException.class);
assertThatThrownBy(() -> statement.execute("TRUNCATE " + PREFIX + "site")).isInstanceOf(SQLException.class);
assertThatThrownBy(() -> statement.executeQuery("SELECT * FROM databasechangelog")).isInstanceOf(SQLException.class).hasMessageContaining("permission denied");
}
}
/*******************************************************************************
** Role ceilings over the DML grants, through QQQ's own actions on the native PG
** backend as the runtime role: an admin writes, a viewer reads only, an editor
** cannot change reference data, and a user with no CMS permission is refused.
*******************************************************************************/
@Test
void testRoleCeilingsAsRuntimeRole() throws Exception
{
CmsQBitConfig config = config().withSchemaVerification(CmsSchemaVerification.CATALOG);
QInstance instance = runtimeInstance();
instance.setEnforcePermissionsInCoreActions(true);
new CmsQBitProducer().withQBitConfig(config).produce(instance);
CmsTestSupport.session(instance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.ADMIN)));
Integer siteId = new InsertAction().execute(new InsertInput(PREFIX + "site").withRecord(new QRecord().withValue("name", "Example").withValue("shortName", "ex")))
.getRecords().get(0).getValueInteger("id");
assertThat(siteId).isNotNull();
CmsTestSupport.session(instance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.VIEWER)));
assertThat(new QueryAction().execute(new QueryInput(PREFIX + "site")).getRecords()).isNotEmpty();
assertThatThrownBy(() -> new InsertAction().execute(new InsertInput(PREFIX + "site").withRecord(new QRecord().withValue("name", "V").withValue("shortName", "v"))))
.isInstanceOf(QPermissionDeniedException.class);
CmsTestSupport.session(instance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.EDITOR)));
assertThatThrownBy(() -> new InsertAction().execute(new InsertInput(PREFIX + "page_type").withRecord(new QRecord().withValue("name", "X"))))
.isInstanceOf(QPermissionDeniedException.class);
CmsTestSupport.session(instance, Set.of());
assertThatThrownBy(() -> new QueryAction().execute(new QueryInput(PREFIX + "site"))).isInstanceOf(QPermissionDeniedException.class);
assertThatThrownBy(() -> new QueryAction().execute(new QueryInput(PREFIX + "page"))).isInstanceOf(QPermissionDeniedException.class);
}
/*******************************************************************************
** The configuration under test: a prefix of its own, never the default.
*******************************************************************************/
private static CmsQBitConfig config()
{
return (new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND).withTableNamePrefix(PREFIX).withNamePrefix("qcms").withPermissionBaseName("qcms"));
}
/*******************************************************************************
** An application instance on the runtime role.
*******************************************************************************/
private static QInstance runtimeInstance()
{
ConnectionManager.resetConnectionProviders();
return (CmsTestSupport.blankInstance(backend(RUNTIME, "runtime-synthetic")));
}
/*******************************************************************************
** A native PostgreSQL backend as a named role.
*******************************************************************************/
private static PostgreSQLBackendMetaData backend(String user, String password)
{
return (new PostgreSQLBackendMetaData()
.withName(CmsTestSupport.BACKEND)
.withHostName(POSTGRES.getHost())
.withPort(POSTGRES.getFirstMappedPort())
.withDatabaseName("cmsdb")
.withUsername(user)
.withPassword(password));
}
/*******************************************************************************
** A JDBC connection as a named role.
*******************************************************************************/
private static Connection connect(String user, String password, String database) throws SQLException
{
return (DriverManager.getConnection("jdbc:postgresql://" + POSTGRES.getHost() + ":" + POSTGRES.getFirstMappedPort() + "/" + database, user, password));
}
}