Compare commits

..
Author SHA1 Message Date
james.maesandCodex cec8b38f34 ci: centralize branch verification and source scanning
Co-Authored-By: Codex <noreply@openai.com>
2026-10-21 12:17:25 -05:00
james.maes 3d81e31279 Merge pull request #7 from QRun-IO/feature/ga-only-release-tags
ci: publish only GA tags (vX.Y.Z), never RC tags
2026-10-12 16:14:41 -05:00
james.maesandClaude Opus 5.5 615a708c06 ci: publish only GA tags (vX.Y.Z), never RC tags
An RC tag matched /v.*/ and the orb republished it as the next RC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:00:38 -05:00
james.maes e098c623c0 chore: bump version to 1.6.1-SNAPSHOT, qqq 0.41.0-SNAPSHOT 2026-03-29 17:43:17 -05:00
james.maes e9d36a7b9f chore: merge release/1.6.0 back to develop 2026-03-29 17:42:43 -05:00
33 changed files with 702 additions and 2167 deletions

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.7.11
qqq-orb: kingsrook/qqq-orb@0.7.15
workflows:
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: 0.7.11
orb_version: 0.7.15
filters:
branches:
ignore: automation/qbit-rc-pending
-1
View File
@@ -1 +0,0 @@
* @KofTwentyTwo
+31 -31
View File
@@ -1,31 +1,31 @@
# GitHub Release Configuration
# Auto-generates release notes from PR labels
changelog:
exclude:
labels:
- ignore-for-release
- duplicate
- invalid
authors:
- dependabot
- dependabot[bot]
categories:
- title: Breaking Changes
labels:
- breaking-change
- breaking
- title: New Features
labels:
- enhancement
- feature
- title: Bug Fixes
labels:
- bug
- fix
- title: Dependencies
labels:
- dependencies
- title: Other Changes
labels:
- "*"
# GitHub Release Configuration
# Auto-generates release notes from PR labels
changelog:
exclude:
labels:
- ignore-for-release
- duplicate
- invalid
authors:
- dependabot
- dependabot[bot]
categories:
- title: Breaking Changes
labels:
- breaking-change
- breaking
- title: New Features
labels:
- enhancement
- feature
- title: Bug Fixes
labels:
- bug
- fix
- title: Dependencies
labels:
- dependencies
- title: Other Changes
labels:
- "*"
+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
+101 -101
View File
@@ -1,101 +1,101 @@
### Maven template
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
# Eclipse m2e generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.idea/material_theme_project_new.xml
.idea/workspace.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Maven template
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
# Eclipse m2e generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.idea/material_theme_project_new.xml
.idea/workspace.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
+3 -3
View File
@@ -1,5 +1,5 @@
profile: qbit
orb_version: 0.7.11
orb_version: 0.7.15
default_branch: main
java_version: '21'
browsers: true
@@ -7,10 +7,10 @@ api_checks: false
docs_only: false
release_on_main: false
release_tag_policy: stable
test_branch_policy: non-release-no-integration
test_branch_policy: feature-no-hotfix
publish:
snapshot: true
release_candidate: true
release: true
hotfix: true
asciidoc: false
release_completion: false
+68 -84
View File
@@ -1,84 +1,68 @@
# Changelog
All notable changes to QBit Build Parent POM will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- G1 default API packages include qbit-assistant's published provider SPI, `com.kingsrook.qbits.assistant.spi`; artifact `com.kingsrook.qbits:qbit-assistant-provider-api` is already covered by `com.kingsrook.qbits:*-api`. Assistant implementation packages remain forbidden.
- G1 default allow-lists include qbit-email's published API: artifact `com.kingsrook.qbits:qbit-email` and package `com.kingsrook.qbits.email.api` (as for qbit-object-storage), so consumers need no per-repo allow-list.
## [2.1.0] - 2026-10-13
### Added
- Independent `qbit-guardrails` JAR with inheritable Jupiter architecture, extension-contract and stand-in expiry checks (G1, G3, G4).
- Report-first Enforcer checks for cross-qbit implementation dependencies and first-party SNAPSHOT dependencies (G1, G7).
- Opt-in enforcement, API/artifact allow-lists, profile skip and strict UI API route forwarding (G2).
- Fixture tests and rollout/configuration documentation. Existing qbits receive warnings by default.
### Changed
- Parent revision is 2.1.0; the repository builds and publishes both artifacts as a reactor.
- Inherited Surefire is pinned to 3.5.6 and Enforcer to 3.6.3, with merging child configuration.
## [1.5.1] - 2025-12-28
### Fixed
- Fixed JUnit BOM version mismatch: updated from 5.12.1 to 6.0.1 to align with JUnit Jupiter 6.0.1 from qqq-bom-pom 0.35.0
## [1.5.0] - 2025-12-28
### Changed
- Migrated to Java 21 LTS (from Java 17)
- Updated qqq-bom-pom dependency to 0.35.0 (from 0.27.9)
- Updated JUnit BOM to 5.12.1 (for JUnit Jupiter 6.0.1 support)
- Updated qqq-orb to 0.5.0 (from 0.3.7)
## [1.0.0]
### Added
- Comprehensive README documentation for parent POM
- Standard GitHub files (LICENSE, CHANGELOG, CONTRIBUTING, SECURITY, CODE_OF_CONDUCT)
- Proper project structure and documentation
- Initial parent POM implementation for QBit projects
- Centralized dependency management with QQQ BOM (0.27.9)
- JUnit 5 BOM integration (5.10.0)
- Maven plugin management for compiler, source, javadoc, GPG, and central publishing
- Release profile for Maven Central publishing
- GPG signing configuration for artifact security
- Sonatype Central publishing plugin integration
- Java 17+ configuration with UTF-8 encoding
- Standardized build properties and plugin versions
### Changed
- Updated README to reflect parent POM purpose and functionality
- Improved project documentation and structure
- Enhanced GitHub repository setup
- Initial public release of QBit Build Parent POM
- Established standardized build configuration for all QBit projects
- Created centralized dependency management approach
---
## 📚 For Detailed Information
**📖 [Complete Documentation Wiki](https://github.com/Kingsrook/qqq/wiki)** - Start here for comprehensive guides
- **[🏠 Home](https://github.com/Kingsrook/qqq/wiki/Home)** - Project overview and quick start
- **[🚀 Release Flow](https://github.com/Kingsrook/qqq/wiki/Release-Flow)** - Detailed release process
- **[🏷️ Changelog & Tagging](https://github.com/Kingsrook/qqq/wiki/Changelog-and-Tagging)** - Commit conventions and release notes
- **[🔧 Developer Onboarding](https://github.com/Kingsrook/qqq/wiki/Developer-Onboarding)** - Setup and contribution guide
## 🔄 Version Compatibility
QBit Build Parent POM follows semantic versioning:
- **MAJOR** versions may contain breaking changes to build configuration
- **MINOR** versions add new functionality (backward compatible)
- **PATCH** versions contain bug fixes (backward compatible)
For detailed compatibility information, see [QQQ Compatibility Matrix](https://github.com/Kingsrook/qqq/wiki/Compatibility-Matrix).
---
**Thank you for using QBit Build Parent POM!** 🚀
# Changelog
All notable changes to QBit Build Parent POM will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.5.1] - 2025-12-28
### Fixed
- Fixed JUnit BOM version mismatch: updated from 5.12.1 to 6.0.1 to align with JUnit Jupiter 6.0.1 from qqq-bom-pom 0.35.0
## [1.5.0] - 2025-12-28
### Changed
- Migrated to Java 21 LTS (from Java 17)
- Updated qqq-bom-pom dependency to 0.35.0 (from 0.27.9)
- Updated JUnit BOM to 5.12.1 (for JUnit Jupiter 6.0.1 support)
- Updated qqq-orb to 0.5.0 (from 0.3.7)
## [1.0.0]
### Added
- Comprehensive README documentation for parent POM
- Standard GitHub files (LICENSE, CHANGELOG, CONTRIBUTING, SECURITY, CODE_OF_CONDUCT)
- Proper project structure and documentation
- Initial parent POM implementation for QBit projects
- Centralized dependency management with QQQ BOM (0.27.9)
- JUnit 5 BOM integration (5.10.0)
- Maven plugin management for compiler, source, javadoc, GPG, and central publishing
- Release profile for Maven Central publishing
- GPG signing configuration for artifact security
- Sonatype Central publishing plugin integration
- Java 17+ configuration with UTF-8 encoding
- Standardized build properties and plugin versions
### Changed
- Updated README to reflect parent POM purpose and functionality
- Improved project documentation and structure
- Enhanced GitHub repository setup
- Initial public release of QBit Build Parent POM
- Established standardized build configuration for all QBit projects
- Created centralized dependency management approach
---
## 📚 For Detailed Information
**📖 [Complete Documentation Wiki](https://github.com/Kingsrook/qqq/wiki)** - Start here for comprehensive guides
- **[🏠 Home](https://github.com/Kingsrook/qqq/wiki/Home)** - Project overview and quick start
- **[🚀 Release Flow](https://github.com/Kingsrook/qqq/wiki/Release-Flow)** - Detailed release process
- **[🏷️ Changelog & Tagging](https://github.com/Kingsrook/qqq/wiki/Changelog-and-Tagging)** - Commit conventions and release notes
- **[🔧 Developer Onboarding](https://github.com/Kingsrook/qqq/wiki/Developer-Onboarding)** - Setup and contribution guide
## 🔄 Version Compatibility
QBit Build Parent POM follows semantic versioning:
- **MAJOR** versions may contain breaking changes to build configuration
- **MINOR** versions add new functionality (backward compatible)
- **PATCH** versions contain bug fixes (backward compatible)
For detailed compatibility information, see [QQQ Compatibility Matrix](https://github.com/Kingsrook/qqq/wiki/Compatibility-Matrix).
---
**Thank you for using QBit Build Parent POM!** 🚀
-20
View File
@@ -1,20 +0,0 @@
# CLAUDE.md — qbit-bom
## Knowledge base
Reviewed dossiers for this repo and the wider QQQ platform live in the second-brain vault:
- Hub / start here: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/qqq-hub.md`
- This repo's dossier: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/repos/qbit-bom.md`
(reviewed at commit `dd9a67723a7d`, branch `main`, 2026-07-04)
Key orientation facts (details and caveats in the dossier):
- Despite the repo name and README, this repo publishes `com.kingsrook:qbit-build-parent` —
the shared Maven parent POM for the qbit repos. It is NOT a qbit-version BOM, and the
README's `io.qrun:qbit-bom` coordinates do not exist on Maven Central.
- v1.6.0 imports `com.kingsrook.qqq:qqq-bom-pom:0.40.0` (Java 21, junit-bom 6.0.1);
develop pins `0.41.0-SNAPSHOT`, which no longer resolves (qqq renumbered to 4.0.x).
- First-party license declarations in the current source are Apache-2.0, consistent
with LICENSE and NOTICE. Previously published artifacts retain their original
metadata; publish a new version for declaration corrections.
+4 -4
View File
@@ -1,4 +1,4 @@
# Code of Conduct
See Project level [Code of Conduct](https://github.com/Kingsrook/qqq/blob/develop/CODE_OF_CONDUCT.md)
# Code of Conduct
See Project level [Code of Conduct](https://github.com/Kingsrook/qqq/blob/develop/CODE_OF_CONDUCT.md)
+3 -3
View File
@@ -1,3 +1,3 @@
# Code Style Guide
* See Project [Code Style Guide](https://github.com/Kingsrook/qqq/blob/develop/CODE_STYLE.md)
# Code Style Guide
* See Project [Code Style Guide](https://github.com/Kingsrook/qqq/blob/develop/CODE_STYLE.md)
+4 -4
View File
@@ -1,4 +1,4 @@
# Contributing
* See Project [Contributing](https://github.com/Kingsrook/qqq/blob/develop/CONTRIBUTING.md)
# Contributing
* See Project [Contributing](https://github.com/Kingsrook/qqq/blob/develop/CONTRIBUTING.md)
+191 -191
View File
@@ -1,191 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright
owner or by an individual or Legal Entity authorized to submit on
behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written
communication sent to the Licensor or its representatives, including
but not limited to communication on electronic mailing lists, source
code control systems, and issue tracking systems that are managed by,
or on behalf of, the Licensor for the purpose of discussing and
improving the Work, but excluding communication that is conspicuously
marked or otherwise designated in writing by the copyright owner as
"Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2022-2025 QRunIO, 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.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright
owner or by an individual or Legal Entity authorized to submit on
behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written
communication sent to the Licensor or its representatives, including
but not limited to communication on electronic mailing lists, source
code control systems, and issue tracking systems that are managed by,
or on behalf of, the Licensor for the purpose of discussing and
improving the Work, but excluding communication that is conspicuously
marked or otherwise designated in writing by the copyright owner as
"Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2022-2025 QRunIO, 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.
+6 -6
View File
@@ -1,6 +1,6 @@
qBit BOM (Bill of Materials)
Copyright 2022-2025 QRunIO, LLC
This product includes software developed at QRunIO, LLC (https://qrun.io/).
Licensed under the Apache License, Version 2.0.
qBit BOM (Bill of Materials)
Copyright 2022-2025 QRunIO, LLC
This product includes software developed at QRunIO, LLC (https://qrun.io/).
Licensed under the Apache License, Version 2.0.
+94 -127
View File
@@ -1,127 +1,94 @@
# QBit Build Parent
This repository publishes the Maven parent **`com.kingsrook:qbit-build-parent`** and the **`com.kingsrook:qbit-guardrails`** test JAR. Version **2.1.0** adds report-first architecture checks, retains the **`com.kingsrook.qqq:qqq-bom-pom:4.0.0`** baseline, sets Java 21 compilation defaults, and provides shared release plugin configuration. Qbits using QQQ 4.1 release candidates import their chosen `qqq-bom-pom:4.1.0-RC.x` in their own dependency management.
## Use the parent
```xml
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.1.0</version>
<relativePath/>
</parent>
<groupId>com.example.qbits</groupId>
<artifactId>my-qbit</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
</dependency>
</dependencies>
```
Requires Java 21 and Maven 3.8 or later. Resolve releases from Maven Central. The imported QQQ BOM supplies framework dependency versions; **production QBit dependency versions are still chosen by each application**. This parent does not certify the production QBit fleet as compatible with QQQ 4.0.
The previous parent release, 1.6.0, imports QQQ 0.40.0. Moving to parent 2.0.0 therefore includes QQQ's breaking API changes; follow the [4.0 migration guide](https://github.com/QRun-IO/qqq/blob/main/docs/migration/4.0.adoc).
## Release workflow
The parent can be published only after its imported final QQQ BOM is available. Run `mvn clean verify` and test consumption from a separate child project before tagging. The existing CircleCI release workflow publishes a final version tag through the `release` Maven profile; a `release/*` branch produces a release candidate.
Both reactor projects have independent release profiles. Publish them together with the same revision; for an alternate release revision also set `-Dqbit.guardrails.version=<revision>` so the parent's dependency points at that release. The guardrail module deliberately has no parent, avoiding a dependency cycle. Its production dependencies are ArchUnit core 1.5.1, JUnit Jupiter API 6.0.1 and Maven artifact 3.9.11; it does not depend on QQQ. The inherited plugins are Surefire 3.5.6 and Enforcer 3.6.3.
## Architecture guardrails
All checks **report by default**: Enforcer logs warnings, and the inherited Jupiter tests print summaries such as `[qbit-guardrails] G1-arch FAIL (report-only): 2 violations` with details. Report-only tests pass without JUnit assumptions or skipped-test results. Existing violations therefore do not fail an upgrading qbit's build.
Opt into enforcement in a qbit's POM (or pass `-Dqbit.guardrails.enforce=true`):
```xml
<properties>
<qbit.guardrails.enforce>true</qbit.guardrails.enforce>
<qbit.guardrails.strictUiApiRoutes>true</qbit.guardrails.strictUiApiRoutes>
</properties>
```
| Rule | Check |
| --- | --- |
| G1-deps | Enforcer bans cross-qbit dependencies in compile, runtime, provided and system scopes, including transitives, except allow-listed artifacts. Test scope is allowed. |
| G1-arch | ArchUnit checks the qbit's main output for foreign qbit implementation references. Own packages, QQQ, JDK and third-party packages are allowed. |
| G1-literals | Java string literals beginning with another qbit's class/package name obey the same API boundary. Diagnostics include file and line. Comments and character literals are ignored; dynamic string construction is outside this check. |
| G2 | `qbit.guardrails.strictUiApiRoutes=true` forwards `qqq.uiApi.strictRoutes=true` to tests. QQQ 4.1.0-RC.6+ rejects raw UI API routes at server startup. Harmless on older core versions. |
| G3 | A main class referencing `MiddlewareSpecExtension` requires at least one `src/main/resources/openapi/qqq-v1/*.yaml` and a compiled test class referencing `UiApiContract` or `UiApiContractAssertions`. In a Git checkout the YAML must exist in HEAD; source archives are checked for source presence. |
| G4 | Malformed or expired `STAND-IN` markers in main/test Java sources are violations. Untagged stand-in prose is advisory. |
| G7 | Enforcer rejects SNAPSHOT dependencies in `com.kingsrook.qqq` and `com.kingsrook.qbits`, including transitives and test dependencies. Other groups, the project's own SNAPSHOT revision and its parent are not banned. |
Main classes that call Javalin route registration methods and contain string literals starting `/qqq/v1/` are listed as **raw /qqq/v1 routes**. These static findings are always warnings, even with enforcement enabled. Runtime G2 checking provides the authoritative route audit. Qbit CI should enable G2 now; the planned default flip is with core **4.1.0-RC.7**. This parent defaults `qbit.guardrails.strictUiApiRoutes` to `false` for the transition.
Configuration properties:
| Property | Default / meaning |
| --- | --- |
| `qbit.guardrails.enforce` | `false`; fail on G1, G3, G4 and G7 violations when `true`. |
| `qbit.guardrails.allowedQbitArtifacts` | Comma-separated Maven `groupId:artifactId` patterns, with `*` wildcards. Default: `com.kingsrook.qbits:qbit-object-storage,com.kingsrook.qbits:qbit-email,com.kingsrook.qbits:*-api`. Replacing this property replaces the allow-list. |
| `qbit.guardrails.allowedApiPackages` | Comma-separated package prefixes. Default: `com.kingsrook.qbits.objectstorage.api,com.kingsrook.qbits.email.api,com.kingsrook.qbits.assistant.spi`. A trailing `..` is accepted. Package boundaries are exact (`apiInternal` is not `api`). |
| `qbit.guardrails.ownPackage` | Optional JVM/CLI property: fourth segment (e.g. `alerts`) or full `com.kingsrook.qbits.alerts` prefix. Otherwise the most frequent fourth segment among main classes is used, with lexical tie-breaking. |
| `qbit.guardrails.strictUiApiRoutes` | `false`; opt into core's strict runtime route audit in tests. |
| `qbit.guardrails.skip` | Unset. `-Dqbit.guardrails.skip=true` disables the entire profile. Activation is based on property absence, so even setting it to `false` disables the profile; leave it unset to enable checks. |
Allowed `*-api` JARs automatically add their actual `com.kingsrook.qbits.<x>.api..` packages, identified using Maven metadata and class entries in the test classpath. An artifact exemption alone does **not** expose its implementation packages: object storage is exempt at the dependency level, while only its configured API package is allowed in code. Test-only qbit implementation dependencies are legal Maven dependencies but still cannot be referenced from main classes.
The parent adds the test JAR and merges Surefire `dependenciesToScan` and system properties through plugin management. Child Surefire settings are preserved; do not replace inherited configuration with `combine.self="override"`. Custom child includes/excludes or skipped tests can prevent discovery; use names matching `*GuardrailTest`. Missing/empty main output passes gracefully, including POM-only children. To run only the inherited tests:
```shell
mvn -B test -DskipTests=false -Dqbit.guardrails.enforce=true -Dtest='*GuardrailTest' -Dsurefire.failIfNoSpecifiedTests=false
```
Enforcer runs at `validate`, with transitive and direct passes because Enforcer 3.6.3's transitive resolver omits optional direct dependencies. The direct pass covers these too. If Enforcer blocks compilation during diagnosis, first record its findings, then use `-Denforcer.skip=true` with the test command to see the independent architecture/source results. That diagnostic command is not a full passing build.
### Stand-in marker grammar
Place each marker on a single Java source line, normally in a comment:
```java
// STAND-IN(core=QRun-IO/qqq#1234, removeBy=4.1.0-RC.6)
// STAND-IN(core=QRun-IO/qbit-x#42, removeBy=4.2.0)
```
The exact case-sensitive grammar is `STAND-IN(core=OWNER/REPO#ISSUE, removeBy=VERSION)`: no whitespace except one space after the comma. OWNER and REPO contain letters, digits, `_`, `.` or `-`; ISSUE is a positive integer; VERSION is `MAJOR.MINOR.PATCH` with optional dot/dash-separated alphanumeric qualifiers (e.g. `-RC.6`). Every marker is checked, including multiple markers on one line. A marker-shaped `stand-in(...)` or `standin(...)` with wrong syntax/case is malformed. Ordinary `stand-in`/`standin` prose without a marker produces only a tagging warning.
The running core version is read from `META-INF/maven/com.kingsrook.qqq/qqq-backend-core/pom.properties`, falling back to `-Dqqq.version=...`. Maven `ComparableVersion` determines expiry: `4.1.0-RC.5 < 4.1.0-RC.6 < 4.1.0`; equality is expired. A marker naming another repository still uses the **QQQ core milestone** for `removeBy`. If no core version is available, valid markers warn that expiry could not be evaluated; malformed markers still violate G4.
## License
### Packaged notices
The packaging change in this source tree copies each consumer's own root
`LICENSE` and `NOTICE` into `META-INF` in main/test archives and Maven shared
archive resources used by source/Javadoc archives. Existing main/test resources
remain included. Consumers must adopt a newly published parent version to receive
this change; the existing public 2.0.0 artifact is unchanged.
The default `qbit.noticesDirectory` is `${project.basedir}`. In a nested module
whose files live at the repository root, set it to `${project.basedir}/..` (adjust
for the actual layout). Do not point it at the build-parent checkout: these are
the consuming project's notices. If a child overrides `<resources>` or
`<testResources>`, retain the inherited shared archive resource directory.
After packaging the consumer, explicitly check every produced archive using
Python 3 and the guard from this checkout:
```bash
mvn -Prelease -DskipTests -Dgpg.skip=true clean package
python3 /path/to/qbit-bom/scripts/check-packaged-notices.py --root . target/*.jar
```
For a multi-module consumer, pass each module's actual JAR paths and its repository
root. The guard rejects missing archives, missing/duplicate entries and changed
notice bytes. Packaging-only checks skip runtime tests; existing tests, review
and release requirements still apply. The publishing workflow does not invoke
this guard automatically.
Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
# qbit-bom
Maven Bill of Materials for QBit dependencies.
**For:** QQQ developers managing multiple QBit dependencies
**Status:** Stable
## Why This Exists
QBits have interdependencies and version requirements. Managing compatible versions across qbit-user-role-permissions, qbit-workflows, qbit-webhooks, and others becomes tedious. Version mismatches cause runtime errors.
This BOM (Bill of Materials) declares compatible versions for all QBits. Import it once and omit version numbers from individual QBit dependencies.
## Features
- **Version Management** - Single source of truth for QBit versions
- **Compatibility Guarantee** - All declared versions tested together
- **Simplified POMs** - No version numbers on individual dependencies
- **Transitive Alignment** - Consistent versions for shared dependencies
## Quick Start
### Import the BOM
Add to your `pom.xml` in the `<dependencyManagement>` section:
```xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.qrun</groupId>
<artifactId>qbit-bom</artifactId>
<version>1.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```
### Add QBits Without Versions
```xml
<dependencies>
<dependency>
<groupId>io.qrun</groupId>
<artifactId>qbit-user-role-permissions</artifactId>
<!-- version managed by BOM -->
</dependency>
<dependency>
<groupId>io.qrun</groupId>
<artifactId>qbit-workflows</artifactId>
</dependency>
<dependency>
<groupId>io.qrun</groupId>
<artifactId>qbit-webhooks</artifactId>
</dependency>
</dependencies>
```
## Included QBits
| QBit | Current Version |
|------|-----------------|
| qbit-user-role-permissions | 0.3.0 |
| qbit-workflows | 0.2.0 |
| qbit-webhooks | 0.2.0 |
| qbit-sftp-data-integration | 0.2.0 |
| qbit-standard-process-trace | 0.1.0 |
| qbit-customizable-table-views | 0.2.0 |
## Compatibility
| BOM Version | QQQ Core Version | Java |
|-------------|------------------|------|
| 1.5.x | 0.35.0+ | 21 |
| 1.4.x | 0.27.0 - 0.34.x | 17 |
| 0.3.x | 0.25.0 - 0.26.x | 17 |
| 0.2.x | 0.22.0 - 0.24.x | 17 |
| 0.1.x | 0.20.0 - 0.21.x | 17 |
## Project Status
Stable. Updated with each QBit release.
## Contributing
1. Fork the repository
2. Update versions in `pom.xml`
3. Submit a pull request
## License
Proprietary - QRun.IO
+3 -3
View File
@@ -1,3 +1,3 @@
# Security Policy
* See Project [Security Policy](https://github.com/Kingsrook/qqq/blob/develop/SECURITY.md)
# Security Policy
* See Project [Security Policy](https://github.com/Kingsrook/qqq/blob/develop/SECURITY.md)
+169 -329
View File
@@ -1,329 +1,169 @@
<?xml version="1.0" encoding="UTF-8"?>
<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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<modules>
<module>qbit-guardrails</module>
</modules>
<name>QRunIO QBit Build Parent</name>
<description>Central build configuration and publishing settings for all QRunIO QBit projects published via our CICD pipelines.</description>
<url>https://github.com/Kingsrook/qbit-bom</url>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
<distribution>repo</distribution>
</license>
</licenses>
<scm>
<connection>scm:git:https://github.com/Kingsrook/qbit-bom.git</connection>
<developerConnection>scm:git:ssh://git@github.com/Kingsrook/qbit-bom.git</developerConnection>
<url>https://github.com/Kingsrook/qbit-bom</url>
<tag>HEAD</tag>
</scm>
<developers>
<developer>
<id>kingsrook</id>
<name>Kingsrook, LLC</name>
<email>pub@qrun.io</email>
<organization>Kingsrook, LLC</organization>
<organizationUrl>https://qrun.io</organizationUrl>
</developer>
</developers>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Multi-module consumers override this in modules whose notices live above basedir. -->
<qbit.noticesDirectory>${project.basedir}</qbit.noticesDirectory>
<!-- use revision property in children -->
<revision>2.1.2</revision>
<!-- Do not use revision here: children override it with their own version. -->
<qbit.guardrails.version>2.1.0</qbit.guardrails.version>
<qbit.guardrails.enforce>false</qbit.guardrails.enforce>
<qbit.guardrails.allowedQbitArtifacts>com.kingsrook.qbits:qbit-object-storage,com.kingsrook.qbits:qbit-email,com.kingsrook.qbits:*-api</qbit.guardrails.allowedQbitArtifacts>
<qbit.guardrails.allowedApiPackages>com.kingsrook.qbits.objectstorage.api,com.kingsrook.qbits.email.api,com.kingsrook.qbits.assistant.spi</qbit.guardrails.allowedApiPackages>
<qbit.guardrails.strictUiApiRoutes>false</qbit.guardrails.strictUiApiRoutes>
</properties>
<!-- Common dependency BOMs -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>4.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>src/test/resources</directory>
</testResource>
<testResource>
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
</testResource>
</testResources>
<plugins>
<!-- Source and Javadoc archives also consume Maven's shared archive resources. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<id>package-root-notices</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/maven-shared-archive-resources/META-INF</outputDirectory>
<resources>
<resource>
<directory>${qbit.noticesDirectory}</directory>
<filtering>false</filtering>
<includes>
<include>LICENSE</include>
<include>NOTICE</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<!-- Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<!-- Add other shared plugin configs here if desired -->
</plugins>
</pluginManagement>
</build>
<profiles>
<profile>
<id>qbit-guardrails</id>
<activation>
<property><name>!qbit.guardrails.skip</name></property>
</activation>
<dependencies>
<dependency>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-guardrails</artifactId>
<version>${qbit.guardrails.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.6</version>
<configuration>
<dependenciesToScan combine.children="append">
<dependency>com.kingsrook:qbit-guardrails</dependency>
</dependenciesToScan>
<systemPropertyVariables>
<qbit.guardrails.basedir>${project.basedir}</qbit.guardrails.basedir>
<qbit.guardrails.outputDirectory>${project.build.outputDirectory}</qbit.guardrails.outputDirectory>
<qbit.guardrails.testOutputDirectory>${project.build.testOutputDirectory}</qbit.guardrails.testOutputDirectory>
<qbit.guardrails.enforce>${qbit.guardrails.enforce}</qbit.guardrails.enforce>
<qbit.guardrails.allowedQbitArtifacts>${qbit.guardrails.allowedQbitArtifacts}</qbit.guardrails.allowedQbitArtifacts>
<qbit.guardrails.allowedApiPackages>${qbit.guardrails.allowedApiPackages}</qbit.guardrails.allowedApiPackages>
<qqq.uiApi.strictRoutes>${qbit.guardrails.strictUiApiRoutes}</qqq.uiApi.strictRoutes>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.6.3</version>
<configuration>
<fail>${qbit.guardrails.enforce}</fail>
<failFast>false</failFast>
<rules>
<bannedDependencies>
<message>[qbit-guardrails] G1-deps: cross-qbit implementation dependencies</message>
<searchTransitive>true</searchTransitive>
<excludes>
<exclude>com.kingsrook.qbits:*:*:*:compile</exclude>
<exclude>com.kingsrook.qbits:*:*:*:runtime</exclude>
<exclude>com.kingsrook.qbits:*:*:*:provided</exclude>
<exclude>com.kingsrook.qbits:*:*:*:system</exclude>
</excludes>
<includes>${qbit.guardrails.allowedQbitArtifacts}</includes>
</bannedDependencies>
<requireReleaseDeps>
<message>[qbit-guardrails] G7: QQQ and qbit dependencies must not be SNAPSHOTs</message>
<onlyWhenRelease>false</onlyWhenRelease>
<failWhenParentIsSnapshot>false</failWhenParentIsSnapshot>
<searchTransitive>true</searchTransitive>
<!-- Includes re-enable checks within exclusions in Enforcer 3.6.3. -->
<excludes><exclude>*:*</exclude></excludes>
<includes>
<include>com.kingsrook.qqq:*</include>
<include>com.kingsrook.qbits:*</include>
</includes>
</requireReleaseDeps>
</rules>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>qbit-architecture-guardrails</id>
<phase>validate</phase>
<goals><goal>enforce</goal></goals>
</execution>
<!-- Enforcer's transitive resolver omits optional direct dependencies. -->
<execution>
<id>qbit-direct-architecture-guardrails</id>
<phase>validate</phase>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<bannedDependencies><searchTransitive>false</searchTransitive></bannedDependencies>
<requireReleaseDeps><searchTransitive>false</searchTransitive></requireReleaseDeps>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<!-- Release profile for Maven Central -->
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals><goal>jar</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.2</version>
<configuration>
<failOnError>false</failOnError>
<doclint>none</doclint>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals><goal>jar</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<keyname>${gpg.keyname}</keyname>
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals><goal>sign</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.8.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>
<autoPublish>true</autoPublish>
<waitUntil>uploaded</waitUntil>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<repositories>
<repository>
<id>central-portal-snapshots</id>
<name>Central Portal Snapshots</name>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<name>QRunIO QBit Build Parent</name>
<description>Central build configuration and publishing settings for all QRunIO QBit projects published via our CICD pipelines.</description>
<url>https://github.com/Kingsrook/qbit-bom</url>
<licenses>
<license>
<name>GNU Affero General Public License v3.0</name>
<url>https://www.gnu.org/licenses/agpl-3.0-standalone.html</url>
<distribution>repo</distribution>
</license>
</licenses>
<scm>
<connection>scm:git:https://github.com/Kingsrook/qbit-bom.git</connection>
<developerConnection>scm:git:ssh://git@github.com/Kingsrook/qbit-bom.git</developerConnection>
<url>https://github.com/Kingsrook/qbit-bom</url>
<tag>HEAD</tag>
</scm>
<developers>
<developer>
<id>kingsrook</id>
<name>Kingsrook, LLC</name>
<email>pub@qrun.io</email>
<organization>Kingsrook, LLC</organization>
<organizationUrl>https://qrun.io</organizationUrl>
</developer>
</developers>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- use revision property in children -->
<revision>1.6.1-SNAPSHOT</revision>
</properties>
<!-- Common dependency BOMs -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>4.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<!-- Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<!-- Add other shared plugin configs here if desired -->
</plugins>
</pluginManagement>
</build>
<profiles>
<!-- Release profile for Maven Central -->
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals><goal>jar</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.2</version>
<configuration>
<failOnError>false</failOnError>
<doclint>none</doclint>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals><goal>jar</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<keyname>${gpg.keyname}</keyname>
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals><goal>sign</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.8.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>
<autoPublish>true</autoPublish>
<waitUntil>uploaded</waitUntil>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<repositories>
<repository>
<id>central-portal-snapshots</id>
<name>Central Portal Snapshots</name>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>
-140
View File
@@ -1,140 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Independent project: inheriting the build parent would create a dependency cycle. -->
<groupId>com.kingsrook</groupId>
<artifactId>qbit-guardrails</artifactId>
<version>${revision}</version>
<packaging>jar</packaging>
<name>QRunIO QBit Architecture Guardrails</name>
<description>Inheritable, report-first architecture checks for QBits.</description>
<url>https://github.com/Kingsrook/qbit-bom</url>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
<distribution>repo</distribution>
</license>
</licenses>
<scm>
<connection>scm:git:https://github.com/Kingsrook/qbit-bom.git</connection>
<developerConnection>scm:git:ssh://git@github.com/Kingsrook/qbit-bom.git</developerConnection>
<url>https://github.com/Kingsrook/qbit-bom</url>
<tag>HEAD</tag>
</scm>
<developers>
<developer>
<id>kingsrook</id>
<name>Kingsrook, LLC</name>
<email>pub@qrun.io</email>
<organization>Kingsrook, LLC</organization>
<organizationUrl>https://qrun.io</organizationUrl>
</developer>
</developers>
<properties>
<revision>2.1.0</revision>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId><artifactId>junit-bom</artifactId><version>6.0.1</version>
<type>pom</type><scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.tngtech.archunit</groupId><artifactId>archunit</artifactId><version>1.5.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId><artifactId>maven-artifact</artifactId><version>3.9.11</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.11.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.6</version>
</plugin>
</plugins>
</build>
<profiles>
<!-- Release profile for Maven Central -->
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals><goal>jar</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.2</version>
<configuration>
<failOnError>false</failOnError>
<doclint>none</doclint>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals><goal>jar</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<keyname>${gpg.keyname}</keyname>
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals><goal>sign</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.8.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>
<autoPublish>true</autoPublish>
<waitUntil>uploaded</waitUntil>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
@@ -1,27 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.guardrails;
import org.junit.jupiter.api.Test;
/*******************************************************************************
** G3: middleware extensions ship a committed contract and a contract test.
*******************************************************************************/
public class ExtensionContractGuardrailTest
{
/***************************************************************************
** Raw routes are advisory even when contract enforcement is enabled.
***************************************************************************/
@Test
void contract() throws Exception
{
GuardrailProject project = new GuardrailProject();
var classes = project.classes(project.output);
if(classes.isEmpty()) { GuardrailProject.empty("G3"); return; }
GuardrailProject.report("G3", GuardrailChecks.extensionViolations(classes, project.classes(project.testOutput), project.hasCommittedYaml()), project.rawRoutes(classes));
}
}
@@ -1,268 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.guardrails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import org.apache.maven.artifact.versioning.ComparableVersion;
/*******************************************************************************
** Pure checks shared by the inherited tests and this module's fixture tests.
*******************************************************************************/
final class GuardrailChecks
{
static final String QBIT_PREFIX = "com.kingsrook.qbits.";
static final String EXTENSION = "com.kingsrook.qqq.middleware.javalin.specs.extensions.MiddlewareSpecExtension";
static final Set<String> CONTRACT_HELPERS = Set.of(
"com.kingsrook.qqq.middleware.javalin.tools.UiApiContract",
"com.kingsrook.qqq.middleware.javalin.tools.UiApiContractAssertions");
private static final Pattern MARKER = Pattern.compile(
"STAND-IN\\(core=([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[1-9][0-9]*), removeBy=([0-9]+\\.[0-9]+\\.[0-9]+(?:[-.][A-Za-z0-9]+)*)\\)");
private static final Pattern MARKER_START = Pattern.compile("(?i)\\bstand-?in\\s*\\(");
private static final Pattern UNTAGGED = Pattern.compile("(?i)\\bstand-?in\\b");
private static final Set<String> ROUTE_METHODS = Set.of("get", "post", "put", "patch", "delete", "head", "options", "path", "routes", "addHandler", "sse", "ws");
/***************************************************************************
** Extract the package segment identifying a qbit, excluding our own harness.
***************************************************************************/
static String qbitName(String className)
{
if(!className.startsWith(QBIT_PREFIX))
{
return "";
}
String tail = className.substring(QBIT_PREFIX.length());
int dot = tail.indexOf('.');
return dot < 0 ? "" : tail.substring(0, dot);
}
/***************************************************************************
** Most frequent fourth segment wins; lexical ordering resolves ties.
***************************************************************************/
static String ownPackage(Collection<String> names, String override)
{
if(override != null && !override.isBlank())
{
String value = override.trim().replaceFirst("\\.+$", "");
return value.startsWith(QBIT_PREFIX) ? value.substring(QBIT_PREFIX.length()) : value;
}
return names.stream().map(GuardrailChecks::qbitName).filter(s -> !s.isEmpty() && !s.equals("guardrails"))
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting())).entrySet().stream()
.sorted(Comparator.<Map.Entry<String, Long>>comparingLong(Map.Entry::getValue).reversed().thenComparing(Map.Entry::getKey))
.map(Map.Entry::getKey).findFirst().orElse("");
}
/***************************************************************************
** Exact package boundaries prevent apiInternal from being treated as api.
***************************************************************************/
static boolean allowed(String className, String own, Collection<String> apiPackages)
{
if(!className.startsWith(QBIT_PREFIX) || qbitName(className).equals(own))
{
return true;
}
return apiPackages.stream().map(s -> s.trim().replaceFirst("\\.+$", ""))
.filter(s -> !s.isEmpty()).anyMatch(s -> className.startsWith(s + "."));
}
/***************************************************************************
** Inspect bytecode dependencies without linking against any QQQ classes.
***************************************************************************/
static List<String> dependencies(JavaClasses classes, String own, Collection<String> apiPackages)
{
return classes.stream().filter(c -> qbitName(c.getName()).equals(own))
.flatMap(c -> c.getDirectDependenciesFromSelf().stream())
.filter(d -> !allowed(d.getTargetClass().getName(), own, apiPackages))
.map(d -> d.getDescription()).distinct().sorted().toList();
}
/***************************************************************************
** Keep source offsets for useful file:line diagnostics.
***************************************************************************/
record Literal(String value, int line) {}
/***************************************************************************
** Lex Java strings/text blocks, ignoring comments and character literals.
** This is intentionally a literal check, not constant-expression evaluation.
***************************************************************************/
static List<Literal> literals(String source)
{
List<Literal> result = new ArrayList<>();
int line = 1;
for(int i = 0; i < source.length(); )
{
char c = source.charAt(i);
if(c == '\n')
{
line++;
i++;
}
else if(source.startsWith("//", i))
{
while(i < source.length() && source.charAt(i) != '\n') { i++; }
}
else if(source.startsWith("/*", i))
{
i += 2;
while(i < source.length() && !source.startsWith("*/", i))
{
if(source.charAt(i++) == '\n') { line++; }
}
i = Math.min(i + 2, source.length());
}
else if(c == '"' || c == '\'')
{
int startLine = line;
boolean block = source.startsWith("\"\"\"", i);
String delimiter = block ? "\"\"\"" : String.valueOf(c);
i += delimiter.length();
StringBuilder value = new StringBuilder();
while(i < source.length() && !source.startsWith(delimiter, i))
{
char next = source.charAt(i++);
if(next == '\n') { line++; }
value.append(next);
if(next == '\\' && i < source.length())
{
char escaped = source.charAt(i++);
if(escaped == '\n') { line++; }
value.append(escaped);
}
}
i = Math.min(i + delimiter.length(), source.length());
if(c == '"')
{
String literal = value.toString();
if(block)
{
String stripped = literal.stripLeading();
startLine += (int) literal.substring(0, literal.length() - stripped.length()).chars().filter(ch -> ch == '\n').count();
literal = stripped;
}
result.add(new Literal(literal, startLine));
}
}
else { i++; }
}
return result;
}
/***************************************************************************
** Reflective class names must obey the same boundary as bytecode references.
***************************************************************************/
static List<String> literalViolations(String file, String source, String own, Collection<String> apiPackages)
{
return literals(source).stream().filter(l -> l.value().startsWith(QBIT_PREFIX))
.filter(l -> !qbitName(l.value()).isEmpty() && !allowed(l.value(), own, apiPackages))
.map(l -> file + ":" + l.line() + ": " + l.value()).toList();
}
/***************************************************************************
** Determine whether imported classes reference one of the named contracts.
***************************************************************************/
static boolean references(JavaClasses classes, Set<String> targets)
{
return classes.stream().flatMap(c -> c.getDirectDependenciesFromSelf().stream())
.anyMatch(d -> targets.contains(d.getTargetClass().getName()));
}
/***************************************************************************
** An extension needs both a source contract and a compiled contract test.
***************************************************************************/
static List<String> extensionViolations(JavaClasses main, JavaClasses tests, boolean hasYaml)
{
List<String> violations = new ArrayList<>();
if(references(main, Set.of(EXTENSION)))
{
if(!hasYaml) { violations.add("MiddlewareSpecExtension requires a committed src/main/resources/openapi/qqq-v1/*.yaml"); }
if(!references(tests, CONTRACT_HELPERS)) { violations.add("No test class references UiApiContract or UiApiContractAssertions"); }
}
return violations;
}
/***************************************************************************
** Raw-route warnings require a registration call, not merely a Context use.
***************************************************************************/
static boolean registersRoutes(JavaClass javaClass)
{
return javaClass.getMethodCallsFromSelf().stream().anyMatch(call ->
call.getTargetOwner().getName().startsWith("io.javalin.") && ROUTE_METHODS.contains(call.getName()));
}
/***************************************************************************
** Maven ordering handles release candidates and final releases correctly.
***************************************************************************/
static boolean expired(String coreVersion, String removeBy)
{
return new ComparableVersion(coreVersion).compareTo(new ComparableVersion(removeBy)) >= 0;
}
/***************************************************************************
** Separate violations from advisory tagging/version warnings.
***************************************************************************/
record StandIns(List<String> violations, List<String> warnings) {}
/***************************************************************************
** Validate every marker, including multiple markers on the same source line.
***************************************************************************/
static StandIns standIns(String file, String source, String coreVersion)
{
List<String> violations = new ArrayList<>();
List<String> warnings = new ArrayList<>();
String[] lines = source.split("\\R", -1);
for(int i = 0; i < lines.length; i++)
{
String location = file + ":" + (i + 1) + ": ";
var start = MARKER_START.matcher(lines[i]);
boolean hasMarker = false;
while(start.find())
{
hasMarker = true;
var marker = MARKER.matcher(lines[i]).region(start.start(), lines[i].length());
if(!marker.lookingAt())
{
violations.add(location + "malformed STAND-IN marker: " + lines[i].trim());
}
else if(coreVersion == null || coreVersion.isBlank())
{
warnings.add(location + "cannot evaluate removeBy=" + marker.group(2) + "; QQQ core version unavailable (set -Dqqq.version)");
}
else if(expired(coreVersion, marker.group(2)))
{
violations.add(location + marker.group() + " expired at QQQ " + coreVersion);
}
}
if(!hasMarker && UNTAGGED.matcher(lines[i]).find())
{
warnings.add(location + "untagged stand-in: " + lines[i].trim());
}
}
return new StandIns(violations, warnings);
}
}
@@ -1,232 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.guardrails;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import com.tngtech.archunit.ArchConfiguration;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
/*******************************************************************************
** Read only the consuming project's outputs and source trees.
*******************************************************************************/
final class GuardrailProject
{
final Path basedir = Path.of(System.getProperty("qbit.guardrails.basedir", System.getProperty("user.dir")));
final Path output = Path.of(System.getProperty("qbit.guardrails.outputDirectory", basedir.resolve("target/classes").toString()));
final Path testOutput = Path.of(System.getProperty("qbit.guardrails.testOutputDirectory", basedir.resolve("target/test-classes").toString()));
/***************************************************************************
** Avoid resolving the entire QQQ/third-party graph just to inspect targets.
***************************************************************************/
JavaClasses classes(Path directory)
{
return ArchConfiguration.get().withThreadLocalScope(configuration ->
{
configuration.setResolveMissingDependenciesFromClassPath(false);
return new ClassFileImporter().importPaths(Files.isDirectory(directory) ? List.of(directory) : List.of());
});
}
/***************************************************************************
** Enumerate sources deterministically and close the directory stream.
***************************************************************************/
List<Path> sources(String tree) throws IOException
{
Path directory = basedir.resolve(tree);
if(!Files.isDirectory(directory)) { return List.of(); }
try(var paths = Files.walk(directory))
{
return paths.filter(Files::isRegularFile).filter(p -> p.toString().endsWith(".java")).sorted().toList();
}
}
/***************************************************************************
** Accept an explicit ownPackage override for unusual or mixed layouts.
***************************************************************************/
String own(JavaClasses classes)
{
return GuardrailChecks.ownPackage(classes.stream().map(c -> c.getName()).toList(), System.getProperty("qbit.guardrails.ownPackage"));
}
/***************************************************************************
** Only API packages actually supplied by allowed *-api artifacts are added.
** Reading jar metadata avoids any dependency on QQQ or Maven project APIs.
***************************************************************************/
Set<String> apiPackages() throws IOException
{
Set<String> packages = new TreeSet<>(csv(System.getProperty("qbit.guardrails.allowedApiPackages", "com.kingsrook.qbits.objectstorage.api,com.kingsrook.qbits.email.api,com.kingsrook.qbits.assistant.spi")));
List<String> allowedArtifacts = csv(System.getProperty("qbit.guardrails.allowedQbitArtifacts", "com.kingsrook.qbits:qbit-object-storage,com.kingsrook.qbits:qbit-email,com.kingsrook.qbits:*-api"));
String classpath = System.getProperty("surefire.test.class.path", System.getProperty("java.class.path"));
for(String entry : classpath.split(Pattern.quote(File.pathSeparator)))
{
Path path = Path.of(entry);
if(!entry.endsWith(".jar") || !Files.isRegularFile(path)) { continue; }
try(JarFile jar = new JarFile(path.toFile()))
{
boolean allowedApi = false;
for(var metadata : jar.stream().filter(e -> e.getName().startsWith("META-INF/maven/com.kingsrook.qbits/") && e.getName().endsWith("/pom.properties")).toList())
{
Properties properties = new Properties();
try(InputStream input = jar.getInputStream(metadata)) { properties.load(input); }
String artifact = properties.getProperty("artifactId", "");
String coordinate = properties.getProperty("groupId", "") + ":" + artifact;
allowedApi |= artifact.endsWith("-api") && allowedArtifacts.stream().anyMatch(pattern -> artifactMatches(pattern, coordinate));
}
if(allowedApi)
{
jar.stream().map(e -> e.getName().replace('/', '.'))
.filter(name -> name.matches("com\\.kingsrook\\.qbits\\.[^.]+\\.api\\..+\\.class"))
.map(name -> GuardrailChecks.QBIT_PREFIX + GuardrailChecks.qbitName(name) + ".api").forEach(packages::add);
}
}
}
return packages;
}
/***************************************************************************
** Allow-list entries use Maven groupId:artifactId patterns with * wildcards.
***************************************************************************/
static boolean artifactMatches(String pattern, String coordinate)
{
String regex = Arrays.stream(pattern.split("\\*", -1)).map(Pattern::quote).collect(java.util.stream.Collectors.joining(".*"));
return coordinate.matches(regex);
}
/***************************************************************************
** Share comma-separated property parsing with the parent POM convention.
***************************************************************************/
static List<String> csv(String text)
{
return Arrays.stream(text.split(",")).map(String::trim).filter(s -> !s.isEmpty()).toList();
}
/***************************************************************************
** Require a source YAML; in a checkout also verify it exists in HEAD.
** Source archives lack Git metadata and are checked by source presence.
***************************************************************************/
boolean hasCommittedYaml() throws IOException, InterruptedException
{
Path directory = basedir.resolve("src/main/resources/openapi/qqq-v1");
if(!Files.isDirectory(directory)) { return false; }
List<Path> yaml;
try(var files = Files.list(directory))
{
yaml = files.filter(Files::isRegularFile).filter(p -> p.getFileName().toString().endsWith(".yaml")).toList();
}
if(yaml.isEmpty()) { return false; }
Path checkout = basedir.toAbsolutePath();
while(checkout != null && !Files.exists(checkout.resolve(".git"))) { checkout = checkout.getParent(); }
if(checkout == null) { return true; }
// An archive may live inside another checkout's ignored scratch directory.
if(!committed(checkout, basedir.resolve("pom.xml"))) { return true; }
for(Path path : yaml)
{
if(committed(checkout, path)) { return true; }
}
return false;
}
/***************************************************************************
** Check HEAD rather than the index: staged files are not yet committed.
***************************************************************************/
private static boolean committed(Path checkout, Path path) throws IOException, InterruptedException
{
String relative = checkout.relativize(path.toAbsolutePath()).toString().replace('\\', '/');
Process process = new ProcessBuilder("git", "-C", checkout.toString(), "cat-file", "-e", "HEAD:" + relative)
.redirectOutput(ProcessBuilder.Redirect.DISCARD).redirectError(ProcessBuilder.Redirect.DISCARD).start();
return process.waitFor() == 0;
}
/***************************************************************************
** Prefer the actual backend artifact's version over a caller's fallback.
***************************************************************************/
String coreVersion() throws IOException
{
try(InputStream input = getClass().getClassLoader().getResourceAsStream("META-INF/maven/com.kingsrook.qqq/qqq-backend-core/pom.properties"))
{
if(input != null)
{
Properties properties = new Properties();
properties.load(input);
String version = properties.getProperty("version");
if(version != null && !version.isBlank()) { return version; }
}
}
return System.getProperty("qqq.version");
}
/***************************************************************************
** Correlate a route registration's class with its Java source file.
***************************************************************************/
List<String> rawRoutes(JavaClasses classes) throws IOException
{
Set<String> providers = new TreeSet<>();
classes.stream().filter(GuardrailChecks::registersRoutes).forEach(c ->
providers.add(c.getName().split("\\$", 2)[0].replace('.', '/') + ".java"));
List<String> warnings = new ArrayList<>();
Path sourceRoot = basedir.resolve("src/main/java");
for(String provider : providers)
{
Path source = sourceRoot.resolve(provider);
if(!Files.isRegularFile(source)) { continue; }
GuardrailChecks.literals(Files.readString(source, StandardCharsets.UTF_8)).stream()
.filter(l -> l.value().startsWith("/qqq/v1/"))
.map(l -> basedir.relativize(source) + ":" + l.line() + ": raw /qqq/v1 routes: " + l.value()).forEach(warnings::add);
}
return warnings;
}
/***************************************************************************
** Each test reports before asserting; report-only tests always pass.
***************************************************************************/
static void report(String rule, List<String> violations, List<String> warnings)
{
boolean enforce = Boolean.getBoolean("qbit.guardrails.enforce");
String status = violations.isEmpty() ? "PASS" : "FAIL" + (enforce ? " (enforced)" : " (report-only)");
System.out.println("[qbit-guardrails] " + rule + " " + status + ": " + violations.size() + " violations");
violations.forEach(v -> System.out.println("[qbit-guardrails] " + rule + " " + v));
warnings.forEach(w -> System.out.println("[qbit-guardrails] " + rule + " WARN: " + w));
if(enforce && !violations.isEmpty())
{
throw new AssertionError(rule + ": " + String.join("\n", violations));
}
}
/***************************************************************************
** Missing/empty main output is normal for aggregator and POM projects.
***************************************************************************/
static void empty(String rule)
{
System.out.println("[qbit-guardrails] " + rule + " PASS: no main classes; nothing to check");
}
}
@@ -1,52 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.guardrails;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
/*******************************************************************************
** G1: implementation and reflective dependencies stay inside the owning qbit.
*******************************************************************************/
public class QbitDependencyGuardrailTest
{
/***************************************************************************
** Keep bytecode and literal results separate in fleet reports.
***************************************************************************/
@Test
void architecture() throws Exception
{
GuardrailProject project = new GuardrailProject();
var classes = project.classes(project.output);
if(classes.isEmpty()) { GuardrailProject.empty("G1-arch"); return; }
String own = project.own(classes);
GuardrailProject.report("G1-arch", GuardrailChecks.dependencies(classes, own, project.apiPackages()),
own.isEmpty() ? List.of("No com.kingsrook.qbits.<own> classes found; set qbit.guardrails.ownPackage if needed") : List.of());
}
/***************************************************************************
** Catch reflective references which bytecode dependency checks cannot see.
***************************************************************************/
@Test
void literals() throws Exception
{
GuardrailProject project = new GuardrailProject();
var classes = project.classes(project.output);
if(classes.isEmpty()) { GuardrailProject.empty("G1-literals"); return; }
String own = project.own(classes);
var apiPackages = project.apiPackages();
List<String> violations = new ArrayList<>();
for(var source : project.sources("src/main/java"))
{
violations.addAll(GuardrailChecks.literalViolations(project.basedir.relativize(source).toString(), Files.readString(source), own, apiPackages));
}
GuardrailProject.report("G1-literals", violations, List.of());
}
}
@@ -1,41 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.guardrails;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
/*******************************************************************************
** G4: temporary stand-ins have a precise removal milestone.
*******************************************************************************/
public class StandInGuardrailTest
{
/***************************************************************************
** Source comments are intentional inputs; both production and tests count.
***************************************************************************/
@Test
void expiry() throws Exception
{
GuardrailProject project = new GuardrailProject();
if(project.classes(project.output).isEmpty()) { GuardrailProject.empty("G4"); return; }
List<String> violations = new ArrayList<>();
List<String> warnings = new ArrayList<>();
String coreVersion = project.coreVersion();
for(String tree : List.of("src/main/java", "src/test/java"))
{
for(var source : project.sources(tree))
{
var result = GuardrailChecks.standIns(project.basedir.relativize(source).toString(), Files.readString(source), coreVersion);
violations.addAll(result.violations());
warnings.addAll(result.warnings());
}
}
GuardrailProject.report("G4", violations, warnings);
}
}
@@ -1,369 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.guardrails;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Behavioral regression tests, using real bytecode fixtures without QQQ jars.
*******************************************************************************/
class GuardrailChecksTest
{
@TempDir
Path temporary;
/***************************************************************************
** Own-package detection is stable and can be explicitly overridden.
***************************************************************************/
@Test
void detectsOwnPackage()
{
var names = List.of("com.kingsrook.qbits.one.A", "com.kingsrook.qbits.two.B", "com.kingsrook.qbits.two.C");
assertEquals("two", GuardrailChecks.ownPackage(names, null));
assertEquals("one", GuardrailChecks.ownPackage(names, "com.kingsrook.qbits.one.."));
assertEquals("one", GuardrailChecks.ownPackage(names, "one"));
assertEquals("", GuardrailChecks.ownPackage(List.of("org.example.App"), null));
}
/***************************************************************************
** Only foreign implementation packages are forbidden.
***************************************************************************/
@Test
void packageBoundaries()
{
var allowed = List.of("com.kingsrook.qbits.other.api..");
for(String name : List.of("java.lang.String", "javax.sql.DataSource", "jdk.foo.Bar", "org.example.Lib", "com.kingsrook.qqq.Core", "com.kingsrook.qbits.own.Internal", "com.kingsrook.qbits.other.api.Model", "com.kingsrook.qbits.other.api.nested.Model"))
{
assertTrue(GuardrailChecks.allowed(name, "own", allowed), name);
}
assertFalse(GuardrailChecks.allowed("com.kingsrook.qbits.other.Internal", "own", allowed));
assertFalse(GuardrailChecks.allowed("com.kingsrook.qbits.other.apiInternal.Model", "own", allowed));
assertFalse(GuardrailChecks.allowed("com.kingsrook.qbits.ownExtra.Model", "own", allowed));
}
/***************************************************************************
** Assistant consumers can use the published SPI without exposing internals.
***************************************************************************/
@Test
void defaultAssistantSpiPackages() throws Exception
{
String oldPackages = System.getProperty("qbit.guardrails.allowedApiPackages");
String oldClasspath = System.getProperty("surefire.test.class.path");
try
{
System.clearProperty("qbit.guardrails.allowedApiPackages");
System.setProperty("surefire.test.class.path", temporary.toString());
var allowed = new GuardrailProject().apiPackages();
assertTrue(GuardrailChecks.allowed("com.kingsrook.qbits.assistant.spi.ModelRequest", "own", allowed));
for(String name : List.of("com.kingsrook.qbits.assistant.spiInternal.X", "com.kingsrook.qbits.assistant.engine.ModelPricing", "com.kingsrook.qbits.assistant.provider.X", "com.kingsrook.qbits.assistant.api.X"))
{
assertFalse(GuardrailChecks.allowed(name, "own", allowed), name);
}
}
finally
{
restore("qbit.guardrails.allowedApiPackages", oldPackages);
restore("surefire.test.class.path", oldClasspath);
}
}
/***************************************************************************
** The existing API artifact wildcard already covers the assistant provider.
***************************************************************************/
@Test
void assistantProviderApiArtifact()
{
assertTrue(GuardrailProject.artifactMatches("com.kingsrook.qbits:*-api", "com.kingsrook.qbits:qbit-assistant-provider-api"));
assertFalse(GuardrailProject.artifactMatches("com.kingsrook.qbits:*-api", "com.kingsrook.qbits:qbit-assistant"));
}
/***************************************************************************
** Architecture analysis uses actual fields and method calls from bytecode.
***************************************************************************/
@Test
void bytecodeDependencies()
{
var classes = new ClassFileImporter().importClasses(com.kingsrook.qbits.own.Fixture.class);
var violations = GuardrailChecks.dependencies(classes, "own", List.of("com.kingsrook.qbits.other.api"));
assertTrue(violations.stream().anyMatch(v -> v.contains("other.Internal")));
assertTrue(violations.stream().noneMatch(v -> v.contains("api.Model")));
}
/***************************************************************************
** Ignore comments/chars; preserve lines for reflection strings/text blocks.
***************************************************************************/
@Test
void scansLiterals()
{
String source = "// \"com.kingsrook.qbits.other.Comment\"\n"
+ "char quote = '\"'; /* \"com.kingsrook.qbits.other.Comment\" */\n"
+ "Class.forName(\"com.kingsrook.qbits.other.Internal\");\n"
+ "String okay = \"com.kingsrook.qbits.other.api.Model\";\n"
+ "String own = \"com.kingsrook.qbits.own.Local\";\n"
+ "String block = \"\"\"\n com.kingsrook.qbits.other.Block\n\"\"\";";
var violations = GuardrailChecks.literalViolations("Example.java", source, "own", List.of("com.kingsrook.qbits.other.api"));
assertEquals(2, violations.size());
assertTrue(violations.get(0).startsWith("Example.java:3:"));
assertTrue(violations.get(1).startsWith("Example.java:7:"));
}
/***************************************************************************
** Escaped quotes cannot terminate a string or create a comment.
***************************************************************************/
@Test
void escapedStrings()
{
var literals = GuardrailChecks.literals("String x = \"quote \\\" // still a string\"; String y = \"next\";");
assertEquals(2, literals.size());
assertEquals("next", literals.get(1).value());
}
/***************************************************************************
** Contracts are required only when the extension type is referenced.
***************************************************************************/
@Test
void detectsExtensionContract()
{
var importer = new ClassFileImporter();
var main = importer.importClasses(com.kingsrook.qbits.own.ExtensionFixture.class);
var tests = importer.importClasses(com.kingsrook.qbits.own.ContractFixture.class);
var empty = importer.importPaths(List.of());
assertEquals(2, GuardrailChecks.extensionViolations(main, empty, false).size());
assertEquals(1, GuardrailChecks.extensionViolations(main, tests, false).size());
assertEquals(1, GuardrailChecks.extensionViolations(main, empty, true).size());
assertTrue(GuardrailChecks.extensionViolations(main, tests, true).isEmpty());
assertTrue(GuardrailChecks.extensionViolations(empty, empty, false).isEmpty());
}
/***************************************************************************
** A Javalin registration differs from merely referring to Javalin types.
***************************************************************************/
@Test
void detectsRouteRegistration()
{
var classes = new ClassFileImporter().importClasses(com.kingsrook.qbits.own.RouteFixture.class, com.kingsrook.qbits.own.Fixture.class);
assertTrue(GuardrailChecks.registersRoutes(classes.get(com.kingsrook.qbits.own.RouteFixture.class)));
assertFalse(GuardrailChecks.registersRoutes(classes.get(com.kingsrook.qbits.own.Fixture.class)));
}
/***************************************************************************
** Removal milestones follow Maven ordering, including numeric RC suffixes.
***************************************************************************/
@Test
void comparesVersions()
{
assertFalse(GuardrailChecks.expired("4.1.0-RC.5", "4.1.0-RC.6"));
assertTrue(GuardrailChecks.expired("4.1.0-RC.6", "4.1.0-RC.6"));
assertTrue(GuardrailChecks.expired("4.1.0-RC.10", "4.1.0-RC.6"));
assertTrue(GuardrailChecks.expired("4.1.0", "4.1.0-RC.6"));
assertFalse(GuardrailChecks.expired("4.1.0-RC.6", "4.1.0"));
}
/***************************************************************************
** Accept any owner/repo and detect expiration with a useful location.
***************************************************************************/
@Test
void validMarkers()
{
String source = "// STAND-IN(core=QRun-IO/qbit-x#42, removeBy=4.1.0-RC.6)";
assertTrue(GuardrailChecks.standIns("A.java", source, "4.1.0-RC.5").violations().isEmpty());
assertTrue(GuardrailChecks.standIns("A.java", source, "4.1.0").violations().getFirst().startsWith("A.java:1:"));
assertEquals(1, GuardrailChecks.standIns("A.java", source, null).warnings().size());
}
/***************************************************************************
** Missing milestones and malformed issue/version syntax cannot pass.
***************************************************************************/
@Test
void malformedMarkers()
{
for(String marker : List.of("STAND-IN(core=QRun-IO/qqq#1)", "STAND-IN(core=qqq#1, removeBy=4.1.0)", "STAND-IN(core=QRun-IO/qqq#0, removeBy=4.1.0)", "STAND-IN(core=QRun-IO/qqq#1, removeBy=soon)", "STAND-IN(core=QRun-IO/qqq#1, removeBy=4.1.0", "stand-in(core=QRun-IO/qqq#1, removeBy=4.1.0)"))
{
assertEquals(1, GuardrailChecks.standIns("A.java", marker, "4.0.0").violations().size(), marker);
}
}
/***************************************************************************
** Scan all markers per line; prose stand-ins remain advisory.
***************************************************************************/
@Test
void multipleMarkersAndWarnings()
{
var result = GuardrailChecks.standIns("A.java", "STAND-IN(core=QRun-IO/qqq#1, removeBy=4.0.0) STAND-IN(core=QRun-IO/qqq#2)\n// temporary StandIn\n// stand-in until core catches up", "4.1.0");
assertEquals(2, result.violations().size());
assertEquals(2, result.warnings().size());
}
/***************************************************************************
** Only allowed API artifacts contribute automatic API package exemptions.
***************************************************************************/
@Test
void discoversApiArtifactPackages() throws Exception
{
Path jar = temporary.resolve("fixture-api.jar");
try(var output = new JarOutputStream(Files.newOutputStream(jar)))
{
output.putNextEntry(new JarEntry("META-INF/maven/com.kingsrook.qbits/fixture-api/pom.properties"));
output.write("groupId=com.kingsrook.qbits\nartifactId=fixture-api\nversion=1.0.0\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));
output.closeEntry();
output.putNextEntry(new JarEntry("com/kingsrook/qbits/fixture/api/Model.class"));
output.closeEntry();
output.putNextEntry(new JarEntry("com/kingsrook/qbits/fixture/internal/Secret.class"));
output.closeEntry();
}
String oldClasspath = System.getProperty("surefire.test.class.path");
String oldAllow = System.getProperty("qbit.guardrails.allowedQbitArtifacts");
try
{
System.setProperty("surefire.test.class.path", jar.toString());
System.setProperty("qbit.guardrails.allowedQbitArtifacts", "com.kingsrook.qbits:*-api");
assertTrue(new GuardrailProject().apiPackages().contains("com.kingsrook.qbits.fixture.api"));
assertFalse(new GuardrailProject().apiPackages().contains("com.kingsrook.qbits.fixture.internal"));
System.setProperty("qbit.guardrails.allowedQbitArtifacts", "com.kingsrook.qbits:qbit-object-storage");
assertFalse(new GuardrailProject().apiPackages().contains("com.kingsrook.qbits.fixture.api"));
}
finally
{
restore("surefire.test.class.path", oldClasspath);
restore("qbit.guardrails.allowedQbitArtifacts", oldAllow);
}
}
/***************************************************************************
** Default reports pass, enforcement fails, and advisory warnings never fail.
***************************************************************************/
@Test
void reportVersusEnforce()
{
String old = System.getProperty("qbit.guardrails.enforce");
PrintStream original = System.out;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try
{
System.setOut(new PrintStream(bytes));
System.setProperty("qbit.guardrails.enforce", "false");
GuardrailProject.report("G1", List.of("bad reference"), List.of());
assertTrue(bytes.toString().contains("G1 FAIL (report-only): 1 violations"));
System.setProperty("qbit.guardrails.enforce", "true");
assertThrows(AssertionError.class, () -> GuardrailProject.report("G1", List.of("bad reference"), List.of()));
GuardrailProject.report("G3", List.of(), List.of("raw /qqq/v1 routes"));
}
finally { System.setOut(original); restore("qbit.guardrails.enforce", old); }
}
/***************************************************************************
** Missing and empty outputs do not throw import errors.
***************************************************************************/
@Test
void emptyOutputs() throws Exception
{
GuardrailProject project = new GuardrailProject();
assertTrue(project.classes(temporary.resolve("missing")).isEmpty());
assertTrue(project.classes(Files.createDirectory(temporary.resolve("empty"))).isEmpty());
}
/***************************************************************************
** Raw route warnings identify the provider's source line and path.
***************************************************************************/
@Test
void rawRouteDiagnostics() throws Exception
{
Path source = temporary.resolve("src/main/java/com/kingsrook/qbits/own/RouteFixture.java");
Files.createDirectories(source.getParent());
Files.writeString(source, "// documentation \"/qqq/v1/not-a-route\"\napp.get(\"/qqq/v1/fixture\");");
String old = System.getProperty("qbit.guardrails.basedir");
try
{
System.setProperty("qbit.guardrails.basedir", temporary.toString());
var classes = new ClassFileImporter().importClasses(com.kingsrook.qbits.own.RouteFixture.class);
var warnings = new GuardrailProject().rawRoutes(classes);
assertEquals(1, warnings.size());
assertTrue(warnings.getFirst().endsWith(":2: raw /qqq/v1 routes: /qqq/v1/fixture"));
}
finally { restore("qbit.guardrails.basedir", old); }
}
/***************************************************************************
** Archives require YAML in the exact contract directory, not test resources.
***************************************************************************/
@Test
void sourceArchiveContracts() throws Exception
{
String old = System.getProperty("qbit.guardrails.basedir");
try
{
System.setProperty("qbit.guardrails.basedir", temporary.toString());
GuardrailProject project = new GuardrailProject();
assertFalse(project.hasCommittedYaml());
Path directory = temporary.resolve("src/main/resources/openapi/qqq-v1");
Files.createDirectories(directory);
Files.writeString(directory.resolve("extension.json"), "{}");
assertFalse(project.hasCommittedYaml());
Files.writeString(directory.resolve("extension.yaml"), "openapi: 3.0.3");
assertTrue(project.hasCommittedYaml());
}
finally { restore("qbit.guardrails.basedir", old); }
}
/***************************************************************************
** Standalone consumers can supply a version if no core resource exists.
***************************************************************************/
@Test
void coreVersionFallback() throws Exception
{
String old = System.getProperty("qqq.version");
try
{
System.setProperty("qqq.version", "4.1.0-RC.6");
assertEquals("4.1.0-RC.6", new GuardrailProject().coreVersion());
}
finally { restore("qqq.version", old); }
}
/***************************************************************************
** Restore process properties so tests do not contaminate each other.
***************************************************************************/
private static void restore(String key, String value)
{
if(value == null) { System.clearProperty(key); }
else { System.setProperty(key, value); }
}
}
@@ -1,10 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.other;
/** Bytecode fixture; deliberately minimal and never shipped in the guardrail jar. */
public class Internal {}
@@ -1,10 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.other.api;
/** Bytecode fixture; deliberately minimal and never shipped in the guardrail jar. */
public class Model {}
@@ -1,10 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.own;
/** Bytecode fixture; deliberately minimal and never shipped in the guardrail jar. */
public class ContractFixture { com.kingsrook.qqq.middleware.javalin.tools.UiApiContractAssertions assertions; }
@@ -1,10 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.own;
/** Bytecode fixture; deliberately minimal and never shipped in the guardrail jar. */
public class ExtensionFixture { com.kingsrook.qqq.middleware.javalin.specs.extensions.MiddlewareSpecExtension extension; }
@@ -1,10 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.own;
/** Bytecode fixture; deliberately minimal and never shipped in the guardrail jar. */
public class Fixture { com.kingsrook.qbits.other.Internal internal; com.kingsrook.qbits.other.api.Model api; java.lang.String thirdParty; }
@@ -1,10 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.own;
/** Bytecode fixture; deliberately minimal and never shipped in the guardrail jar. */
public class RouteFixture { public void register(io.javalin.Javalin app) { app.get("/qqq/v1/fixture"); } }
@@ -1,10 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qqq.middleware.javalin.specs.extensions;
/** Bytecode fixture; deliberately minimal and never shipped in the guardrail jar. */
public class MiddlewareSpecExtension {}
@@ -1,10 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qqq.middleware.javalin.tools;
/** Bytecode fixture; deliberately minimal and never shipped in the guardrail jar. */
public class UiApiContractAssertions {}
@@ -1,10 +0,0 @@
/*
* Copyright 2026 QRunIO, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package io.javalin;
/** Bytecode fixture; deliberately minimal and never shipped in the guardrail jar. */
public class Javalin { public void get(String path) {} }
-39
View File
@@ -1,39 +0,0 @@
#!/usr/bin/env python3
"""Verify exact project notices in explicitly listed Maven archives."""
import argparse
from pathlib import Path
import sys
from zipfile import BadZipFile, ZipFile
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True,
help="consumer repository containing LICENSE and NOTICE")
parser.add_argument("--prefix", default="META-INF",
help="directory containing the project's archive copies")
parser.add_argument("archives", type=Path, nargs="+")
args = parser.parse_args()
expected = {name: (args.root / name).read_bytes() for name in ("LICENSE", "NOTICE")}
failures = []
for archive in args.archives:
try:
with ZipFile(archive) as jar:
for name, content in expected.items():
entry = f"{args.prefix.rstrip('/')}/{name}"
if jar.namelist().count(entry) != 1:
failures.append(f"{archive}: expected exactly one {entry}")
elif jar.read(entry) != content:
failures.append(f"{archive}: {entry} differs from root {name}")
except (OSError, BadZipFile) as error:
failures.append(f"{archive}: {error}")
if failures:
print("\n".join(failures), file=sys.stderr)
return 1
print(f"PASS: exact LICENSE/NOTICE in {len(args.archives)} archives")
return 0
if __name__ == "__main__":
sys.exit(main())