Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67fa618d37 | ||
|
|
ba1bd6e7bc
|
||
|
|
bfaba1e260 | ||
|
|
5de8fcd3fe
|
||
|
|
aaf52d202d | ||
|
|
806d20dcad | ||
|
|
dd9a67723a | ||
|
|
b67f00bc32 |
No files matched your search
+61
-10
@@ -1,14 +1,65 @@
|
||||
version: 2.1
|
||||
setup: true
|
||||
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
qqq-orb: kingsrook/qqq-orb@0.5.0
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
steps:
|
||||
- run:
|
||||
name: Keep package installation unattended
|
||||
command: |
|
||||
# These build machines are discarded after the job.
|
||||
sudo install -d /etc/needrestart/conf.d
|
||||
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
|
||||
|
||||
workflows:
|
||||
setup:
|
||||
|
||||
test_only:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: '0.8'
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only: /^v\d+\.\d+\.\d+$/
|
||||
- qqq-orb/mvn_test_only:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(develop|release/.*|integration.*)/
|
||||
tags:
|
||||
only: []
|
||||
|
||||
publish_snapshot:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
branch_type: snapshot
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- develop
|
||||
|
||||
publish_release_candidate:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
branch_type: release_candidate
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /release\/.*/
|
||||
|
||||
publish_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
branch_type: release
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: [ /v.*/ ]
|
||||
@@ -1,7 +0,0 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
jobs:
|
||||
maven:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
|
||||
secrets: inherit
|
||||
@@ -0,0 +1 @@
|
||||
* @KofTwentyTwo
|
||||
@@ -1,23 +0,0 @@
|
||||
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
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
profile: qbit
|
||||
orb_version: '0.8'
|
||||
default_branch: main
|
||||
java_version: '21'
|
||||
browsers: true
|
||||
api_checks: false
|
||||
docs_only: false
|
||||
release_on_main: false
|
||||
release_tag_policy: stable
|
||||
test_branch_policy: feature-no-hotfix
|
||||
publish:
|
||||
snapshot: true
|
||||
release_candidate: true
|
||||
release: true
|
||||
hotfix: true
|
||||
asciidoc: false
|
||||
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [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
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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).
|
||||
- Known licensing inconsistency: LICENSE/NOTICE = Apache-2.0, pom `<licenses>` = AGPL-3.0
|
||||
(shipped to Central in 1.6.0), README says Proprietary. See dossier "Maturity & risks".
|
||||
@@ -1,94 +1,98 @@
|
||||
# qbit-bom
|
||||
# QBit Build Parent
|
||||
|
||||
Maven Bill of Materials for QBit dependencies.
|
||||
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.
|
||||
|
||||
**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:
|
||||
## Use the parent
|
||||
|
||||
```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>
|
||||
```
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.1.0</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
### Add QBits Without Versions
|
||||
<groupId>com.example.qbits</groupId>
|
||||
<artifactId>my-qbit</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
|
||||
```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>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
## Included QBits
|
||||
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.
|
||||
|
||||
| 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 |
|
||||
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).
|
||||
|
||||
## Compatibility
|
||||
## Release workflow
|
||||
|
||||
| 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 |
|
||||
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.
|
||||
|
||||
## Project Status
|
||||
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.
|
||||
|
||||
Stable. Updated with each QBit release.
|
||||
## Architecture guardrails
|
||||
|
||||
## Contributing
|
||||
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.
|
||||
|
||||
1. Fork the repository
|
||||
2. Update versions in `pom.xml`
|
||||
3. Submit a pull request
|
||||
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:*-api`. Replacing this property replaces the allow-list. |
|
||||
| `qbit.guardrails.allowedApiPackages` | Comma-separated package prefixes. Default: `com.kingsrook.qbits.objectstorage.api`. 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
|
||||
|
||||
Proprietary - QRun.IO
|
||||
See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
||||
@@ -10,6 +10,10 @@
|
||||
<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>
|
||||
@@ -42,7 +46,13 @@
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
|
||||
<!-- use revision property in children -->
|
||||
<revision>1.6.1-SNAPSHOT</revision>
|
||||
<revision>2.1.0</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:*-api</qbit.guardrails.allowedQbitArtifacts>
|
||||
<qbit.guardrails.allowedApiPackages>com.kingsrook.qbits.objectstorage.api</qbit.guardrails.allowedApiPackages>
|
||||
<qbit.guardrails.strictUiApiRoutes>false</qbit.guardrails.strictUiApiRoutes>
|
||||
</properties>
|
||||
|
||||
<!-- Common dependency BOMs -->
|
||||
@@ -87,6 +97,108 @@
|
||||
</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>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
<?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>
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* 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")));
|
||||
List<String> allowedArtifacts = csv(System.getProperty("qbit.guardrails.allowedQbitArtifacts", "com.kingsrook.qbits:qbit-object-storage,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");
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** 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); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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 {}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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 {}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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; }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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; }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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; }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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"); } }
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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 {}
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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 {}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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) {} }
|
||||
|
||||
Reference in new issue
Block a user