Compare commits

...
23 Commits
Author SHA1 Message Date
james.maes 4ef8a6293f Merge pull request 'build(deps): align with Gitea registry (parent 2.2.1, QQQ 4.1.0-RC.26)' (#53) from deps/align-gitea into develop
build / maven (push) Successful in 1m50s
maven / build (push) Successful in 1m50s
2026-11-07 18:06:38 +00:00
james.maesandClaude Opus 5.5 c58324bccf build(deps): align with Gitea registry (parent 2.2.1, QQQ 4.1.0-RC.26)
build / maven (push) Successful in 3m28s
maven / build (push) Successful in 3m28s
pom.xml: parent -> 2.2.1
pom.xml: qqq.snapshot.version: 4.1.0-RC.14 -> 4.1.0-RC.26
pom.xml: qbit-object-storage: 0.1.0-RC.2 -> 0.1.0-RC.10
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:55:30 +00:00
james.maes 06b766d516 Merge pull request 'ci: pin shared workflow to v1' (#52) from ci/pin-v1-develop into develop
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (push) Successful in 20s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 16s
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-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (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-business (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 11s
build / maven (push) Successful in 3m59s
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 / 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: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 3m59s
2026-11-06 16:03:45 +00:00
james.maesandClaude Opus 5.5 5c32e3c5f3 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: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
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-mcp (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 / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Waiting to run
build / maven (push) Successful in 3m27s
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 / shared-pipeline (pull_request) Skipped
maven / build (push) Successful in 3m27s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:04:43 +00:00
james.maes 1de785328e Merge pull request 'ci: use shared ci-workflows Maven workflow' (#51) from ci/ci-workflows into develop
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (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 / 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-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (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-business (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 7s
build / maven (push) Successful in 2m49s
QRun-IO/ci-policy: ci / Scoped workflow [workflow d20a578c7d818bc7ca32a2ac2c675da4e4f27385] (push) Successful in 8s
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 / 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: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 2m48s
2026-11-05 23:14:34 +00:00
james.maesandClaude Opus 5.5 dbcfd85da8 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: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1b3648d9421543350067e95e54959dfb471b26d5] (pull_request) Successful in 7s
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: native / native-result (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (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 / not-enrolled (pull_request) Skipped
build / maven (push) Successful in 2m32s
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 2s
QRun-IO/ci-policy: ci / gradle-1 (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 / shared-pipeline (pull_request) Skipped
maven / build (push) Successful in 2m33s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (pull_request) Successful in 2m3s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (pull_request) Successful in 16s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (pull_request) Failing after 2s
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Failing after 19m29s
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:02:21 +00:00
forge-recovery ff76412d80 ci: enroll shared Gitea lifecycle
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / head-profile (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-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (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 / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
Shared Java Maven CI with Docker / secrets (push) Successful in 27s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (push) Successful in 24s
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 10s
QRun-IO/ci-policy: ci / java-docker-3 (push) Successful in 11m52s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 6s
Shared Java Maven CI with Docker / verify (push) Successful in 2m38s
qrun-release / postpublish (push) Failing after 10m40s
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-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (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: 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 with Docker / verify (push) Successful in 1m52s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (push) Successful in 13s
Shared Java Maven CI with Docker / gate (push) Successful in 11s
qrun-release / release (push) Successful in 12m25s
Shared QRun source version / version (push) Failing after 10m40s
2026-11-02 22:10:12 +00:00
forge-recovery 76ac13c14f ci: enroll shared Gitea lifecycle
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / head-profile (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-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (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 / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 7s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (push) Successful in 22s
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: ci / java-docker-3 (push) Successful in 7m24s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 11s
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-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (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: 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 with Docker / verify (push) Successful in 2m6s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (push) Successful in 20s
2026-11-02 21:30:25 +00:00
forge-recoveryandCodex 7ba2740283 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-6 (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: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (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 / 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
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-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-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (pull_request) Successful in 2m7s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (pull_request) Successful in 16s
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Successful in 55m33s
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 20:13:41 +00:00
c82c847b35 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-6 (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: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
Shared Java Maven CI with Docker / secrets (push) 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
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 4s
Shared Java Maven CI with Docker / verify (push) Successful in 1m35s
qrun-release / postpublish (push) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 5s
Release candidate / release-rc (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (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: Shared Java Maven CI with Docker / secrets (pull_request) Successful in 14s
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
Shared Java Maven CI with Docker / gate (push) Successful in 10s
qrun-release / release (push) Successful in 2h25m44s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (pull_request) Successful in 1m31s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Successful in 2h27m8s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 15:45:49 +00:00
dc69ffe245 ci: move the QRun release caller to the thin v2 contract
QRun-IO/ci-policy: ci / java-frontend-browser-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-9 (pull_request) Skipped
Shared Java Maven CI with Docker / secrets (push) Failing after 17s
QRun-IO/ci-policy: ci / java-docker-2 (pull_request) Canceled after 1h25m12s
Shared Java Maven CI with Docker / verify (push) Canceled after 0s
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-6 (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: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
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
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / java-maven-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-7 (pull_request) Skipped
Shared Java Maven CI with Docker / gate (push) Canceled after 0s
qrun-release / release (push) Canceled after 1h25m16s
QRun-IO/ci-policy: ci / gradle-1 (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 / source-config-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (pull_request) Successful in 21s
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (pull_request) Successful in 1m39s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Successful in 1h41m21s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 06:16:09 +00:00
forge-recoveryandCodex 66fd64e1c4 ci: add the identical QRun release caller
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-2 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-1 (pull_request) Failing after 14m1s
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 5s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (pull_request) Failing after 16s
QRun-IO/ci-policy: ci / gradle-0 (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (pull_request) Successful in 1m46s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (pull_request) Failing after 15s
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 03:25:46 +00:00
james.maesandCodex 895727acbb ci: adopt managed orb 0.8 patch track (#45)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 15:28:38 -05:00
james.maesandCodex 886e76b333 ci: adopt shared orb 0.7.18 (#44)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 14:48:47 -05:00
james.maesandCodex 603f0c9d82 ci: adopt shared orb 0.7.17 (#43)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 14:16:17 -05:00
james.maesandCodex bdf79eae09 ci: centralize QBit source scanning (#40)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 10:43:14 -05:00
james.maesandCodex ec5fef94bc ci: adopt central orb 0.7.12 and stable gates (#39)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 08:56:47 -05:00
james.maesandCodex b21db17a9f ci: centralize rc release completion in qqq orb (#38)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 02:25:46 -05:00
james.maesandCodex 7fa71bbaa6 ci: adopt shared orb 0.7.11 (#37)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 00:59:34 -05:00
james.maes 84f030d048 Merge pull request #36 from QRun-IO/feature/central-ci
ci: adopt shared QQQ orb standard
2026-10-20 18:57:40 -05:00
james.maesandCodex 9cb78190d8 ci: adopt verified central QQQ orb 0.7.3
Co-Authored-By: Codex <noreply@openai.com>
2026-10-20 18:52:23 -05:00
james.maes 91d86b016a Merge pull request #33 from QRun-IO/feature/backups-error-detail-unqueryable
fix(run): skip unqueryable tables and record sanitized error detail
2026-10-16 11:51:14 -05:00
james.maesandClaude Opus 5.5 0c28ea6263 fix(run): skip unqueryable tables and record sanitized error detail
A table on a backend module with no query interface (a storage-only
module such as qbit-edi's edi_preview_download) was classified DATA,
read by the keyset pager, and failed the whole run with
"IllegalStateException: Query is not implemented in this module".
TableRoles.isQueryable() now resolves the module through
QBackendModuleDispatcher (cached per backend type) and such tables are
EXTERNAL and always excluded by ScopeResolver with reason NOT_QUERYABLE,
even when selected by name or role. The Run backup review lists them,
and no per-table export process is offered for them.

Failed runs now persist "Backup failed: Class: message" in error_message
and the class, cause chain and top root-cause frames (bounded to 8 KB)
in error_detail, both sanitized by the new FailureText (URL user-info,
presigned query parameters, key=value secrets, Authorization/Bearer
values, AWS access key ids, PEM blocks and Postgres row values are
redacted; object keys and bucket names are kept).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:41:15 -05:00
18 changed files with 813 additions and 96 deletions

No files matched your search

+11 -57
View File
@@ -1,60 +1,14 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.6.9
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\/.*)/
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\d+\.\d+\.\d+$/]
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
+62 -14
View File
@@ -1,26 +1,74 @@
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
# the release line and the release/* push runs CircleCI publish_release_candidate.
name: Release candidate
on:
'on':
workflow_dispatch:
inputs:
dry_run:
description: Print the plan without merging or creating branches
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]
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.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
secrets: inherit
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 }}
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
+2
View File
@@ -16,6 +16,8 @@ All notable changes to this project are documented here. The format follows Keep
- Repository scaffold: `BackupsQBitConfig` and `BackupsQBitProducer` (table prefix `backups_`, permissions `backups.*`), build, CI and repository standard files.
### Changed
- Tables whose backend module has no query interface (storage-only modules such as qbit-edi `edi_preview_download`) are classified EXTERNAL and always left out of a backup with the reason `NOT_QUERYABLE`, even when selected by name or role; the Run backup review lists them. Before, such a table was read as DATA and the whole run FAILED with "Query is not implemented in this module".
- Failed runs persist an actionable `error_message` ("Backup failed: Class: message") and fill `error_detail` with the exception class, the cause chain and the top root-cause frames (bounded to 8 KB). Both go through `FailureText.sanitize`, which redacts URL user-info, presigned query parameters, key=value secrets, Authorization/Bearer values, AWS access key ids, PEM blocks and Postgres row values while keeping object keys and bucket names.
- Multi-node deployments: the cached Prometheus text (`BackupsMetrics`) is registered with QQQ `ClusterCaches` as `qbit-backups.metrics` and cleared on every node when a run finishes or a plan is inserted, updated or deleted; the 60 s TTL remains the safety net. `SlaChecker`'s manifest-head and bucket-audit throttles and `BackupsRuntime`'s process-permission map are registered as node-local, with reasons. `BackupsClusterCachesTest` audits the package for unregistered static caches and covers a remote invalidation.
- Builds against QQQ `4.1.0-SNAPSHOT` (`qqq.snapshot.version`) for `ClusterCaches` until the next QQQ RC ships; a release must pin that RC.
- Read the QBit metadata version from the Maven artifact version so release builds report their released version.
+5 -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 -->
<qbit.guardrails.enforce>true</qbit.guardrails.enforce>
<qbit.guardrails.strictUiApiRoutes>true</qbit.guardrails.strictUiApiRoutes>
<revision>0.1.0-SNAPSHOT</revision>
@@ -29,7 +30,7 @@
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.1.0</version>
<version>2.2.1</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
@@ -86,7 +87,7 @@
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-object-storage</artifactId>
<version>0.1.0-RC.2</version>
<version>0.1.0-RC.10</version>
<optional>true</optional>
</dependency>
@@ -106,7 +107,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>
@@ -216,7 +217,7 @@
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<qqq.snapshot.version>4.1.0-RC.14</qqq.snapshot.version>
<qqq.snapshot.version>4.1.0-RC.26</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -22,6 +22,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import com.kingsrook.qbits.backups.BackupsRuntime;
import com.kingsrook.qbits.backups.model.BackupsTables;
import com.kingsrook.qbits.backups.permissions.BackupsPermissions;
@@ -178,6 +180,10 @@ public final class BackupsProcesses
private final ProcessSummaryLine tables = line(Status.INFO, "table will be backed up", "tables will be backed up", "table was backed up", "tables were backed up");
private final ProcessSummaryLine excluded = line(Status.INFO, "table is left out by its role or scope", "tables are left out by their role or scope",
"table was left out by its role or scope", "tables were left out by their role or scope");
private final ProcessSummaryLine unreadable = line(Status.INFO, "table is left out because its backend cannot be queried (storage only)",
"tables are left out because their backend cannot be queried (storage only)", "table was left out because its backend cannot be queried (storage only)",
"tables were left out because their backend cannot be queried (storage only)");
private final Set<String> unreadableNames = new TreeSet<>();
private final ProcessSummaryLine inactive = line(Status.WARNING, "plan is not active (it can still be run by hand)", "plans are not active (they can still be run by hand)",
"plan was not active", "plans were not active");
private final ProcessSummaryLine invalid = line(Status.ERROR, "plan cannot run", "plans cannot run", "plan could not run", "plans could not run");
@@ -205,6 +211,14 @@ public final class BackupsProcesses
plans.incrementCountAndAddPrimaryKey(plan.getValue("id"));
tables.incrementCount(scope.tables().size());
excluded.incrementCount(scope.excluded().size());
for(ScopeResolver.ExcludedTable out : scope.excluded())
{
if(ScopeResolver.NOT_QUERYABLE.equals(out.reason()) && unreadableNames.add(out.name()))
{
unreadable.incrementCount();
unreadable.setMessageSuffix(": " + String.join(", ", unreadableNames));
}
}
if(!Boolean.TRUE.equals(plan.getValueBoolean("isActive")))
{
inactive.incrementCountAndAddPrimaryKey(plan.getValue("id"));
@@ -235,6 +249,7 @@ public final class BackupsProcesses
plans.addSelfToListIfAnyCount(lines);
tables.addSelfToListIfAnyCount(lines);
excluded.addSelfToListIfAnyCount(lines);
unreadable.addSelfToListIfAnyCount(lines);
inactive.addSelfToListIfAnyCount(lines);
invalid.addSelfToListIfAnyCount(lines);
return (lines);
@@ -118,7 +118,7 @@ public final class ExportTableProcess
continue;
}
TableRole role = TableRoles.roleOf(qInstance, table);
if(role == TableRole.DATA || role == TableRole.AUDIT || role == TableRole.DERIVED)
if((role == TableRole.DATA || role == TableRole.AUDIT || role == TableRole.DERIVED) && TableRoles.isQueryable(qInstance, table))
{
processes.add(processFor(table.getName()));
}
@@ -77,7 +77,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
** content-addressed store; checksums.sha256 is written; and the manifest is
** written LAST, then checked with a head. A run with no manifest did not
** complete. The run row is updated every page (heartbeat), and a failure marks
** the run FAILED with a scrubbed message (never row values).
** the run FAILED with a sanitized message and detail (no secrets, no row values).
*******************************************************************************/
public class BackupEngine
{
@@ -142,6 +142,7 @@ public class BackupEngine
runValues.remove("startDate");
runValues.put("endDate", null);
runValues.put("errorMessage", null);
runValues.put("errorDetail", null);
}
if(request.getExistingRunId() != null)
{
@@ -155,9 +156,12 @@ public class BackupEngine
}
catch(Exception e)
{
LOG.warn("Could not create the backup run record", e, logPair("runKey", runKey));
String message = FailureText.summary("Could not create the run record", e);
result.setStatus("FAILED");
result.setErrorMessage("Could not create the run record (" + e.getClass().getSimpleName() + ")");
failProgress(progress, "Could not create the run record (" + e.getClass().getSimpleName() + ")");
result.setErrorMessage(message);
result.setErrorDetail(FailureText.detail(e));
failProgress(progress, message);
return (result);
}
@@ -174,12 +178,12 @@ public class BackupEngine
}
catch(CancelledException e)
{
finish(request, result, "CANCELLED", "Cancelled", started);
finish(request, result, "CANCELLED", "Cancelled", null, started);
progress.info("Backup cancelled");
}
catch(TimedOutException e)
{
finish(request, result, "TIMED_OUT", e.getMessage(), started);
finish(request, result, "TIMED_OUT", e.getMessage(), null, started);
failProgress(progress, e.getMessage());
}
catch(Exception e)
@@ -189,14 +193,14 @@ public class BackupEngine
{
if(interrupted.getReason() != JobInterruptedException.Reason.LEASE_LOST)
{
finish(request, result, interrupted.getReason() == JobInterruptedException.Reason.CANCEL ? "CANCELLED" : "ABANDONED", "Job interrupted", started);
finish(request, result, interrupted.getReason() == JobInterruptedException.Reason.CANCEL ? "CANCELLED" : "ABANDONED", "Job interrupted", null, started);
}
throw (interrupted);
}
LOG.warn("Backup run failed", e, logPair("runKey", runKey));
// Provider and reader messages can contain credentials or row values.
failProgress(progress, scrub(e));
finish(request, result, "FAILED", scrub(e), started);
String message = scrub(e);
failProgress(progress, message);
finish(request, result, "FAILED", message, FailureText.detail(e), started);
}
finally
{
@@ -573,7 +577,7 @@ public class BackupEngine
extra.put("bytesRaw", bytesRaw);
extra.put("bytesStored", bytesStored);
extra.put("warningCount", result.getWarnings().size());
finishWith(request, result, result.getWarnings().isEmpty() ? "SUCCEEDED" : "SUCCEEDED_WITH_WARNINGS", null, started, extra);
finishWith(request, result, result.getWarnings().isEmpty() ? "SUCCEEDED" : "SUCCEEDED_WITH_WARNINGS", null, null, started, extra);
}
@@ -741,11 +745,11 @@ public class BackupEngine
/*******************************************************************************
**
*******************************************************************************/
private void finish(BackupRunRequest request, BackupRunResult result, String status, String message, Instant started)
private void finish(BackupRunRequest request, BackupRunResult result, String status, String message, String detail, Instant started)
{
try
{
finishWith(request, result, status, message, started, new LinkedHashMap<>());
finishWith(request, result, status, message, detail, started, new LinkedHashMap<>());
}
catch(Exception e)
{
@@ -753,6 +757,7 @@ public class BackupEngine
request.getProgress().warn("Could not finish the run record");
result.setStatus(status);
result.setErrorMessage(message);
result.setErrorDetail(detail);
}
}
@@ -761,7 +766,7 @@ public class BackupEngine
/*******************************************************************************
**
*******************************************************************************/
private void finishWith(BackupRunRequest request, BackupRunResult result, String status, String message, Instant started, Map<String, Serializable> extra) throws Exception
private void finishWith(BackupRunRequest request, BackupRunResult result, String status, String message, String detail, Instant started, Map<String, Serializable> extra) throws Exception
{
Instant end = request.getClock().instant();
Map<String, Serializable> v = new LinkedHashMap<>(extra);
@@ -770,10 +775,12 @@ public class BackupEngine
v.put("durationMs", end.toEpochMilli() - started.toEpochMilli());
v.put("heartbeatDate", end);
v.put("errorMessage", message);
v.put("errorDetail", detail);
v.put("warningCount", result.getWarnings().size());
catalog.update(BackupsTables.RUN, result.getRunId(), v);
result.setStatus(status);
result.setErrorMessage(message);
result.setErrorDetail(detail);
signal(request, result, status, message);
BackupsMetrics.invalidate();
}
@@ -828,12 +835,17 @@ public class BackupEngine
/*******************************************************************************
** Provider exceptions may contain credentials or row values. Persist only the
** failure type; detailed diagnostics stay in the server log.
** The error_message of a failed run: "Backup failed: SimpleClass: message".
** Provider, driver and reader messages can contain credentials or row values,
** so the message goes through FailureText.sanitize (URL user-info, presigned
** query parameters, key=value secrets, Authorization values, AWS key ids, PEM
** blocks and Postgres row-value fragments are redacted; object keys and bucket
** names are kept). The class, the cause chain and the root cause frames go to
** error_detail (FailureText.detail); the full trace stays in the server log.
*******************************************************************************/
private static String scrub(Exception e)
{
return ("Backup failed (" + e.getClass().getSimpleName() + "); see server logs for details.");
return (FailureText.summary(e));
}
@@ -43,6 +43,7 @@ public class BackupRunResult
private int filesMissing;
private long fileBytesCopied;
private String errorMessage;
private String errorDetail;
private List<String> warnings = new ArrayList<>();
@@ -397,6 +398,26 @@ public class BackupRunResult
/*******************************************************************************
** Getter for errorDetail (sanitized class, message, cause chain and root frames)
*******************************************************************************/
public String getErrorDetail()
{
return (errorDetail);
}
/*******************************************************************************
** Setter for errorDetail
*******************************************************************************/
public void setErrorDetail(String errorDetail)
{
this.errorDetail = errorDetail;
}
/*******************************************************************************
** Getter for warnings
*******************************************************************************/
@@ -0,0 +1,184 @@
/*
* 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.backups.run;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/*******************************************************************************
** Safe text for a failure: what is persisted on a run row (error_message and
** error_detail) and shown in job output.
**
** Exception messages from storage providers, drivers and readers can carry
** credentials (URL user-info, presigned query strings, key=value secrets,
** Authorization headers, AWS access key ids, PEM blocks) and row values (the
** Postgres "Key (col)=(value)" and "Detail:" fragments). sanitize() redacts
** those and keeps the rest (class names, object keys, bucket names), which is
** what an operator needs to act on a failure without reading the server log.
*******************************************************************************/
public final class FailureText
{
/** Redaction marker. */
public static final String REDACTED = "[REDACTED]";
/** The longest error_detail written. */
public static final int MAX_DETAIL_CHARS = 8192;
/** The longest exception message kept in error_message. */
public static final int MAX_SUMMARY_MESSAGE_CHARS = 500;
/** Stack frames of the root cause kept in error_detail. */
public static final int ROOT_CAUSE_FRAMES = 8;
private static final int MAX_CAUSES = 20;
private static final String SECRET_NAME = "[\\w.-]*(?:password|passwd|pwd|secret|token|credential|signature|api[_-]?key|access[_-]?key|private[_-]?key)[\\w.-]*";
private static final Pattern PEM_BLOCK = Pattern.compile("-----BEGIN [A-Z0-9 ]+-----.*?(?:-----END [A-Z0-9 ]+-----|$)", Pattern.DOTALL);
private static final Pattern URL_USERINFO = Pattern.compile("(?i)\\b([a-z][a-z0-9+.-]*://)[^/\\s@?#]+@");
private static final Pattern QUERY_PARAM = Pattern.compile("(?i)([?&;](?:X-Amz-[\\w-]+|Signature|Credential|AWSAccessKeyId|Policy|sig|key|" + SECRET_NAME + ")=)[^&\\s\"'<>]*");
private static final Pattern PG_KEY_VALUE = Pattern.compile("(?i)\\b(Key \\([^)]*\\)=\\().*?(\\))(?=\\s|$|[.,;:])");
private static final Pattern PG_DETAIL = Pattern.compile("(?i)\\b(Detail:|Failing row contains)[^\\r\\n]*");
private static final Pattern AUTHORIZATION = Pattern.compile("(?i)\\b((?:Proxy-)?Authorization\"?\\s*[:=]\\s*)(\"[^\"]*\"|[^\\r\\n,;]+)");
private static final Pattern BEARER = Pattern.compile("(?i)\\b(Bearer|Basic)\\s+[A-Za-z0-9._~+/=-]{6,}");
private static final Pattern SECRET_JSON = Pattern.compile("(?i)(\"" + SECRET_NAME + "\"\\s*:\\s*)\"[^\"]*\"");
private static final Pattern SECRET_ASSIGN = Pattern.compile("(?i)(?<![\\w.-])((?:" + SECRET_NAME + "|key)\\s*=\\s*)(?!\\[REDACTED)(\"[^\"]*\"|'[^']*'|[^\\s&,;)\\]}]+)");
private static final Pattern AWS_KEY_ID = Pattern.compile("\\b(?:AKIA|ASIA|AGPA|AIDA|AROA|ANPA|ANVA|AIPA)[A-Z0-9]{16}\\b");
private static final String R = Matcher.quoteReplacement(REDACTED);
/*******************************************************************************
** Utility class.
*******************************************************************************/
private FailureText()
{
}
/*******************************************************************************
** Redact secrets and row values from a message. Null stays null.
*******************************************************************************/
public static String sanitize(String text)
{
if(text == null)
{
return (null);
}
String s = PEM_BLOCK.matcher(text).replaceAll(Matcher.quoteReplacement("[REDACTED PEM]"));
s = URL_USERINFO.matcher(s).replaceAll("$1" + R + "@");
s = QUERY_PARAM.matcher(s).replaceAll("$1" + R);
s = PG_KEY_VALUE.matcher(s).replaceAll("$1" + R + "$2");
s = PG_DETAIL.matcher(s).replaceAll("$1 " + R);
s = AUTHORIZATION.matcher(s).replaceAll("$1" + R);
s = BEARER.matcher(s).replaceAll("$1 " + R);
s = SECRET_JSON.matcher(s).replaceAll("$1\"" + R + "\"");
s = SECRET_ASSIGN.matcher(s).replaceAll("$1" + R);
s = AWS_KEY_ID.matcher(s).replaceAll(R);
return (s);
}
/*******************************************************************************
** The short error_message: "Backup failed: SimpleClass: sanitized message".
*******************************************************************************/
public static String summary(Throwable t)
{
return (summary("Backup failed", t));
}
/*******************************************************************************
** A short message: "prefix: SimpleClass: sanitized message" (bounded).
*******************************************************************************/
public static String summary(String prefix, Throwable t)
{
String message = sanitize(t.getMessage());
if(message == null || message.isBlank())
{
return (prefix + ": " + t.getClass().getSimpleName());
}
message = message.strip();
if(message.length() > MAX_SUMMARY_MESSAGE_CHARS)
{
message = message.substring(0, MAX_SUMMARY_MESSAGE_CHARS) + "...";
}
return (prefix + ": " + t.getClass().getSimpleName() + ": " + message);
}
/*******************************************************************************
** The error_detail: the exception class and sanitized message, each cause
** ("Caused by: ..."), then the top stack frames of the root cause, bounded to
** MAX_DETAIL_CHARS. Frames are class, method, file and line only (no values).
*******************************************************************************/
public static String detail(Throwable t)
{
StringBuilder out = new StringBuilder(line(t));
Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
seen.add(t);
Throwable root = t;
Throwable cause = t.getCause();
while(cause != null && seen.size() < MAX_CAUSES && seen.add(cause))
{
out.append("\nCaused by: ").append(line(cause));
root = cause;
cause = cause.getCause();
}
List<StackTraceElement> frames = List.of(root.getStackTrace());
int shown = Math.min(ROOT_CAUSE_FRAMES, frames.size());
if(shown > 0)
{
out.append("\nRoot cause stack (top ").append(shown).append(" of ").append(frames.size()).append(" frames):");
for(StackTraceElement frame : frames.subList(0, shown))
{
out.append("\n\tat ").append(frame);
}
}
if(out.length() > MAX_DETAIL_CHARS)
{
String marker = "\n... (truncated)";
out.setLength(MAX_DETAIL_CHARS - marker.length());
out.append(marker);
}
return (out.toString());
}
/*******************************************************************************
** "fully.qualified.Class: sanitized message".
*******************************************************************************/
private static String line(Throwable t)
{
String message = sanitize(t.getMessage());
return (message == null ? t.getClass().getName() : t.getClass().getName() + ": " + message);
}
}
@@ -48,6 +48,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
*******************************************************************************/
public class ScopeResolver
{
/** Exclusion reason of a table whose backend has no query interface (storage-only): it cannot be read, even when selected. */
public static final String NOT_QUERYABLE = "NOT_QUERYABLE";
private final QInstance qInstance;
@@ -205,6 +208,10 @@ public class ScopeResolver
{
excluded.add(new ExcludedTable(name, "BACKUPS"));
}
else if(!TableRoles.isQueryable(qInstance, table))
{
excluded.add(new ExcludedTable(name, NOT_QUERYABLE));
}
else if((role == TableRole.EPHEMERAL || role == TableRole.DERIVED || role == TableRole.EXTERNAL) && !explicit.contains(name) && !roles.contains(role))
{
excluded.add(new ExcludedTable(name, role.name()));
@@ -35,6 +35,6 @@ public enum TableRole
SECRET,
/** The tables of this qbit: the catalog is rebuilt from manifests. */
BACKUPS,
/** Tables backed by another system (API). Excluded by default; selectable. */
/** Tables backed by another system (API), or by a backend that cannot be queried (storage-only: never read, even when selected). Excluded by default. */
EXTERNAL
}
@@ -17,12 +17,20 @@
package com.kingsrook.qbits.backups.scope;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import com.kingsrook.qbits.backups.BackupsQBitConfig;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
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.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@@ -31,6 +39,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
*******************************************************************************/
public final class TableRoles
{
private static final QLogger LOG = QLogger.getLogger(TableRoles.class);
////////////////////////////////////////////////////////////////////////////////
// whether a backend type's module has a query interface: the answer depends //
// only on the module class, so it is cached per backend type (roleOf is hot) //
////////////////////////////////////////////////////////////////////////////////
private static final Map<String, Boolean> QUERYABLE_BY_BACKEND_TYPE = new ConcurrentHashMap<>();
private static final Set<String> AUDIT_TABLES = Set.of("audit", "auditTable", "auditUser", "auditDetail");
private static final String[] SECRET_NAME_HINTS = { "refreshToken", "RefreshToken", "oauthToken", "OAuthToken", "apiKey" };
@@ -76,6 +92,10 @@ public final class TableRoles
{
return (TableRole.SECRET);
}
if(qInstance != null && !isQueryable(qInstance, table))
{
return (TableRole.EXTERNAL);
}
if(AUDIT_TABLES.contains(name))
{
return (TableRole.AUDIT);
@@ -108,6 +128,56 @@ public final class TableRoles
/*******************************************************************************
** Whether the rows of a table can be read: its backend module must have a query
** interface. A storage-only module (for example qbit-edi's preview download
** backend) throws from getQueryInterface(), and reading its table would fail the
** whole run, so such a table is never backed up. A backend whose module cannot
** be resolved cannot be read either (not cached: the module may register later).
*******************************************************************************/
public static boolean isQueryable(QInstance qInstance, QTableMetaData table)
{
if(qInstance == null || table == null || table.getBackendName() == null)
{
return (true);
}
QBackendMetaData backend = qInstance.getBackend(table.getBackendName());
if(backend == null || backend.getBackendType() == null)
{
return (true);
}
Boolean cached = QUERYABLE_BY_BACKEND_TYPE.get(backend.getBackendType());
if(cached != null)
{
return (cached);
}
QBackendModuleInterface module;
try
{
module = new QBackendModuleDispatcher().getQBackendModule(backend);
}
catch(QModuleDispatchException e)
{
LOG.debug("No backend module for a table, so it cannot be backed up", logPair("table", table.getName()), logPair("backendType", backend.getBackendType()));
return (false);
}
boolean queryable;
try
{
queryable = module.getQueryInterface() != null;
}
catch(IllegalStateException | UnsupportedOperationException e)
{
queryable = false;
}
QUERYABLE_BY_BACKEND_TYPE.put(backend.getBackendType(), queryable);
return (queryable);
}
/*******************************************************************************
** A table that is hidden and has a password or sensitive field (other than the
** keys and dates) is treated as a table of secrets.
@@ -29,6 +29,8 @@ import com.kingsrook.qbits.backups.manifest.BackupManifest;
import com.kingsrook.qbits.backups.model.BackupsTables;
import com.kingsrook.qbits.backups.scope.PlanSettings;
import com.kingsrook.qbits.backups.scope.ScopeResolver;
import com.kingsrook.qbits.backups.scope.TableRole;
import com.kingsrook.qbits.backups.scope.TableRoles;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.jobs.plans.JobChunkScope;
@@ -37,6 +39,11 @@ import com.kingsrook.qqq.backend.core.jobs.progress.JobLogLevel;
import com.kingsrook.qqq.backend.core.jobs.progress.JobPhase;
import com.kingsrook.qqq.backend.core.jobs.progress.JobProgress;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
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.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -230,6 +237,38 @@ class BackupEngineTest
/*******************************************************************************
** A table on a backend with no query interface (a storage-only module, like
** qbit-edi's edi_preview_download) is left out of the scope, even when it is
** selected by name, and the run succeeds. Before the fix the reader queried it
** and the run FAILED with "Query is not implemented in this module".
*******************************************************************************/
@Test
void testUnqueryableTableIsExcluded() throws Exception
{
QInstance qInstance = QContext.getQInstance();
qInstance.addBackend(new QBackendMetaData().withName("storageOnly").withBackendType(StorageOnlyBackendModule.class));
qInstance.addTable(new QTableMetaData().withName("previewDownload").withBackendName("storageOnly").withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER)));
BackupRunResult result = run(plan(Map.of()));
assertThat(result.getErrorMessage()).isNull();
assertThat(result.getStatus()).isEqualTo("SUCCEEDED_WITH_WARNINGS");
BackupManifest manifest = BackupManifest.read(BackupsTestUtils.BACKUPS.getBytes(result.getManifestKey()));
assertThat(manifest.table("previewDownload")).isNull();
assertThat(manifest.table("blogPost")).isNotNull();
assertThat(TableRoles.roleOf(qInstance, qInstance.getTable("previewDownload"))).isEqualTo(TableRole.EXTERNAL);
assertThat(TableRoles.isQueryable(qInstance, qInstance.getTable("blogPost"))).isTrue();
ScopeResolver.ResolvedScope scope = new ScopeResolver(qInstance).resolve(PlanSettings.adHoc().with("scopeType", "TABLES").with("includeRoles", "DATA,EXTERNAL"),
List.of(new ScopeResolver.ScopeRule(true, "TABLE", "previewDownload", null, false, null), new ScopeResolver.ScopeRule(true, "ROLE", "EXTERNAL", null, false, null)));
assertThat(scope.names()).doesNotContain("previewDownload");
assertThat(scope.excluded()).contains(new ScopeResolver.ExcludedTable("previewDownload", ScopeResolver.NOT_QUERYABLE));
}
/*******************************************************************************
** Required snapshots fail before anything is written (no backend can open one yet).
*******************************************************************************/
@@ -352,13 +391,14 @@ class BackupEngineTest
BackupRunRequest request = BackupRunFactory.forPlan(plan(Map.of()), "MANUAL").withProgress(progress)
.withReader((table, filter, start, size, transaction, sink) ->
{
throw (new com.kingsrook.qqq.backend.core.exceptions.QException("https://storage-user:storage-password@host/?secret=storage-secret accessKey=storage-access row=private-row"));
throw (new com.kingsrook.qqq.backend.core.exceptions.QException("https://storage-user:storage-password@host/?secret=storage-secret accessKey=storage-access Detail: Key (email)=(private-row) already exists."));
});
BackupRunResult result = new BackupEngine().execute(request);
assertThat(result.getStatus()).isEqualTo("FAILED");
assertThat(progress.logs).contains("ERROR:" + result.getErrorMessage());
assertThat(progress.getSnapshot().getPhases()).anyMatch(p -> p.getStatus() == JobPhase.Status.FAILED);
assertThat(String.join("\n", progress.logs) + result.getErrorMessage()).doesNotContain("storage-user", "storage-password", "storage-secret", "storage-access", "private-row", "https://");
assertThat(result.getErrorMessage()).startsWith("Backup failed: QException: https://[REDACTED]@host/");
assertThat(String.join("\n", progress.logs) + result.getErrorMessage() + result.getErrorDetail()).doesNotContain("storage-user", "storage-password", "storage-secret", "storage-access", "private-row");
}
@@ -408,7 +448,52 @@ class BackupEngineTest
BackupRunResult result = new BackupEngine().execute(BackupRunFactory.forPlan(plan(Map.of()), "MANUAL").withProgress(progress).withStorage(storage));
assertThat(result.getStatus()).isEqualTo("FAILED");
assertThat(progress.logs).contains("ERROR:" + result.getErrorMessage());
assertThat(String.join("\n", progress.logs) + result.getErrorMessage()).doesNotContain("endpoint-user", "endpoint-password", "access-secret", "secret-secret", "https://");
assertThat(String.join("\n", progress.logs) + result.getErrorMessage() + result.getErrorDetail()).doesNotContain("endpoint-user", "endpoint-password", "access-secret", "secret-secret");
}
/*******************************************************************************
** A failed put records what an operator needs on the run row, with no secrets:
** error_message is "Backup failed: Class: message" and error_detail has the
** class, the cause chain and the root cause frames; the presigned signature,
** the access key id and the URL user-info are redacted, the object key is kept.
*******************************************************************************/
@Test
void testFailedPutRecordsSanitizedMessageAndDetail() throws Exception
{
var storage = new com.kingsrook.qbits.backups.storage.InMemoryBackupStorage()
{
/*******************************************************************************
** A MinIO-style failure with a presigned URL in the message.
*******************************************************************************/
@Override
public java.io.OutputStream openWrite(String key, com.kingsrook.qbits.backups.storage.WriteOptions options)
{
throw (new com.kingsrook.qbits.backups.storage.BackupStorageException(com.kingsrook.qbits.backups.storage.BackupStorageException.Reason.ACCESS_DENIED,
"AccessDenied on PUT https://minio-user:minio-pass@minio.galaxy.lan/kof22-backups/" + key
+ "?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261016%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=deadbeefcafe0123",
new IOException("XMinioStorageFull: Storage backend has reached its minimum free drive threshold. Authorization: AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/x, Signature=feedface")));
}
};
BackupRunResult result = new BackupEngine().execute(BackupRunFactory.forPlan(plan(Map.of()), "MANUAL").withStorage(storage));
assertThat(result.getStatus()).isEqualTo("FAILED");
QRecord run = new BackupCatalog().get(BackupsTables.RUN, result.getRunId()).orElseThrow();
String message = run.getValueString("errorMessage");
String detail = run.getValueString("errorDetail");
assertThat(message).isEqualTo(result.getErrorMessage());
assertThat(detail).isEqualTo(result.getErrorDetail());
assertThat(message).startsWith("Backup failed: BackupStorageException: AccessDenied on PUT https://[REDACTED]@minio.galaxy.lan/kof22-backups/qqq-backups/testapp/nightly/")
.doesNotContain("see server logs");
assertThat(detail).startsWith("com.kingsrook.qbits.backups.storage.BackupStorageException: AccessDenied on PUT")
.contains("\nCaused by: java.io.IOException: XMinioStorageFull: Storage backend has reached its minimum free drive threshold.")
.contains("X-Amz-Signature=[REDACTED]")
.contains("Root cause stack (top ")
.contains("\tat ");
assertThat(detail.length()).isLessThanOrEqualTo(FailureText.MAX_DETAIL_CHARS);
assertThat(message + detail).doesNotContain("deadbeefcafe0123", "AKIAIOSFODNN7EXAMPLE", "minio-user", "minio-pass", "feedface");
}
@@ -0,0 +1,186 @@
/*
* 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.backups.run;
import java.io.IOException;
import java.sql.SQLException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Unit tests of the failure text sanitizer, summary and detail.
*******************************************************************************/
class FailureTextTest
{
/*******************************************************************************
** Null and plain text pass through; object keys and bucket names are kept.
*******************************************************************************/
@Test
void testPlainTextIsKept()
{
assertThat(FailureText.sanitize(null)).isNull();
String plain = "Query is not implemented in this module: PreviewDownloadBackend (bucket kof22-backups, key qqq-backups/kof22/nightly/2026/10/16/run/tables/a/part-00001.jsonl.gz)";
assertThat(FailureText.sanitize(plain)).isEqualTo(plain);
assertThat(FailureText.sanitize("objectKey=qqq-backups/x/y.gz bucket=kof22-backups")).isEqualTo("objectKey=qqq-backups/x/y.gz bucket=kof22-backups");
}
/*******************************************************************************
** URL user-info and presigned query parameters are redacted; host and path kept.
*******************************************************************************/
@Test
void testUrls()
{
assertThat(FailureText.sanitize("PUT https://user:pass@minio.lan/bucket/a/b.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2Fx&X-Amz-Signature=abc123&partNumber=2 failed"))
.isEqualTo("PUT https://[REDACTED]@minio.lan/bucket/a/b.gz?X-Amz-Algorithm=[REDACTED]&X-Amz-Credential=[REDACTED]&X-Amz-Signature=[REDACTED]&partNumber=2 failed");
assertThat(FailureText.sanitize("jdbc:postgresql://app:hunter2@db:5432/app")).isEqualTo("jdbc:postgresql://[REDACTED]@db:5432/app");
assertThat(FailureText.sanitize("https://h/p?Signature=s1&Expires=1&AWSAccessKeyId=k&access_token=t&key=v"))
.isEqualTo("https://h/p?Signature=[REDACTED]&Expires=1&AWSAccessKeyId=[REDACTED]&access_token=[REDACTED]&key=[REDACTED]");
}
/*******************************************************************************
** key=value and JSON secrets, Authorization and Bearer values.
*******************************************************************************/
@Test
void testAssignmentsAndHeaders()
{
assertThat(FailureText.sanitize("password=p1 secret=\"s 2\" token='t3' key=k4 accessKey=a5 secretKey=s6 apiKey=x7 db.password = p8, other=ok"))
.isEqualTo("password=[REDACTED] secret=[REDACTED] token=[REDACTED] key=[REDACTED] accessKey=[REDACTED] secretKey=[REDACTED] apiKey=[REDACTED] db.password = [REDACTED], other=ok");
assertThat(FailureText.sanitize("{\"clientSecret\": \"abc\", \"name\": \"keep\"}")).isEqualTo("{\"clientSecret\": \"[REDACTED]\", \"name\": \"keep\"}");
assertThat(FailureText.sanitize("Authorization: Bearer eyJhbGciOi.payload.sig\nnext line")).isEqualTo("Authorization: [REDACTED]\nnext line");
assertThat(FailureText.sanitize("sent Bearer abcdefghijk to the API")).isEqualTo("sent Bearer [REDACTED] to the API");
assertThat(FailureText.sanitize("id AKIAIOSFODNN7EXAMPLE and ASIAIOSFODNN7EXAMPLE")).isEqualTo("id [REDACTED] and [REDACTED]");
}
/*******************************************************************************
** PEM blocks (terminated or not) and Postgres row values.
*******************************************************************************/
@Test
void testPemAndRowValues()
{
assertThat(FailureText.sanitize("bad key -----BEGIN PRIVATE KEY-----\nMIIabc\n-----END PRIVATE KEY----- here")).isEqualTo("bad key [REDACTED PEM] here");
assertThat(FailureText.sanitize("cut -----BEGIN RSA PRIVATE KEY-----\nMIIabc")).isEqualTo("cut [REDACTED PEM]");
assertThat(FailureText.sanitize("duplicate key value violates unique constraint \"u_email\" Key (email)=(ada@example.com) already exists."))
.isEqualTo("duplicate key value violates unique constraint \"u_email\" Key (email)=([REDACTED]) already exists.");
assertThat(FailureText.sanitize("ERROR: null value in column \"x\"\n Detail: Failing row contains (1, ada, secret-row).\n Where: x"))
.isEqualTo("ERROR: null value in column \"x\"\n Detail: [REDACTED]\n Where: x");
}
/*******************************************************************************
** The summary is short, sanitized and bounded; no message falls back to the class.
*******************************************************************************/
@Test
void testSummary()
{
assertThat(FailureText.summary(new IllegalStateException("Query is not implemented in this module: PreviewDownloadBackend")))
.isEqualTo("Backup failed: IllegalStateException: Query is not implemented in this module: PreviewDownloadBackend");
assertThat(FailureText.summary(new NullPointerException())).isEqualTo("Backup failed: NullPointerException");
assertThat(FailureText.summary("Could not create the run record", new IOException(" password=x "))).isEqualTo("Could not create the run record: IOException: password=[REDACTED]");
assertThat(FailureText.summary(new IOException("x".repeat(2000)))).hasSize("Backup failed: IOException: ".length() + FailureText.MAX_SUMMARY_MESSAGE_CHARS + 3);
}
/*******************************************************************************
** The detail has the class, the cause chain and the root cause frames, bounded.
*******************************************************************************/
@Test
void testDetail()
{
SQLException root = new SQLException("Key (id)=(4242xyz) already exists.");
IOException middle = new IOException("write failed password=pwsecret", root);
RuntimeException top = new RuntimeException("Backup of t failed", middle);
String detail = FailureText.detail(top);
assertThat(detail).startsWith("java.lang.RuntimeException: Backup of t failed\nCaused by: java.io.IOException: write failed password=[REDACTED]\nCaused by: java.sql.SQLException: Key (id)=([REDACTED]) already exists.\nRoot cause stack (top ");
assertThat(detail).contains("\tat com.kingsrook.qbits.backups.run.FailureTextTest.testDetail(FailureTextTest.java:");
assertThat(detail.split("\n\tat ")).hasSize(FailureText.ROOT_CAUSE_FRAMES + 1);
assertThat(detail).doesNotContain("4242xyz", "pwsecret");
IOException noFrames = new IOException((String) null);
noFrames.setStackTrace(new StackTraceElement[0]);
assertThat(FailureText.detail(noFrames)).isEqualTo("java.io.IOException");
String huge = FailureText.detail(new IOException("y".repeat(20_000)));
assertThat(huge).hasSize(FailureText.MAX_DETAIL_CHARS).endsWith("... (truncated)");
}
/*******************************************************************************
** A cause cycle does not loop.
*******************************************************************************/
@Test
void testCauseCycle()
{
CyclicException a = new CyclicException("a");
CyclicException b = new CyclicException("b");
a.setCause(b);
b.setCause(a);
assertThat(FailureText.detail(a)).startsWith("com.kingsrook.qbits.backups.run.FailureTextTest$CyclicException: a\nCaused by: com.kingsrook.qbits.backups.run.FailureTextTest$CyclicException: b\nRoot cause stack");
}
/***************************************************************************
** An exception whose cause can be set to form a cycle.
***************************************************************************/
private static class CyclicException extends Exception
{
private Throwable cause;
/*******************************************************************************
** Constructor.
*******************************************************************************/
CyclicException(String message)
{
super(message);
}
/*******************************************************************************
** Set the cause.
*******************************************************************************/
void setCause(Throwable cause)
{
this.cause = cause;
}
/*******************************************************************************
** The cause.
*******************************************************************************/
@Override
public synchronized Throwable getCause()
{
return (cause);
}
}
}
@@ -0,0 +1,86 @@
/*
* 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.backups.run;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import com.kingsrook.qqq.backend.core.actions.interfaces.QStorageInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
/*******************************************************************************
** A storage-only backend module (like qbit-edi's PreviewDownloadBackend): it
** streams objects but has no query interface, so a table on it cannot be read.
*******************************************************************************/
public class StorageOnlyBackendModule implements QBackendModuleInterface, QStorageInterface
{
public static final String TYPE = "test-storage-only";
static
{
QBackendModuleDispatcher.registerBackendModule(new StorageOnlyBackendModule());
}
/*******************************************************************************
** The backend type.
*******************************************************************************/
@Override
public String getBackendType()
{
return (TYPE);
}
/*******************************************************************************
** Storage only.
*******************************************************************************/
@Override
public QStorageInterface getStorageInterface()
{
return (this);
}
/*******************************************************************************
** Nothing is stored.
*******************************************************************************/
@Override
public InputStream getInputStream(StorageInput input)
{
return (new ByteArrayInputStream(new byte[0]));
}
/*******************************************************************************
** Writes are discarded.
*******************************************************************************/
@Override
public OutputStream createOutputStream(StorageInput input)
{
return (new ByteArrayOutputStream());
}
}