Compare commits

..
1 Commits
Author SHA1 Message Date
CircleCI c57521f23c Bump version to 0.1.0-RC.1 [skip ci] 2026-10-15 22:02:37 +00:00
12 changed files with 77 additions and 362 deletions

No files matched your search

+57 -11
View File
@@ -1,14 +1,60 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.8
qqq-orb: kingsrook/qqq-orb@0.6.9
commands:
configure-package-install:
steps:
- run:
name: Keep package installation unattended
command: |
# These build machines are discarded after the job.
sudo install -d /etc/needrestart/conf.d
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
workflows:
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: '0.8'
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
only: /^v\d+\.\d+\.\d+$/
test_only:
jobs:
- qqq-orb/mvn_test_only:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
filters:
branches:
ignore: /(develop|release\/.*)/
publish_snapshot:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: snapshot
filters:
branches:
only: [develop]
publish_release_candidate:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release_candidate
filters:
branches:
only: [/release\/.*/]
publish_release:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release
filters:
branches:
ignore: /.*/
tags:
only: [/^v\d+\.\d+\.\d+$/]
-7
View File
@@ -1,7 +0,0 @@
name: build
on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
-23
View File
@@ -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
+13 -62
View File
@@ -1,74 +1,25 @@
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
# the release line and the release/* push runs CircleCI publish_release_candidate.
name: Release candidate
'on':
on:
workflow_dispatch:
inputs:
dry_run:
description: Plan a cut without changing release state
description: Print the plan without merging or creating branches
type: boolean
default: false
finalize:
description: Retry finalization of an existing cut; do not cut another RC
type: boolean
default: false
recover:
description: Recover the same published RC after a deployment timeout; never
reupload
type: boolean
default: false
circle_build:
description: Original Circle publisher build number (recovery only)
type: string
default: ''
merge_sha:
description: Existing cut merge_sha (finalize retry only)
type: string
default: ''
bump_sha:
description: Existing cut bump_sha (finalize retry only)
type: string
default: ''
branch:
description: Existing cut branch (finalize retry only)
type: string
default: ''
version:
description: Existing cut version (finalize retry only)
type: string
default: ''
pull_request:
types:
- closed
branches:
- develop
repository_dispatch:
types:
- qbit-rc-published
types: [closed]
branches: [develop]
permissions:
contents: write
actions: read
checks: read
jobs:
release-rc:
if: github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch'
|| (github.event_name == 'pull_request' && github.event.pull_request.merged
== true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name,
'release:rc'))
uses: QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml@cc30f599e63c8e41629c99d23395bac1adb8bc44
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
with:
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
}}
finalize: ${{ github.event_name == 'repository_dispatch' || (github.event_name
== 'workflow_dispatch' && inputs.finalize) }}
recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false
}}
circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build
|| '' }}
merge_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.merge_sha
|| inputs.merge_sha || '' }}
bump_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.bump_sha
|| inputs.bump_sha || '' }}
branch: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch
|| inputs.branch || '' }}
version: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version
|| inputs.version || '' }}
secrets: inherit
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
-16
View File
@@ -1,16 +0,0 @@
profile: qbit
orb_version: '0.8'
default_branch: develop
java_version: '21'
browsers: true
api_checks: false
docs_only: false
release_on_main: false
release_tag_policy: stable
test_branch_policy: non-release
publish:
snapshot: true
release_candidate: true
release: true
asciidoc: false
release_completion: true
-1
View File
@@ -5,7 +5,6 @@ All notable changes are documented here. This project follows Keep a Changelog a
## [Unreleased]
### Added
- When the host declares the QQQ `release-notes` service principal (`ReleaseNotesQBitConfig.withServicePrincipal`), detection, drafting and publishing run as that principal instead of the system user. It uses its granted permissions, is audited as `service:release-notes`, and fails closed if unavailable. With no principal declared, behavior is unchanged. Needs the QQQ release with service identities (qqq#1255).
- Release records, items, settings and per-user read state as standard QQQ tables (`release_notes_*`) with a Liquibase changelog (item-to-release foreign key) and seeded settings (require approval, all users, notify).
- Once-per-version detection across pods and restarts (unique app + version), started by the host after startup.
- `ReleaseSource` SPI with `GitHubReleaseSource` (TAG and WINDOW modes; token read from the environment at call time) and `ComponentVersionsSource`.
+3 -4
View File
@@ -18,8 +18,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<qqq.version>4.1.0-RC.26</qqq.version> <!-- feeds qbit-build-parent's qqq-bom-pom import -->
<revision>0.1.0-SNAPSHOT</revision>
<revision>0.1.0-RC.1</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<jacoco.version>0.8.15</jacoco.version>
@@ -30,7 +29,7 @@
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.2.1</version>
<version>2.1.1</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
@@ -220,7 +219,7 @@
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<qqq.snapshot.version>4.1.0-RC.26</qqq.snapshot.version>
<qqq.snapshot.version>4.1.0-RC.13</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -42,7 +42,6 @@ public class ReleaseNotesQBitConfig implements QBitConfig
private static final Pattern RELEASE_VERSION = Pattern.compile("^\\d+\\.\\d+\\.\\d+$");
private String backendName;
private String servicePrincipal = "release-notes";
private String appName = "releaseNotes";
private String parentAppName;
private String permissionBaseName = "release_notes";
@@ -486,26 +485,4 @@ public class ReleaseNotesQBitConfig implements QBitConfig
this.sources = sources;
return (this);
}
/*******************************************************************************
** Getter for servicePrincipal: when the host declares this QQQ service
** principal, release detection, drafting and publishing run as it (its
** granted permissions, audited as service:{name}) instead of the system user.
*******************************************************************************/
public String getServicePrincipal()
{
return (this.servicePrincipal);
}
/*******************************************************************************
** Fluent setter for servicePrincipal (default release-notes).
*******************************************************************************/
public ReleaseNotesQBitConfig withServicePrincipal(String servicePrincipal)
{
this.servicePrincipal = servicePrincipal;
return (this);
}
}
@@ -38,9 +38,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.core.serviceidentity.ServiceIdentities;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@@ -162,18 +160,12 @@ public class ReleaseNotesRuntime implements QSupplementalInstanceMetaData
/*******************************************************************************
** Run qbit work with no signed-in user: as the release-notes service
** principal when the host declares it (QQQ service identities; an
** unavailable principal fails, never falling back), else as the system user
** (previous behavior).
** Run as the system user.
*******************************************************************************/
public <T> T asSystem(Work<T> work) throws Exception
{
AtomicReference<T> result = new AtomicReference<>();
String principal = config.getServicePrincipal();
QSession session = principal != null && ServiceIdentities.isDeclared(instance, principal)
? ServiceIdentities.session(instance, principal) : new QSystemUserSession(instance);
QContext.withTemporaryContext(new CapturedContext(instance, session), () -> result.set(work.run()));
AtomicReference<T> result = new AtomicReference<>();
QContext.withTemporaryContext(new CapturedContext(instance, new QSystemUserSession(instance)), () -> result.set(work.run()));
return (result.get());
}
@@ -1,44 +1,5 @@
components:
schemas:
BasicErrorResponseV1:
description: "The error body of every UI API operation (core and extensions)."
properties:
error:
description: "The error message (the same as message; kept for clients of\
\ UI API 1.0 to 1.5)."
type: "string"
code:
description: "A stable, machine-readable code. Well-known values: BAD_REQUEST\
\ (400), UNAUTHENTICATED (401),\nPERMISSION_DENIED (403), NOT_FOUND (404),\
\ CONFLICT (409), RATE_LIMITED (429), INTERNAL_ERROR (500),\nERROR (other).\
\ Extensions may add codes; treat an unknown code by the HTTP status.\
\ Since 1.6.0."
type: "string"
message:
description: "A message to show the user. Since 1.6.0."
type: "string"
fieldErrors:
description: "Errors about particular request fields, when known. Since\
\ 1.6.0."
items:
properties:
field:
description: "The field (or parameter) name."
type: "string"
message:
description: "The error about that field."
type: "string"
code:
description: "An optional machine-readable code, e.g. REQUIRED, TOO_LONG,\
\ PATTERN."
type: "string"
type: "object"
type: "array"
traceId:
description: "Identifies this request in the server logs (also the X-QQQ-Trace-Id\
\ response header); quote it when reporting a problem. Since 1.6.0."
type: "string"
type: "object"
ReleaseNotesErrorV1:
properties:
error:
@@ -149,42 +110,9 @@ components:
type: "object"
securitySchemes:
sessionUuidCookie:
description: "The session cookie set by POST /qqq/v1/manageSession (web clients)."
in: "cookie"
name: "sessionUUID"
type: "apiKey"
bearerAuth:
bearerFormat: "JWT or session UUID"
description: "`Authorization: Bearer <token>`, where the token is either\n-\
\ an OAuth2 access token (a signed JWT) issued by the app's identity provider\
\ to the app's client,\n or to its native client (`values.nativeClientId`\
\ in GET /qqq/v1/metaData/authentication), for\n example from the device-authorization\
\ grant or auth-code + PKCE. It is verified against the\n provider's keys\
\ (issuer, audience, expiry, signature) and gives the same user and permissions\n\
\ as signing in on the web. When it expires (401), refresh it at the provider\
\ and retry; or\n- a session UUID returned by POST /qqq/v1/manageSession,\
\ which the server keeps refreshed."
scheme: "bearer"
type: "http"
oauth2:
description: "The app's OAuth2 / OpenID Connect provider. The real endpoints\
\ come from the provider's discovery\ndocument: `{values.baseUrl}.well-known/openid-configuration`,\
\ with values from\nGET /qqq/v1/metaData/authentication. Native clients use\
\ `values.nativeClientId` (a public client)\nwith auth-code + PKCE (loopback\
\ or custom-scheme redirect URI registered at the provider) or the\ndevice-authorization\
\ grant (`device_authorization_endpoint`, RFC 8628), then send the access\
\ token\nas `Authorization: Bearer`."
flows:
authorizationCode:
authorizationUrl: "/authorize"
scopes:
email: "Email address"
offline_access: "A refresh token, to renew the access token without signing\
\ in again"
openid: "OpenID Connect sign-in"
profile: "Name, username and groups (permissions come from the groups)"
tokenUrl: "/token"
type: "oauth2"
info:
contact:
email: "contact@kingsrook.com"
@@ -230,44 +158,10 @@ paths:
schema:
$ref: "#/components/schemas/ReleaseNotesReleasesV1"
description: "Successful Response"
403:
content:
application/json:
schema:
$ref: "#/components/schemas/BasicErrorResponseV1"
description: "The user (or the credential's scope) may not do this (code\
\ PERMISSION_DENIED)."
404:
content:
application/json:
schema:
$ref: "#/components/schemas/BasicErrorResponseV1"
description: "Not found, or not visible to this user (code NOT_FOUND)."
409:
content:
application/json:
schema:
$ref: "#/components/schemas/BasicErrorResponseV1"
description: "Conflict with the current state, e.g. an edit conflict or\
\ an invalid transition (code CONFLICT)."
429:
content:
application/json:
schema:
$ref: "#/components/schemas/BasicErrorResponseV1"
description: "Too many requests (code RATE_LIMITED); retry later."
security:
- sessionUuidCookie:
- "N/A"
- bearerAuth:
- "N/A"
- oauth2:
- "openid"
- "profile"
- "email"
summary: "Published release notes"
tags:
- "release-notes"
/qqq/v1/qbit/release-notes/summary:
get:
description: "How many published releases are new to the signed-in user, and\
@@ -292,44 +186,10 @@ paths:
schema:
$ref: "#/components/schemas/ReleaseNotesSummaryV1"
description: "Successful Response"
403:
content:
application/json:
schema:
$ref: "#/components/schemas/BasicErrorResponseV1"
description: "The user (or the credential's scope) may not do this (code\
\ PERMISSION_DENIED)."
404:
content:
application/json:
schema:
$ref: "#/components/schemas/BasicErrorResponseV1"
description: "Not found, or not visible to this user (code NOT_FOUND)."
409:
content:
application/json:
schema:
$ref: "#/components/schemas/BasicErrorResponseV1"
description: "Conflict with the current state, e.g. an edit conflict or\
\ an invalid transition (code CONFLICT)."
429:
content:
application/json:
schema:
$ref: "#/components/schemas/BasicErrorResponseV1"
description: "Too many requests (code RATE_LIMITED); retry later."
security:
- sessionUuidCookie:
- "N/A"
- bearerAuth:
- "N/A"
- oauth2:
- "openid"
- "profile"
- "email"
summary: "What's new summary"
tags:
- "release-notes"
/qqq/v1/qbit/release-notes/seen:
post:
description: "Clears the signed-in user's new indicator. Takes no body."
@@ -353,47 +213,7 @@ paths:
schema:
$ref: "#/components/schemas/ReleaseNotesSeenV1"
description: "Successful Response"
403:
content:
application/json:
schema:
$ref: "#/components/schemas/BasicErrorResponseV1"
description: "The user (or the credential's scope) may not do this (code\
\ PERMISSION_DENIED)."
404:
content:
application/json:
schema:
$ref: "#/components/schemas/BasicErrorResponseV1"
description: "Not found, or not visible to this user (code NOT_FOUND)."
409:
content:
application/json:
schema:
$ref: "#/components/schemas/BasicErrorResponseV1"
description: "Conflict with the current state, e.g. an edit conflict or\
\ an invalid transition (code CONFLICT)."
429:
content:
application/json:
schema:
$ref: "#/components/schemas/BasicErrorResponseV1"
description: "Too many requests (code RATE_LIMITED); retry later."
security:
- sessionUuidCookie:
- "N/A"
- bearerAuth:
- "N/A"
- oauth2:
- "openid"
- "profile"
- "email"
summary: "Mark What's new as seen"
tags:
- "release-notes"
servers:
- description: "The QQQ server that serves this document (paths include /qqq/v1)."
url: "/"
tags:
- description: "Operations of the release-notes extension."
name: "release-notes"
@@ -45,8 +45,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.core.serviceidentity.QServicePrincipalMetaData;
import com.kingsrook.qqq.backend.core.serviceidentity.ServiceIdentitiesMetaData;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qbits.releasenotes.ReleaseNotesTestSupport.NOTIFICATIONS;
import static com.kingsrook.qbits.releasenotes.ReleaseNotesTestSupport.pr;
@@ -99,24 +97,6 @@ class ReleaseLifecycleTest
/*******************************************************************************
** With the release-notes principal declared, qbit work runs as it, never
** as the system user.
*******************************************************************************/
@Test
void detectsAsTheDeclaredPrincipal() throws Exception
{
QInstance instance = ReleaseNotesTestSupport.instance("1.2.3", CHANGES);
ReleaseNotesRuntime runtime = ReleaseNotesTestSupport.runtime(instance, "2026-10-15T12:00:00Z");
ServiceIdentitiesMetaData.declare(instance, new QServicePrincipalMetaData().withName("release-notes"));
String[] ranAs = new String[1];
runtime.asSystem(() -> ranAs[0] = QContext.getQSession().getUser().getIdReference());
assertThat(ranAs[0]).isEqualTo("service:release-notes");
assertThat(runtime.service().detect()).isNotNull();
}
/*******************************************************************************
** Pods starting together record the version exactly once.
*******************************************************************************/
@@ -166,10 +166,7 @@ class ReleaseNotesMetaDataTest
assertThat(UiApiContract.check(generated, Files.readString(path), null, 1, "mvnw -Dtest=ReleaseNotesMetaDataTest -Dreleasenotes.openapi.update=true test")).isEmpty();
var model = version.generateExtensionOpenAPIModel("qqq", extension);
assertThat(model.getPaths().keySet()).containsExactlyInAnyOrder("/qqq/v1/qbit/release-notes/releases", "/qqq/v1/qbit/release-notes/summary", "/qqq/v1/qbit/release-notes/seen");
///////////////////////////////////////////////////////////////////////
// QQQ UI API 1.6.0 adds the shared error schema to every extension. //
///////////////////////////////////////////////////////////////////////
assertThat(model.getComponents().getSchemas().keySet()).allMatch(name -> name.equals("BasicErrorResponseV1") || (name.startsWith("ReleaseNotes") && name.endsWith("V1")));
assertThat(model.getComponents().getSchemas().keySet()).allMatch(name -> name.startsWith("ReleaseNotes") && name.endsWith("V1"));
assertThat(generated).doesNotContain("sourceRef", "sourceLog", "commitSha", "componentVersions");
QInstance instance = ReleaseNotesTestSupport.instance("1.0.0", List.of());
assertThat(MiddlewareSpecExtensionsMetaData.of(instance).getExtensions()).containsOnlyKeys("release-notes");