Compare commits

...
Author SHA1 Message Date
james.maesandCodex de4631fb51 ci: use central source scanning
Co-Authored-By: Codex <noreply@openai.com>
2026-10-21 11:39:23 -05:00
james.maesandCodex 2e535d5037 ci: adopt central orb 0.7.12 and stable gates (#35)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 08:56:03 -05:00
james.maesandCodex f47c40ffbf ci: centralize rc release completion in qqq orb (#34)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 02:25:19 -05:00
james.maesandCodex 15844af91e ci: adopt shared orb 0.7.11 (#33)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 00:56:30 -05:00
james.maes 2b7edc25a5 Merge pull request #32 from QRun-IO/feature/central-ci
ci: adopt shared QQQ orb standard
2026-10-20 19:07:11 -05:00
james.maesandCodex f98d8124f4 ci: adopt verified central QQQ orb 0.7.3
Co-Authored-By: Codex <noreply@openai.com>
2026-10-20 19:03:06 -05:00
james.maes 677f800015 Merge pull request #29 from QRun-IO/ci/release-rc-secrets-inherit
ci(release-rc): pass secrets so the shared workflow can sign the RC tag
2026-10-16 08:05:28 -05:00
james.maesandClaude Opus 5.5 56b17b5768 ci(release-rc): pass secrets to the shared workflow so it can sign the RC tag
QRun-IO/.github#2 adds a tag-and-release job that signs vX with the
QRUN_TAG_SIGNING_KEY secret after Maven Central confirms the artifact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:19:05 -05:00
james.maes ad4a492c31 Merge pull request #28 from QRun-IO/feature/ui-api-1.6
feat(ui-api): extension for QQQ UI API 1.6.0 (operationIds, BasicErrorResponseV1, QQQ 4.1.0-RC.15)
2026-10-15 16:13:14 -05:00
james.maesandClaude Opus 5.5 912840a22d chore: target QQQ 4.1.0-RC.15 (UI API 1.6.0)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:27:08 -05:00
james.maesandClaude Opus 5.5 db4715c906 feat(ui-api): extension for QQQ UI API 1.6.0 (operationIds, BasicErrorResponseV1, QQQ 4.1.0-RC.14)
Regenerated against UI API 1.6.0: every operation has an operationId and
the standard error responses (BasicErrorResponseV1: error, code, message,
fieldErrors, traceId); the extension's own error schema is kept but
deprecated; hand-written error bodies go through QJavalinUtils so they carry
code/message/traceId. Extension MINOR bumped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:48:14 -05:00
james.maes 3fcff49d4f Merge pull request #27 from QRun-IO/fix/help-bundle-version
fix(help): help bundle version comes from the build, not a hardcoded SNAPSHOT
2026-10-14 22:02:58 -05:00
12 changed files with 631 additions and 188 deletions

No files matched your search

+11 -57
View File
@@ -1,60 +1,14 @@
version: 2.1 version: 2.1
setup: true
orbs: orbs:
qqq-orb: kingsrook/qqq-orb@0.6.9 qqq-orb: kingsrook/qqq-orb@0.7.12
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: workflows:
test_only: setup:
jobs: jobs:
- qqq-orb/mvn_test_only: - qqq-orb/generate_pipeline:
pre-steps: orb_version: 0.7.12
- configure-package-install filters:
context: [qqq-maven-registry-credentials] branches:
filters: ignore: automation/qbit-rc-pending
branches: tags:
ignore: /(develop|release\/.*)/ only: /^v\d+\.\d+\.\d+$/
publish_snapshot:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: snapshot
filters:
branches:
only: [develop]
publish_release_candidate:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release_candidate
filters:
branches:
only: [/release\/.*/]
publish_release:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release
filters:
branches:
ignore: /.*/
tags:
only: [/^v\d+\.\d+\.\d+$/]
+23
View File
@@ -0,0 +1,23 @@
name: CodeQL
'on':
push:
branches:
- main
- develop
- release/**
- hotfix/**
pull_request: {}
schedule:
- cron: 41 16 * * 0
workflow_dispatch: {}
permissions:
contents: read
actions: read
security-events: write
packages: read
concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
+62 -13
View File
@@ -1,25 +1,74 @@
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
# the release line and the release/* push runs CircleCI publish_release_candidate.
name: Release candidate name: Release candidate
'on':
on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
dry_run: dry_run:
description: Print the plan without merging or creating branches description: Plan a cut without changing release state
type: boolean type: boolean
default: false default: false
finalize:
description: Retry finalization of an existing cut; do not cut another RC
type: boolean
default: false
recover:
description: Recover the same published RC after a deployment timeout; never
reupload
type: boolean
default: false
circle_build:
description: Original Circle publisher build number (recovery only)
type: string
default: ''
merge_sha:
description: Existing cut merge_sha (finalize retry only)
type: string
default: ''
bump_sha:
description: Existing cut bump_sha (finalize retry only)
type: string
default: ''
branch:
description: Existing cut branch (finalize retry only)
type: string
default: ''
version:
description: Existing cut version (finalize retry only)
type: string
default: ''
pull_request: pull_request:
types: [closed] types:
branches: [develop] - closed
branches:
- develop
repository_dispatch:
types:
- qbit-rc-published
permissions: permissions:
contents: write contents: write
actions: read
checks: read
jobs: jobs:
release-rc: release-rc:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc')) if: github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch'
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main || (github.event_name == 'pull_request' && github.event.pull_request.merged
== true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name,
'release:rc'))
uses: QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml@cc30f599e63c8e41629c99d23395bac1adb8bc44
with: with:
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
}}
finalize: ${{ github.event_name == 'repository_dispatch' || (github.event_name
== 'workflow_dispatch' && inputs.finalize) }}
recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false
}}
circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build
|| '' }}
merge_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.merge_sha
|| inputs.merge_sha || '' }}
bump_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.bump_sha
|| inputs.bump_sha || '' }}
branch: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch
|| inputs.branch || '' }}
version: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version
|| inputs.version || '' }}
secrets: inherit
+16
View File
@@ -0,0 +1,16 @@
profile: qbit
orb_version: 0.7.12
default_branch: develop
java_version: '21'
browsers: true
api_checks: false
docs_only: false
release_on_main: false
release_tag_policy: stable
test_branch_policy: non-release
publish:
snapshot: true
release_candidate: true
release: true
asciidoc: false
release_completion: true
+1
View File
@@ -16,6 +16,7 @@ All notable changes to this project are documented here. The format follows Keep
- **Record developer panel** "SQL Workbench" (`SqlWorkbenchDeveloperPanelProvider`, registered by `SqlWorkbenchDeveloperPanelsMetaDataProducer`): read-only `SELECT`s for the record and its table in the backend's own names, backend details, and links that open them in the Runner as a new cell (`/app/sqlWorkbenchRunner?sql=&backend=`, never run). Requires QQQ 4.1.0-RC.4 (the developer panel contract) and a Next UI with the Runner `?sql=` support. - **Record developer panel** "SQL Workbench" (`SqlWorkbenchDeveloperPanelProvider`, registered by `SqlWorkbenchDeveloperPanelsMetaDataProducer`): read-only `SELECT`s for the record and its table in the backend's own names, backend details, and links that open them in the Runner as a new cell (`/app/sqlWorkbenchRunner?sql=&backend=`, never run). Requires QQQ 4.1.0-RC.4 (the developer panel contract) and a Next UI with the Runner `?sql=` support.
### Changed ### Changed
- UI API extension sql-workbench 1.1.0 (requires QQQ 4.1.0-RC.15, UI API 1.6.0): operationIds, standard error responses with BasicErrorResponseV1, extension tags, and deprecated SqlWorkbenchErrorV1 retained for compatibility. Existing operationIds, HTTP statuses and success payloads are preserved.
- Require QQQ `4.1.0-RC.6` for the UI API extension contract. Replace the raw Javalin route provider with endpoint specs registered by the qbit producer. `registerRoutes=false` still disables HTTP registration. No existing operation wire changes. - Require QQQ `4.1.0-RC.6` for the UI API extension contract. Replace the raw Javalin route provider with endpoint specs registered by the qbit producer. `registerRoutes=false` still disables HTTP registration. No existing operation wire changes.
- **The nav lists only the main tables**: Notebooks and the execution audit, after the Runner. Cells, versions, pinned results and shares are child lists on a notebook (still standard tables, reachable by URL and API). The app's widget list is now the dashboard, not the `SQL_WORKBENCH` widget; links to `/app/sqlWorkbench` that expected the editor should point at `/app/sqlWorkbenchRunner`. - **The nav lists only the main tables**: Notebooks and the execution audit, after the Runner. Cells, versions, pinned results and shares are child lists on a notebook (still standard tables, reachable by URL and API). The app's widget list is now the dashboard, not the `SQL_WORKBENCH` widget; links to `/app/sqlWorkbench` that expected the editor should point at `/app/sqlWorkbenchRunner`.
- The notebook record's header shows the description. - The notebook record's header shows the description.
+3 -3
View File
@@ -254,9 +254,9 @@
<profiles> <profiles>
<!-- Built against qqq 4.1 (QBitMetaDataProducer, runtime services, Javalin 7 --> <!-- Built against qqq 4.1 (QBitMetaDataProducer, runtime services, Javalin 7 -->
<!-- specs and help bundles require 4.1.0-RC.7). Until --> <!-- specs and help bundles require 4.1.0-RC.15). Until -->
<!-- qbit-build-parent 2.1.0 imports the released 4.1 BOM, this profile is --> <!-- qbit-build-parent 2.1.0 imports the released 4.1 BOM, this profile is -->
<!-- active by default and imports qqq-bom-pom 4.1.0-RC.7 ahead of the --> <!-- active by default and imports qqq-bom-pom 4.1.0-RC.15 ahead of the -->
<!-- parent's 4.0.0 BOM (same as qbit-quick-search, ADR-0007). --> <!-- parent's 4.0.0 BOM (same as qbit-quick-search, ADR-0007). -->
<profile> <profile>
<id>qqq-snapshot</id> <id>qqq-snapshot</id>
@@ -264,7 +264,7 @@
<activeByDefault>true</activeByDefault> <activeByDefault>true</activeByDefault>
</activation> </activation>
<properties> <properties>
<qqq.snapshot.version>4.1.0-RC.7</qqq.snapshot.version> <qqq.snapshot.version>4.1.0-RC.15</qqq.snapshot.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
@@ -43,7 +43,7 @@ public class SqlWorkbenchEndpoints
public static MiddlewareSpecExtension extension() public static MiddlewareSpecExtension extension()
{ {
return (new MiddlewareSpecExtension().withName("sql-workbench").withPathPrefix("/sql-workbench") return (new MiddlewareSpecExtension().withName("sql-workbench").withPathPrefix("/sql-workbench")
.withSemanticVersion("1.0.0").withDescription("Audited SQL execution and notebooks") .withSemanticVersion("1.1.0").withDescription("Audited SQL execution and notebooks")
.withSpec(GetCapabilitiesSpec.class) .withSpec(GetCapabilitiesSpec.class)
.withSpec(GetSchemaSpec.class) .withSpec(GetSchemaSpec.class)
.withSpec(ExecuteSpec.class) .withSpec(ExecuteSpec.class)
@@ -33,6 +33,7 @@ import com.kingsrook.qqq.middleware.javalin.QJavalinUtils;
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation; import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse; import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
import com.kingsrook.qqq.middleware.javalin.specs.extensions.AbstractJsonEndpointSpec; import com.kingsrook.qqq.middleware.javalin.specs.extensions.AbstractJsonEndpointSpec;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.BasicErrorResponseV1;
import com.kingsrook.qqq.openapi.model.HttpMethod; import com.kingsrook.qqq.openapi.model.HttpMethod;
import com.kingsrook.qqq.openapi.model.In; import com.kingsrook.qqq.openapi.model.In;
import com.kingsrook.qqq.openapi.model.Method; import com.kingsrook.qqq.openapi.model.Method;
@@ -155,11 +156,11 @@ public abstract class SqlWorkbenchJsonSpec extends AbstractJsonEndpointSpec
*******************************************************************************/ *******************************************************************************/
static List<BasicResponse> errors() static List<BasicResponse> errors()
{ {
return (List.of(new BasicResponse(HttpStatus.BAD_REQUEST, "Invalid input or execution limit exceeded", "SqlWorkbenchErrorV1"), return (List.of(new BasicResponse(HttpStatus.BAD_REQUEST, "Invalid input or execution limit exceeded", BasicErrorResponseV1.class.getSimpleName()),
new BasicResponse(HttpStatus.UNAUTHORIZED, "Sign in required", "SqlWorkbenchErrorV1"), new BasicResponse(HttpStatus.UNAUTHORIZED, "Sign in required", BasicErrorResponseV1.class.getSimpleName()),
new BasicResponse(HttpStatus.FORBIDDEN, "Permission denied", "SqlWorkbenchErrorV1"), new BasicResponse(HttpStatus.FORBIDDEN, "Permission denied", BasicErrorResponseV1.class.getSimpleName()),
new BasicResponse(HttpStatus.NOT_FOUND, "Missing or invisible resource", "SqlWorkbenchErrorV1"), new BasicResponse(HttpStatus.NOT_FOUND, "Missing or invisible resource", BasicErrorResponseV1.class.getSimpleName()),
new BasicResponse(HttpStatus.CONFLICT, "Execution UUID already used", "SqlWorkbenchErrorV1"))); new BasicResponse(HttpStatus.CONFLICT, "Execution UUID already used", BasicErrorResponseV1.class.getSimpleName())));
} }
@@ -36,6 +36,7 @@ public class SqlWorkbenchSchemas
{ {
Map<String, Schema> schemas = new LinkedHashMap<>(); Map<String, Schema> schemas = new LinkedHashMap<>();
schemas.put("SqlWorkbenchErrorV1", new Schema().withType(Type.OBJECT) schemas.put("SqlWorkbenchErrorV1", new Schema().withType(Type.OBJECT)
.withDescription("Deprecated since 1.1.0: errors use BasicErrorResponseV1.")
.withProperty("error", new Schema().withType(Type.STRING))); .withProperty("error", new Schema().withType(Type.STRING)));
schemas.put("SqlWorkbenchEmptyRequestV1", new Schema().withType(Type.OBJECT)); schemas.put("SqlWorkbenchEmptyRequestV1", new Schema().withType(Type.OBJECT));
schemas.put("SqlWorkbenchDeletedV1", new Schema().withType(Type.OBJECT) schemas.put("SqlWorkbenchDeletedV1", new Schema().withType(Type.OBJECT)
File diff suppressed because it is too large. Load diff
@@ -22,6 +22,7 @@ import java.nio.file.Path;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
import com.kingsrook.qqq.middleware.javalin.specs.v1.responses.BasicErrorResponseV1;
import com.kingsrook.qqq.middleware.javalin.tools.UiApiContract; import com.kingsrook.qqq.middleware.javalin.tools.UiApiContract;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -50,7 +51,7 @@ class SqlWorkbenchApiContractTest
assertThat(UiApiContract.check(generated, Files.readString(path), null, 1, assertThat(UiApiContract.check(generated, Files.readString(path), null, 1,
"./mvnw -Dtest=SqlWorkbenchApiContractTest -DsqlWorkbench.openapi.update=true test")).isEmpty(); "./mvnw -Dtest=SqlWorkbenchApiContractTest -DsqlWorkbench.openapi.update=true test")).isEmpty();
var model = version.generateExtensionOpenAPIModel("qqq", extension); var model = version.generateExtensionOpenAPIModel("qqq", extension);
assertThat(model.getComponents().getSchemas().keySet()).allMatch(name -> name.startsWith("SqlWorkbench")); assertThat(model.getComponents().getSchemas().keySet()).allMatch(name -> name.startsWith("SqlWorkbench") || name.equals(BasicErrorResponseV1.class.getSimpleName()));
assertThat(model.getPaths().keySet()).allMatch(name -> name.startsWith("/qqq/v1/sql-workbench/")); assertThat(model.getPaths().keySet()).allMatch(name -> name.startsWith("/qqq/v1/sql-workbench/"));
assertThat(extension.getSpecs()).hasSize(21); assertThat(extension.getSpecs()).hasSize(21);
assertThat(extension.getEffectivePathPrefix()).isEqualTo("/sql-workbench"); assertThat(extension.getEffectivePathPrefix()).isEqualTo("/sql-workbench");
@@ -98,7 +98,29 @@ class SqlWorkbenchEndpointSpecTest extends BaseSqlWorkbenchTest
builder.header("Content-Type", contentType); builder.header("Content-Type", contentType);
} }
builder.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(body)); builder.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(body));
return (client.send(builder.build(), HttpResponse.BodyHandlers.ofString())); HttpResponse<String> response = client.send(builder.build(), HttpResponse.BodyHandlers.ofString());
if(response.statusCode() >= 400)
{
assertErrorBody(response);
}
return (response);
}
/*******************************************************************************
** HTTP errors retain their message and include the standard diagnostic fields.
*******************************************************************************/
private static void assertErrorBody(HttpResponse<String> response) throws Exception
{
Map<String, Object> body = json(response);
assertThat(body).containsKeys("error", "code", "message", "traceId");
for(String key : List.of("error", "code", "message", "traceId"))
{
assertThat(body.get(key)).as(key).isInstanceOf(String.class);
assertThat((String) body.get(key)).as(key).isNotBlank();
}
assertThat(body.get("message")).isEqualTo(body.get("error"));
} }
@@ -236,7 +258,9 @@ class SqlWorkbenchEndpointSpecTest extends BaseSqlWorkbenchTest
HttpRequest unauthenticated = HttpRequest.newBuilder(URI.create(base + path)) HttpRequest unauthenticated = HttpRequest.newBuilder(URI.create(base + path))
.header("Authorization", "Bearer Deny") .header("Authorization", "Bearer Deny")
.method(method, HttpRequest.BodyPublishers.noBody()).build(); .method(method, HttpRequest.BodyPublishers.noBody()).build();
assertThat(client.send(unauthenticated, HttpResponse.BodyHandlers.ofString()).statusCode()).as(method + " authentication").isEqualTo(401); HttpResponse<String> denied = client.send(unauthenticated, HttpResponse.BodyHandlers.ofString());
assertThat(denied.statusCode()).as(method + " authentication").isEqualTo(401);
assertErrorBody(denied);
} }
assertThat(json(send("GET", "/history", READER, null, null)).get("executions")).asList().isEmpty(); assertThat(json(send("GET", "/history", READER, null, null)).get("executions")).asList().isEmpty();
} }