Compare commits

..
Author SHA1 Message Date
CircleCI 972a4c325e Bump version to 0.1.0-RC.15 [skip ci] 2026-10-15 21:20:53 +00:00
james.maesandClaude Opus 5.5 bf2eed3db7 Merge develop into release/0.1 for 0.1.0-RC.15 (#44 UI API 1.6.0 extension, QQQ RC.15)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:16:17 -05:00
CircleCI 70b606a3bd Bump version to 0.1.0-RC.14 [skip ci] 2026-10-15 12:51:30 +00:00
github-actions[bot] 29fc27a51c Merge develop into release/0.1 for the next 0.1.0 RC 2026-10-15 12:48:58 +00:00
CircleCI ec03b810be Bump version to 0.1.0-RC.13 [skip ci] 2026-10-15 12:41:30 +00:00
github-actions[bot] eddbe20fd7 Merge develop into release/0.1 for the next 0.1.0 RC 2026-10-15 12:38:37 +00:00
CircleCI 17017cc693 Bump version to 0.1.0-RC.12 [skip ci] 2026-10-15 02:14:59 +00:00
james.maesandClaude Opus 5.5 1bbdff2fd9 Merge develop into release/0.1 for 0.1.0-RC.12 (#38 help bundle)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:10:04 -05:00
CircleCI 1c5aa5d33b Bump version to 0.1.0-RC.11 [skip ci] 2026-10-14 23:58:00 +00:00
james.maesandClaude Opus 5.5 1a911284ef Merge develop into release/0.1 for 0.1.0-RC.11 (#37 email REQUESTED recipients immediately)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:54:26 -05:00
CircleCI a4116b3c75 Bump version to 0.1.0-RC.10 [skip ci] 2026-10-14 23:08:43 +00:00
james.maesandClaude Opus 5.5 6c3fb450eb Merge develop into release/0.1 for 0.1.0-RC.10 (#33 duplicate notifications, #32, #35 indexes and FKs)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:03:54 -05:00
CircleCI 7f51d8ee93 Bump version to 0.1.0-RC.9 [skip ci] 2026-10-14 22:38:07 +00:00
james.maesandClaude Opus 5.5 cc297f649a Merge develop into release/0.1 for 0.1.0-RC.9 (#36 seeds, Setup card, coverage wiring)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:33:42 -05:00
CircleCI 289acee3cc Bump version to 0.1.0-RC.8 [skip ci] 2026-10-14 21:49:55 +00:00
james.maesandClaude Opus 5.5 8ca9fff6a1 Merge develop into release/0.1 for 0.1.0-RC.8 (#18 Notifications preferences section, QQQ RC.7)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:45:03 -05:00
CircleCI c67e1a343d Bump version to 0.1.0-RC.7 [skip ci] 2026-10-14 21:24:49 +00:00
james.maesandClaude Opus 5.5 4fe1db3462 Merge develop into release/0.1 for 0.1.0-RC.7 (#34 child lists JSON serialization fix)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:20:57 -05:00
CircleCI 27d471a8e1 Bump version to 0.1.0-RC.6 [skip ci] 2026-10-14 20:24:26 +00:00
james.maesandClaude Opus 5.5 ee54fc4ba5 Merge develop into release/0.1 for 0.1.0-RC.6 (per-user Slack delivery)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:21:06 -05:00
CircleCI bba86368d9 Bump version to 0.1.0-RC.5 [skip ci] 2026-10-14 16:22:13 +00:00
james.maesandClaude Opus 5.5 fb88168107 Merge develop into release/0.1 for 0.1.0-RC.5 (#23 #26 #28 #29 #30)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:17:45 -05:00
CircleCI 6d4b80793b Bump version to 0.1.0-RC.4 [skip ci] 2026-10-14 12:51:58 +00:00
james.maesandClaude Opus 5.5 da5aba1f39 Merge develop into release/0.1 for 0.1.0-RC.4 (#19 #21 #24)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:48:10 -05:00
CircleCI dbbcb97baf Bump version to 0.1.0-RC.3 [skip ci] 2026-10-14 00:51:41 +00:00
james.maesandClaude Opus 5.5 a84a10a4cd Merge develop into release/0.1 for 0.1.0-RC.3 (core QAlerts sink and WebhookSigner, #17)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:49:04 -05:00
CircleCI dae98dae82 Bump version to 0.1.0-RC.2 [skip ci] 2026-10-13 16:33:36 +00:00
james.maesandClaude Opus 5.5 aede554b25 Merge develop into release/0.1 for 0.1.0-RC.2
User notifications: spec (#8), tables, admin, audiences and bell API
(#9), job-failure notices (#10), security/share/release notices, email
digest and link hardening (#11).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:31:35 -05:00
CircleCI 567b4833fc Bump version to 0.1.0-RC.1 [skip ci] 2026-10-12 22:57:11 +00:00
11 changed files with 94 additions and 226 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.7.12
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.7.12
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+$/]
-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.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
@@ -5,7 +5,6 @@ All notable changes to this project are documented here. The format follows Keep
## [0.1.0] - Unreleased
### Fixed
- Under OAuth2, per-recipient checks (alert visibility for external channels, follower record access, audience sessions) never worked: they used token-less automated sessions, which OAuth2 rejects, so external sends fell back to the conservative rule. When the host declares the QQQ `alerts` service principal (`AlertsQBitConfig.withServicePrincipal`, default `alerts`), these checks run on the recipient's behalf through it: the recipient's current access intersected with the principal's. Alert recording and routing bookkeeping stay on the system session, so alerting does not depend on the identity provider being up. With no principal declared, behavior is unchanged. Needs the QQQ release with service identities (qqq#1255, #1256).
- Audit noise: every monitor run (lease claim, then run result) and every reminder check wrote a "Record was Edited" audit (about 7,000 `alerts_monitor` and 1,800 `alerts_alert` audits in 6 hours on one production host, most with no field details). Engine updates that touch only bookkeeping fields (`RuntimeFields`: monitor `claimedBy`, `claimedUntil`, `lastRunAt`, `nextRunAt`, `lastStatus`, `lastValue`, `lastError`, `lastElapsedMs`, `consecutiveFailures`, `conditionSince`; alert `timedNotificationCheckedAt`, `lastNotifiedAt`, `recordRefCount`) are written with `omitDmlAudit`. Configuration edits, raises and alert state changes are audited as before, as are UI and API edits of any field.
- Read the QBit metadata version from the Maven-filtered artifact version so release builds report their release version.
- Record-view child lists (e.g. a notification's Recipients) failed with "Error in JSON Serialization": the child table's meta-data includes the qbit config, and `AlertsQBitConfig.getRuntime()` points at a runtime that points back at the config, so Jackson recursed to its 1000-level nesting limit. `getRuntime()` and `getAudienceResolver()` are now `@JsonIgnore`, as in the other qbits' configs. A regression test renders and serializes every child-list widget.
+2 -2
View File
@@ -18,7 +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>
<revision>0.1.0-SNAPSHOT</revision>
<revision>0.1.0-RC.15</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<jacoco.version>0.8.15</jacoco.version>
@@ -267,7 +267,7 @@
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<qqq.snapshot.version>4.1.0-RC.19</qqq.snapshot.version>
<qqq.snapshot.version>4.1.0-RC.15</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -80,7 +80,6 @@ public class AlertsQBitConfig implements QBitConfig
private Boolean legacyAlertmanagerPath = false;
private String backendName;
private String servicePrincipal = "alerts";
private String schedulerName;
private String messagingProviderName;
private String fromAddress;
@@ -894,30 +893,4 @@ public class AlertsQBitConfig implements QBitConfig
this.legacyAlertmanagerPath = legacyAlertmanagerPath;
return (this);
}
/*******************************************************************************
** Getter for servicePrincipal: when the host declares this QQQ service
** principal, per-recipient checks (alert visibility, follower record access,
** audience) run on the recipient's behalf through it - their current access
** intersected with the principal's - instead of a token-less automated
** session (rejected under OAuth2). Alert recording and routing bookkeeping
** stay on the system session so alerting never depends on the identity
** provider being up.
*******************************************************************************/
public String getServicePrincipal()
{
return (this.servicePrincipal);
}
/*******************************************************************************
** Fluent setter for servicePrincipal (default alerts).
*******************************************************************************/
public AlertsQBitConfig withServicePrincipal(String servicePrincipal)
{
this.servicePrincipal = servicePrincipal;
return (this);
}
}
@@ -26,7 +26,6 @@ import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
@@ -35,9 +34,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.serviceidentity.ServiceIdentities;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -311,25 +307,4 @@ public final class AlertsSecurity
{
return (QContext.getQSession() instanceof QSystemUserSession);
}
/*******************************************************************************
** A session for a recipient: on their behalf through the alerts service
** principal when the host declares it (QQQ service identities), else the
** authentication module's automated session (previous behavior). Null when
** neither can be made - callers fail closed.
*******************************************************************************/
public static QSession recipientSession(QInstance qInstance, String principalName, String userId) throws QException
{
if(qInstance == null || qInstance.getAuthentication() == null)
{
return (null);
}
if(StringUtils.hasContent(principalName) && ServiceIdentities.isDeclared(qInstance, principalName))
{
return (ServiceIdentities.sessionOnBehalfOf(qInstance, principalName, userId));
}
QAuthenticationModuleInterface module = new QAuthenticationModuleDispatcher().getQModule(qInstance.getAuthentication());
return (module.createAutomatedSessionForUser(qInstance, userId));
}
}
@@ -66,6 +66,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.collaboration.FollowEvent;
import com.kingsrook.qqq.backend.core.model.metadata.collaboration.RecordFollower;
import com.kingsrook.qqq.backend.core.model.metadata.collaboration.RecordReference;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@@ -880,12 +882,18 @@ public class AlertRouter
/*******************************************************************************
** A session for a user: on their behalf via the alerts service principal
** when declared, else the authentication module's (null when neither).
** A session for a user, from the instance's authentication module (null
** when it can't make one).
*******************************************************************************/
private QSession automatedSession(String userId) throws QException
{
return (AlertsSecurity.recipientSession(runtime.getQInstance(), runtime.getConfig().getServicePrincipal(), userId));
QInstance qInstance = runtime.getQInstance();
if(qInstance.getAuthentication() == null)
{
return (null);
}
QAuthenticationModuleInterface module = new QAuthenticationModuleDispatcher().getQModule(qInstance.getAuthentication());
return (module.createAutomatedSessionForUser(qInstance, userId));
}
@@ -26,7 +26,6 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qbits.alerts.AlertsRuntime;
import com.kingsrook.qbits.alerts.AlertsSecurity;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.alerts.NotificationAudience;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@@ -34,6 +33,8 @@ 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.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@@ -217,14 +218,20 @@ public class AlertsAudienceResolver
/*******************************************************************************
** A session for a user: on their behalf via the alerts service principal
** when declared, else the authentication module's (null when neither).
** A session for a user from the instance's authentication module (null
** when it can't make one).
*******************************************************************************/
private QSession automatedSession(String userId)
{
try
{
return (AlertsSecurity.recipientSession(runtime.getQInstance(), runtime.getConfig().getServicePrincipal(), userId));
QInstance qInstance = runtime.getQInstance();
if(qInstance.getAuthentication() == null)
{
return (null);
}
QAuthenticationModuleInterface module = new QAuthenticationModuleDispatcher().getQModule(qInstance.getAuthentication());
return (module.createAutomatedSessionForUser(qInstance, userId));
}
catch(Exception e)
{
@@ -1,52 +0,0 @@
/*
* Copyright 2026 Kingsrook, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.alerts;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.serviceidentity.QDelegatedSession;
import com.kingsrook.qqq.backend.core.serviceidentity.QServicePrincipalMetaData;
import com.kingsrook.qqq.backend.core.serviceidentity.ServiceIdentities;
import com.kingsrook.qqq.backend.core.serviceidentity.ServiceIdentitiesMetaData;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Per-recipient checks run on the recipient's behalf through the alerts
** service principal when the host declares it; otherwise unchanged.
*******************************************************************************/
class AlertsServicePrincipalTest extends BaseAlertsTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void recipientSessionsGoThroughTheDeclaredPrincipal() throws Exception
{
QSession before = AlertsSecurity.recipientSession(qInstance, config.getServicePrincipal(), "alice");
assertThat(before).isNotInstanceOf(QDelegatedSession.class);
assertThat(before.getUser().getIdReference()).isEqualTo("alice");
ServiceIdentitiesMetaData.declare(qInstance, new QServicePrincipalMetaData().withName("alerts").withMayActOnBehalfOfUsers(true));
QSession after = AlertsSecurity.recipientSession(qInstance, config.getServicePrincipal(), "alice");
assertThat(after).isInstanceOf(QDelegatedSession.class);
assertThat(after.getUser().getIdReference()).isEqualTo("alice");
assertThat(ServiceIdentities.principalOf(after)).isEqualTo("alerts");
assertThat(ServiceIdentities.attributionName(after)).endsWith("via service:alerts");
}
}