Compare commits

...
Author SHA1 Message Date
james.maesandCodex 4490545c4f fix: accept leading-dash Mongo credentials in Compose
Use explicit username/password option assignments in health and replica-set initialization. Reproduced the original failure and verified authenticated ping with the corrected arguments against mongo:7 and a leading-dash fixture password.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-20 21:48:30 -05:00
james.maesandCodex e13bda1d8a test: verify the HttpOnly OIDC session contract
Current QQQ omits the session UUID from JSON for HttpOnly cookies. Require the nonempty HttpOnly cookie and reject exposed UUIDs, retaining authenticated backend and permission checks.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-20 21:37:27 -05:00
james.maesandCodex 654e1c265a fix: use shared SDK v2 S3 support for MinIO
Replace obsolete SDK v1 client overrides with upstream endpoint and path-style metadata. Preserve endpoint coverage for every filesystem action and local HTTP listing/write assertions.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-20 21:26:34 -05:00
james.maesandCodex 718ca93b0c ci: centralize All builds smoke checks and releases
Use orb 0.7.9 and its immutable shared release worker. Build once, preserve core/full verification and conditional public GHCR delivery, and consume public Circle provenance without exporting a token.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-20 21:18:33 -05:00
15 changed files with 91 additions and 473 deletions

No files matched your search

+10 -24
View File
@@ -1,28 +1,14 @@
---
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.6.8
commands:
configure-package-install:
steps:
- run:
name: Keep package installation unattended
command: |
# These build machines are discarded after the job.
# Use Ubuntu's archive mirror, which also carries the signed security suite.
sudo sed -i 's|http://security.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list
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
qqq-orb: kingsrook/qqq-orb@0.7.9
workflows:
# Build and unit tests on every branch push. Dependencies are public
# (Maven Central and the Central snapshot repository), so no context is needed.
test_only:
setup:
jobs:
- qqq-orb/mvn_test_only:
pre-steps:
- configure-package-install
- qqq-orb/generate_pipeline:
orb_version: 0.7.9
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
only: /.*/
-48
View File
@@ -1,48 +0,0 @@
name: Publish develop image
on:
push:
branches: [develop]
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check public package visibility
id: package_visibility
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if visibility="$(gh api /orgs/QRun-IO/packages/container/qqq-all --jq .visibility 2>/dev/null)" && [ "$visibility" = public ]; then
echo 'publish=true' >> "$GITHUB_OUTPUT"
echo 'The qqq-all container package is public; develop image publication is enabled.' >> "$GITHUB_STEP_SUMMARY"
else
echo 'publish=false' >> "$GITHUB_OUTPUT"
echo 'Skipped develop image publication: the qqq-all container package is not yet public or cannot be read. An owner must bootstrap the package and set it to Public before C9 enables publication.' >> "$GITHUB_STEP_SUMMARY"
fi
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven
- name: Verify application
run: mvn -B -ntp verify
- uses: docker/login-action@v3
if: steps.package_visibility.outputs.publish == 'true'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and publish develop image
if: steps.package_visibility.outputs.publish == 'true'
run: |
docker build \
--label org.opencontainers.image.source=https://github.com/QRun-IO/qqq-all \
--tag ghcr.io/qrun-io/qqq-all:develop .
docker push ghcr.io/qrun-io/qqq-all:develop
+33 -159
View File
@@ -1,179 +1,53 @@
name: Release 4.1.0
on:
'on':
workflow_dispatch:
inputs:
version:
description: Reviewed source version (GA or exact coordinated RC)
required: true
type: choice
default: '4.1.0'
default: 4.1.0
options:
- '4.1.0'
- '4.1.0-RC.1'
- 4.1.0
- 4.1.0-RC.1
release_sha:
description: Full develop commit SHA already signed and tagged with the selected version
description: Full develop commit SHA already signed and tagged with the selected
version
required: true
type: string
smoke_run_id:
description: Successful Smoke workflow run ID for that exact develop SHA
required: true
required: false
type: string
default: ''
smoke_evidence:
description: Evidence provider; github preserves the original Smoke workflow
validation
type: choice
default: circle
options:
- github
- circle
required: true
circle_workflow_id:
description: Exact successful Circle workflow UUID when circle evidence is
selected
type: string
required: false
default: ''
permissions:
actions: read
contents: write
packages: write
concurrency:
group: qqq-all-release-4.1.0
cancel-in-progress: false
checks: read
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 60
environment: qqq-all-release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_SHA: ${{ inputs.release_sha }}
SMOKE_RUN_ID: ${{ inputs.smoke_run_id }}
RELEASE_ENABLED: ${{ vars.QQQ_ALL_RELEASE_ENABLED }}
LICENSE_CLEARED: ${{ vars.QQQ_ALL_RELEASE_LICENSE_APPROVED }}
VERSION: ${{ inputs.version }}
TAG: ${{ inputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Check manual release controls and exact tag
shell: bash
run: |
set -euo pipefail
test "$GITHUB_REF" = refs/heads/develop
test "$RELEASE_ENABLED" = true
test "$LICENSE_CLEARED" = true
case "$VERSION" in 4.1.0|4.1.0-RC.1) ;; *) exit 1 ;; esac
[[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$SMOKE_RUN_ID" =~ ^[0-9]+$ ]]
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
test "$RELEASE_SHA" = "$GITHUB_SHA"
test "$(git cat-file -t "refs/tags/$TAG")" = tag
test "$(git rev-parse "refs/tags/$TAG^{commit}")" = "$RELEASE_SHA"
gh api "repos/$GITHUB_REPOSITORY/git/tags/$(git rev-parse "refs/tags/$TAG")" > "$RUNNER_TEMP/tag-object.json"
- name: Verify upstream release, signed tag, exact-commit smoke, public pins, and public GHCR
shell: bash
run: |
set -euo pipefail
gh api "repos/QRun-IO/qqq/releases/tags/v$VERSION" > "$RUNNER_TEMP/upstream-release.json"
next_version="$(python3 -c 'import sys; sys.path.insert(0, "scripts"); from release_gate import properties, validate_poms; from pathlib import Path; import os; validate_poms(Path("."), os.environ["VERSION"]); print(properties(Path("qqq-all-bom/pom.xml"))["qqq-frontend-next.version"])')"
gh api "repos/QRun-IO/qqq-frontend-next/releases/tags/v$next_version" > "$RUNNER_TEMP/next-release.json"
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SMOKE_RUN_ID" > "$RUNNER_TEMP/smoke-run.json"
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SMOKE_RUN_ID/jobs?per_page=100" > "$RUNNER_TEMP/smoke-jobs.json"
central="https://repo.maven.apache.org/maven2/com/kingsrook/qqq"
for artifact in qqq-all-parent qqq-all-bom; do
curl --fail --silent --show-error \
"$central/$artifact/$VERSION/$artifact-$VERSION.pom" \
--output "$RUNNER_TEMP/$artifact.pom"
done
python3 scripts/release_gate.py --root . --version "$VERSION" --sha "$RELEASE_SHA" \
--smoke-run "$RUNNER_TEMP/smoke-run.json" \
--smoke-jobs "$RUNNER_TEMP/smoke-jobs.json" \
--published-parent "$RUNNER_TEMP/qqq-all-parent.pom" \
--published-bom "$RUNNER_TEMP/qqq-all-bom.pom" \
--upstream-release "$RUNNER_TEMP/upstream-release.json" \
--next-release "$RUNNER_TEMP/next-release.json" \
--tag-object "$RUNNER_TEMP/tag-object.json" \
--tag-sha "$(git rev-parse "refs/tags/$TAG")" \
--public-inputs "$RUNNER_TEMP/public-inputs"
test "$(gh api /orgs/QRun-IO/packages/container/qqq-all --jq .visibility)" = public
gh api --paginate 'orgs/QRun-IO/packages/container/qqq-all/versions?per_page=100' \
--jq '.[] | .metadata.container.tags[]?' > "$RUNNER_TEMP/ghcr-tags.txt"
if grep -Fxq "$VERSION" "$RUNNER_TEMP/ghcr-tags.txt"; then
echo "GHCR tag $VERSION already exists" >&2
exit 1
fi
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven
- name: Verify release reactor
shell: bash
run: |
set -euo pipefail
printf '<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"/>\n' > "$RUNNER_TEMP/public-settings.xml"
python3 -m unittest discover -s scripts -p 'test_*.py' -v
mvn -B -ntp -s "$RUNNER_TEMP/public-settings.xml" -gs "$RUNNER_TEMP/public-settings.xml" \
-Dmaven.repo.local="$RUNNER_TEMP/release-m2" verify \
org.apache.maven.plugins:maven-enforcer-plugin:3.5.0:enforce -Drules=requireReleaseDeps
- name: Recheck develop, annotated tag, and release absence before publication
shell: bash
run: |
set -euo pipefail
test "$(gh api "repos/$GITHUB_REPOSITORY/branches/develop" --jq .commit.sha)" = "$RELEASE_SHA"
test "$(git ls-remote origin "refs/tags/$TAG" | cut -f1)" = "$(git rev-parse "refs/tags/$TAG")"
test "$(git ls-remote origin "refs/tags/$TAG^{}" | cut -f1)" = "$RELEASE_SHA"
if release_check="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$TAG" 2>&1)"; then
echo "GitHub release $TAG already exists" >&2
exit 1
elif [[ "$release_check" != *"HTTP 404"* ]]; then
echo "$release_check" >&2
exit 1
fi
gh api --paginate 'orgs/QRun-IO/packages/container/qqq-all/versions?per_page=100' \
--jq '.[] | .metadata.container.tags[]?' > "$RUNNER_TEMP/ghcr-tags-final.txt"
if grep -Fxq "$VERSION" "$RUNNER_TEMP/ghcr-tags-final.txt"; then
echo "GHCR tag $VERSION already exists" >&2
exit 1
fi
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish versioned image
shell: bash
run: |
set -euo pipefail
image="ghcr.io/qrun-io/qqq-all:$VERSION"
docker build \
--label org.opencontainers.image.source=https://github.com/QRun-IO/qqq-all \
--label "org.opencontainers.image.version=$VERSION" \
--label "org.opencontainers.image.revision=$RELEASE_SHA" \
--tag "$image" .
docker push "$image"
printf 'Published image: %s\n' "$image" >> "$GITHUB_STEP_SUMMARY"
- name: Build image Compose distribution bundle
shell: bash
run: |
set -euo pipefail
image="$(docker image inspect --format '{{index .RepoDigests 0}}' "ghcr.io/qrun-io/qqq-all:$VERSION")"
python3 scripts/release_bundle.py --root . --version "$VERSION" --sha "$RELEASE_SHA" \
--image "$image" --output "$RUNNER_TEMP"
- name: Publish GitHub release with Compose bundle
shell: bash
run: |
set -euo pipefail
release_flags=()
case "$VERSION" in
4.1.0) ;;
4.1.0-RC.1) release_flags=(--prerelease --latest=false) ;;
*) exit 1 ;;
esac
gh release create "$TAG" "$RUNNER_TEMP/qqq-all-$VERSION.tar.gz" \
"$RUNNER_TEMP/qqq-all-$VERSION/compose.yaml" --repo "$GITHUB_REPOSITORY" \
--verify-tag --title "qqq-all $VERSION" --notes-from-tag "${release_flags[@]}"
- uses: actions/upload-artifact@v4
if: always()
with:
name: release-evidence-${{ inputs.version }}
path: |
${{ runner.temp }}/public-inputs/receipts.json
${{ runner.temp }}/tag-object.json
${{ runner.temp }}/upstream-release.json
${{ runner.temp }}/next-release.json
${{ runner.temp }}/smoke-run.json
${{ runner.temp }}/smoke-jobs.json
${{ runner.temp }}/qqq-all-${{ inputs.version }}/release.json
uses: QRun-IO/qqq-orb/.github/workflows/qqq-all-release.yml@65265e48a4176d8fb0f4f744e2919bad587bd5ca
with:
version: ${{ inputs.version }}
release_sha: ${{ inputs.release_sha }}
smoke_run_id: ${{ inputs.smoke_run_id }}
smoke_evidence: ${{ inputs.smoke_evidence }}
circle_workflow_id: ${{ inputs.circle_workflow_id }}
secrets:
CIRCLECI_TOKEN: ${{ secrets.CIRCLECI_TOKEN }}
-58
View File
@@ -1,58 +0,0 @@
name: Smoke
on:
pull_request:
push:
branches: [develop]
workflow_dispatch:
permissions:
contents: read
jobs:
core:
runs-on: ubuntu-latest
timeout-minutes: 25
env:
PYTHONDONTWRITEBYTECODE: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven
- name: Check smoke assertions
run: python3 -m unittest discover -s scripts -p 'test_*.py' -v
- name: Verify and package core app
run: mvn -B -ntp -pl qqq-all-app -am verify
- name: Smoke packaged core over HTTP
run: python3 scripts/run_core_smoke.py qqq-all-app/target/qqq-all-app.jar
full:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
PYTHONDONTWRITEBYTECODE: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven
- name: Package full app for Docker image
run: >-
mvn -B -ntp -pl qqq-all-app -am -DskipTests
dependency:build-classpath -DincludeGroupIds=com.kingsrook.qqq,com.kingsrook.qbits
-Dmdep.outputFile=target/smoke-classpath.txt package
- name: Smoke full Compose over HTTP
run: python3 scripts/run_full_smoke.py
- name: Retain sanitized full smoke diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: full-smoke-diagnostics-${{ github.run_id }}-${{ github.run_attempt }}
path: target/smoke-diagnostics/*.json
if-no-files-found: warn
retention-days: 14
+6
View File
@@ -0,0 +1,6 @@
profile: all
orb_version: 0.7.9
default_branch: develop
publish:
snapshot: true
publish_context: qqq-maven-registry-credentials
+1 -1
View File
@@ -41,7 +41,7 @@ Full adds sample tables on PostgreSQL, MySQL, MongoDB, MinIO S3, and SFTP. It in
To stop the full stack, run `docker compose --profile full down`. Add `--volumes` only when you intend to erase demo data. For a fresh Artemis volume, the configured login is required; old volumes created for anonymous access should be recreated. This reference stack uses demo credentials and is not a production deployment.
The [develop image workflow](.github/workflows/publish-image.yml) verifies the build and publishes to GHCR only after an organization owner bootstraps the container package and sets its visibility to public. Until then, image publication is skipped and local Compose remains available.
The [canonical QQQ pipeline](.circleci/config.yml), configured by [.qqq-ci.yml](.qqq-ci.yml), builds once and runs core/full smoke checks through the shared orb. Its develop-image job publishes to GHCR only after the container package exists and its visibility is public. Until then, image publication is skipped and local Compose remains available.
The [4.1.0 release procedure](docs/release.md) describes the separate Maven parent/BOM publication path and the manually gated, exact-commit [release workflow](.github/workflows/release.yml). The workflow will not release the current snapshot build.
+1 -1
View File
@@ -124,7 +124,7 @@ services:
- mongo-config:/data/configdb
- ./infra/mongo/start.sh:/start.sh:ro
healthcheck:
test: ["CMD-SHELL", "mongosh --quiet -u \"$$MONGO_INITDB_ROOT_USERNAME\" -p \"$$MONGO_INITDB_ROOT_PASSWORD\" --authenticationDatabase admin --eval 'db.adminCommand({ping:1}).ok' | grep -q 1"]
test: ["CMD-SHELL", "mongosh --quiet --username=\"$$MONGO_INITDB_ROOT_USERNAME\" --password=\"$$MONGO_INITDB_ROOT_PASSWORD\" --authenticationDatabase admin --eval 'db.adminCommand({ping:1}).ok' | grep -q 1"]
interval: 5s
timeout: 5s
retries: 20
+5 -5
View File
@@ -6,9 +6,9 @@ This procedure prepares a release; this repository currently remains on `4.1.0-S
The same workflow accepts only `4.1.0` (default GA path) or exact `4.1.0-RC.1`. For the [coordinated RC chain](https://github.com/QRun-IO/qqq/issues/921), first publish and verify the accepted Next candidate, QQQ `v4.1.0-RC.1`, and the corresponding build parent/eight QBits. Then review actual source pins: root `revision` and BOM `qqq.version` both `4.1.0-RC.1`, explicit accepted Next version, and immutable published QBit versions. No command-line version overrides substitute for source edits. The current RC8/snapshot pins are intentionally unchanged and cannot pass release validation. Acceptance limitations belong to the exact accepted pair; the RC8 notes below are historical evidence, not automatic approval for a newer pair.
Use the existing `release-central` profile and the parent/BOM-only command below to stage the exact RC POMs for **manual** Central review (`autoPublish=false`). Its enforcers accept only GA or exact RC1, require the QQQ pin to equal the source revision, and reject snapshot/range/LATEST dependency pins. After both POMs are public and the reviewed source is merged, require successful push `Smoke` core/full on that exact develop SHA and an annotated signed tag named `4.1.0-RC.1` at it. The workflow requires GitHub's tag verification to report `verified=true`, `reason=valid`, and checks the exact local/remote tag object and commit. Public QQQ/Next release tags must match their pins and prerelease state; first-party BOM inputs (including recursive public parents) must resolve anonymously, with qbit-build-parent importing this QQQ version. The full reactor also rejects transitive snapshot dependencies. Public POM comparison includes the explicit Next pin. Existing GHCR visibility, protected environment, license/enable controls, destination-absence checks and live ref rechecks remain required.
Use the existing `release-central` profile and the parent/BOM-only command below to stage the exact RC POMs for **manual** Central review (`autoPublish=false`). Its enforcers accept only GA or exact RC1, require the QQQ pin to equal the source revision, and reject snapshot/range/LATEST dependency pins. After both POMs are public and the reviewed source is merged, require a successful CircleCI `verify-and-smoke` workflow with `build-and-test`, `core` and `full` on that exact develop SHA and an annotated signed tag named `4.1.0-RC.1` at it. The workflow requires GitHub's tag verification to report `verified=true`, `reason=valid`, and checks the exact local/remote tag object and commit. Public QQQ/Next release tags must match their pins and prerelease state; first-party BOM inputs (including recursive public parents) must resolve anonymously, with qbit-build-parent importing this QQQ version. The full reactor also rejects transitive snapshot dependencies. Public POM comparison includes the explicit Next pin. Existing GHCR visibility, protected environment, license/enable controls, destination-absence checks and live ref rechecks remain required.
When those gates and owner authorization are satisfied, dispatch **Release 4.1.0** on develop with `version=4.1.0-RC.1`, the full `release_sha`, and its successful push `smoke_run_id`. RC publication uses `--prerelease --latest=false`; it never becomes the latest GA release. The default `version=4.1.0` still requires upstream non-prerelease GA and follows the separate GA requirements below. Do not dispatch while any pin or external prerequisite is pending.
When those gates and owner authorization are satisfied, dispatch **Release 4.1.0** on develop with `version=4.1.0-RC.1`, the full `release_sha`, `smoke_evidence=circle`, and its successful `circle_workflow_id`. RC publication uses `--prerelease --latest=false`; it never becomes the latest GA release. The default `version=4.1.0` still requires upstream non-prerelease GA and follows the separate GA requirements below. Do not dispatch while any pin or external prerequisite is pending.
## Image Compose bundle
@@ -37,10 +37,10 @@ The owner selected Apache-2.0 for first-party code in [QQQ #923](https://github.
## Exact commit and manual dispatch
After the release POMs are public, merge the reviewed version-pin change and the C7 smoke workflow to `develop`. Require a successful **push** run of `Smoke` on the exact intended commit; both `core` and `full` jobs must pass. Review the run's full SHA and run ID in Actions. Create an **annotated**, signed `4.1.0` tag at that exact `develop` commit. The workflow checks GitHub's valid signature verification, the tag object and its exact commit, including live remote refs. Do not move the tag or advance `develop` before dispatch.
After the release POMs are public, merge the version-pin change to `develop`. Require a successful webhook-triggered CircleCI `verify-and-smoke` workflow on the exact intended commit; `build-and-test`, `core` and `full` must all pass. Record its full SHA and workflow UUID. The shared verifier checks public Circle records against the latest trusted GitHub CircleCI check; it needs no exported Circle token for this public repository. Create an **annotated**, signed `4.1.0` tag at that exact `develop` commit. The workflow checks GitHub's valid signature verification, the tag object and its exact commit, including live remote refs. Do not move the tag or advance `develop` before dispatch.
Configure a protected `qqq-all-release` GitHub environment with required reviewers and branch restriction to `develop`. After Apache alignment and the publication preconditions pass, set environment variables `QQQ_ALL_RELEASE_LICENSE_APPROVED=true` and `QQQ_ALL_RELEASE_ENABLED=true`. Unset them to close the gate. These variables and the environment approval are operational controls, not substitutes for the release review.
Configure a protected `qqq-all-release` GitHub environment with no required human reviewers and branch restriction to `develop`. After Apache alignment and the publication preconditions pass, set environment variables `QQQ_ALL_RELEASE_LICENSE_APPROVED=true` and `QQQ_ALL_RELEASE_ENABLED=true`. Unset them to close the gate. These variables are automated release controls; enable them only after the documented artifact and license prerequisites are established.
Dispatch **Release 4.1.0** on `develop` with `version=4.1.0` (default), `release_sha` (the full 40-character commit SHA) and `smoke_run_id` (the successful push run ID). The job requires that checkout, `develop`, the annotated signed tag, and the smoke run all refer to that SHA. It checks upstream GA, both public Central POMs and immutable upstream inputs, public GHCR visibility, and destination absence; verifies the reactor with a fresh public-only cache and rejects transitive snapshots; then rechecks the live `develop` and remote tag refs before publication. Only then does it build and push `ghcr.io/qrun-io/qqq-all:4.1.0` and create the GitHub `4.1.0` release with the Compose bundle and rendered `compose.yaml` attached.
Dispatch **Release 4.1.0** on `develop` with `version=4.1.0` (default), `release_sha` (the full 40-character commit SHA) `smoke_evidence=circle`, and `circle_workflow_id` (the successful Circle workflow UUID). The job requires that checkout, `develop`, the annotated signed tag, and the smoke run all refer to that SHA. It checks upstream GA, both public Central POMs and immutable upstream inputs, public GHCR visibility, and destination absence; verifies the reactor with a fresh public-only cache and rejects transitive snapshots; then rechecks the live `develop` and remote tag refs before publication. Only then does it build and push `ghcr.io/qrun-io/qqq-all:4.1.0` and create the GitHub `4.1.0` release with the Compose bundle and rendered `compose.yaml` attached.
The image push and GitHub release creation are separate API operations, not an atomic transaction. If the release step fails after the image push, inspect the image digest and workflow logs before recovery; never reuse or overwrite the version tag blindly. Verify the public image pull, release asset, and both Maven POMs from an unauthenticated environment after publication. No workflow in this preparation branch publishes or tags anything by itself.
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
for attempt in {1..30}; do
if mongosh --host mongo --quiet -u "${MONGO_USER}" -p "${MONGO_PASSWORD}" --authenticationDatabase admin \
if mongosh --host mongo --quiet --username="${MONGO_USER}" --password="${MONGO_PASSWORD}" --authenticationDatabase admin \
--eval 'try { rs.status().ok } catch (e) { if (e.codeName === "NotYetInitialized") { rs.initiate({_id: process.env.MONGO_REPLICA_SET, members: [{_id: 0, host: "mongo:27017"}]}).ok } else { throw e } }' | grep -q 1; then
exit 0
fi
@@ -11,16 +11,10 @@ import com.kingsrook.qqq.backend.module.filesystem.s3.model.metadata.S3BackendMe
/** S3 metadata with an explicit endpoint for MinIO. */
public final class MinioBackendMetaData extends S3BackendMetaData
{
private final String endpoint;
public MinioBackendMetaData(String endpoint)
{
this.endpoint = endpoint;
setEndpoint(endpoint);
setPathStyleAccess(true);
setBackendType(MinioBackendModule.class);
}
public String getEndpoint()
{
return endpoint;
}
}
@@ -6,29 +6,11 @@
*/
package com.kingsrook.qqq.all;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QStorageInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFilesystemAction;
import com.kingsrook.qqq.backend.module.filesystem.s3.S3BackendModule;
import com.kingsrook.qqq.backend.module.filesystem.s3.actions.AbstractS3Action;
import com.kingsrook.qqq.backend.module.filesystem.s3.actions.S3CountAction;
import com.kingsrook.qqq.backend.module.filesystem.s3.actions.S3DeleteAction;
import com.kingsrook.qqq.backend.module.filesystem.s3.actions.S3InsertAction;
import com.kingsrook.qqq.backend.module.filesystem.s3.actions.S3QueryAction;
import com.kingsrook.qqq.backend.module.filesystem.s3.actions.S3StorageAction;
/** Routes every S3 operation to the configured MinIO endpoint. */
/** Uses the shared S3 actions with MinIO endpoint metadata. */
public final class MinioBackendModule extends S3BackendModule
{
static
@@ -47,92 +29,4 @@ public final class MinioBackendModule extends S3BackendModule
{
return MinioBackendMetaData.class;
}
private static AmazonS3 client(QBackendMetaData metadata)
{
MinioBackendMetaData minio = (MinioBackendMetaData) metadata;
return AmazonS3ClientBuilder.standard()
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(minio.getEndpoint(), minio.getRegion()))
.withPathStyleAccessEnabled(true)
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(minio.getAccessKey(), minio.getSecretKey())))
.build();
}
@Override
public AbstractBaseFilesystemAction<S3ObjectSummary> getActionBase()
{
return new AbstractS3Action()
{
@Override
protected AmazonS3 buildAmazonS3ClientFromBackendMetaData(QBackendMetaData metadata)
{
return client(metadata);
}
};
}
@Override
public QueryInterface getQueryInterface()
{
return new S3QueryAction()
{
@Override
protected AmazonS3 buildAmazonS3ClientFromBackendMetaData(QBackendMetaData metadata)
{
return client(metadata);
}
};
}
@Override
public InsertInterface getInsertInterface()
{
return new S3InsertAction()
{
@Override
protected AmazonS3 buildAmazonS3ClientFromBackendMetaData(QBackendMetaData metadata)
{
return client(metadata);
}
};
}
@Override
public CountInterface getCountInterface()
{
return new S3CountAction()
{
@Override
protected AmazonS3 buildAmazonS3ClientFromBackendMetaData(QBackendMetaData metadata)
{
return client(metadata);
}
};
}
@Override
public DeleteInterface getDeleteInterface()
{
return new S3DeleteAction()
{
@Override
protected AmazonS3 buildAmazonS3ClientFromBackendMetaData(QBackendMetaData metadata)
{
return client(metadata);
}
};
}
@Override
public QStorageInterface getStorageInterface()
{
return new S3StorageAction()
{
@Override
protected AmazonS3 buildAmazonS3ClientFromBackendMetaData(QBackendMetaData metadata)
{
return client(metadata);
}
};
}
}
@@ -44,7 +44,7 @@ class MinioBackendTest
action.preAction(backend);
S3Utils utils = utils(action);
assertEquals("http://minio.example.test:9000/qqq-demo/example.json",
utils.getAmazonS3().getUrl("qqq-demo", "example.json").toString());
utils.getS3Client().utilities().getUrl(request -> request.bucket("qqq-demo").key("example.json")).toString());
}
@Test
@@ -88,8 +88,8 @@ class MinioBackendTest
AbstractS3Action query = (AbstractS3Action) module.getQueryInterface();
query.preAction(backend);
assertEquals("documents/example.json", utils(query).getAmazonS3()
.listObjectsV2("qqq-demo").getObjectSummaries().getFirst().getKey());
assertEquals("documents/example.json", utils(query).getS3Client()
.listObjectsV2(request -> request.bucket("qqq-demo")).contents().getFirst().key());
AbstractS3Action insert = (AbstractS3Action) module.getInsertInterface();
insert.preAction(backend);
+11 -2
View File
@@ -119,6 +119,16 @@ class SmokeClient:
raise AssertionError(f"missing expected {table} {field}")
def require_oidc_session(client, result):
# Next uses an HttpOnly cookie; current QQQ intentionally omits its UUID from JSON.
cookies = [cookie for cookie in client.cookies if cookie.name == "sessionUUID"]
if (len(cookies) != 1 or not cookies[0].value
or not cookies[0].has_nonstandard_attr("HttpOnly")):
raise AssertionError("QQQ did not establish an HttpOnly OIDC session")
if "uuid" in result:
raise AssertionError("QQQ exposed the HttpOnly session UUID in JSON")
def allow_loopback_http_session(client):
parsed = urllib.parse.urlsplit(client.base_url)
if parsed.scheme != "http" or parsed.hostname not in ("127.0.0.1", "localhost"):
@@ -273,8 +283,7 @@ def oidc_login(client, keycloak_url, username, password):
result = client.json("/manageSession", {
"code": parameters["code"][0], "redirectUri": callback, "codeVerifier": verifier
})
if not result.get("uuid") or not any(cookie.name == "sessionUUID" for cookie in client.cookies):
raise AssertionError("QQQ did not establish an OIDC session")
require_oidc_session(client, result)
if urllib.parse.urlsplit(client.base_url).scheme == "http":
allow_loopback_http_session(client)
except (OSError, KeyError, ValueError, urllib.error.HTTPError) as error:
-51
View File
@@ -1,51 +0,0 @@
"""Run the workflow's publication shell with a recording CLI, never GitHub."""
import json
import os
import subprocess
import tempfile
import textwrap
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def workflow_shell(name):
text = (ROOT / ".github/workflows/release.yml").read_text()
step = text.split(" - name: " + name + "\n", 1)[1].split(" - ", 1)[0]
return textwrap.dedent(step.split(" run: |\n", 1)[1])
class ReleaseWorkflowTest(unittest.TestCase):
def test_rc_is_prerelease_never_latest_and_ga_keeps_existing_release_behavior(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
gh = root / "gh"
gh.write_text("#!/usr/bin/env python3\nimport json, os, sys\n"
"with open(os.environ['CALLS'], 'w') as f: json.dump(sys.argv[1:], f)\n")
gh.chmod(0o755)
calls = root / "calls.json"
for version in ("4.1.0", "4.1.0-RC.1", "4.1.0-RC.2"):
calls.unlink(missing_ok=True)
environment = {**os.environ, "PATH": directory + os.pathsep + os.environ["PATH"],
"CALLS": str(calls), "VERSION": version, "TAG": version,
"RUNNER_TEMP": directory, "GITHUB_REPOSITORY": "QRun-IO/qqq-all"}
result = subprocess.run(["bash", "-c", workflow_shell("Publish GitHub release with Compose bundle")],
env=environment, capture_output=True, text=True)
if version == "4.1.0-RC.2":
self.assertNotEqual(0, result.returncode)
self.assertFalse(calls.exists())
continue
self.assertEqual(0, result.returncode, result.stderr)
args = json.loads(calls.read_text())
self.assertEqual(["release", "create", version], args[:3])
self.assertIn(str(root / ("qqq-all-" + version + ".tar.gz")), args)
self.assertIn("--verify-tag", args)
if version == "4.1.0-RC.1":
self.assertIn("--prerelease", args)
self.assertIn("--latest=false", args)
else:
self.assertNotIn("--prerelease", args)
self.assertNotIn("--latest=false", args)
+17 -5
View File
@@ -148,10 +148,12 @@ class SmokeTest(unittest.TestCase):
pom = ET.parse(ROOT / "qqq-all-app" / "pom.xml")
final_name = pom.findtext(".//{http://maven.apache.org/POM/4.0.0}build/"
"{http://maven.apache.org/POM/4.0.0}finalName")
workflow = (ROOT / ".github" / "workflows" / "smoke.yml").read_text()
command = re.search(r"run: python3 scripts/run_core_smoke.py (\S+)", workflow)
self.assertIsNotNone(command)
self.assertEqual(f"qqq-all-app/target/{final_name}.jar", command.group(1))
# The shared all profile owns the smoke command; this repository owns
# the Maven artifact it consumes. Keep that cross-repository contract.
self.assertEqual("qqq-all-app", final_name)
manifest = (ROOT / ".qqq-ci.yml").read_text()
self.assertRegex(manifest, r"(?m)^profile: all$")
def test_standalone_full_smoke_requires_explicit_search_endpoint(self):
result = subprocess.run(
@@ -234,7 +236,7 @@ class SmokeTest(unittest.TestCase):
run_full_smoke.prepare_sftp_import_directory(["docker", "compose"], {}, "qqq")
def test_oidc_cookie_is_sent_only_to_loopback_http_app(self):
from smoke import allow_loopback_http_session
from smoke import allow_loopback_http_session, require_oidc_session
class SecureSession(DemoHandler):
def do_GET(self):
@@ -258,7 +260,17 @@ class SmokeTest(unittest.TestCase):
thread.start()
try:
client = SmokeClient(f"http://127.0.0.1:{server.server_port}")
with self.assertRaisesRegex(AssertionError, "HttpOnly OIDC session"):
require_oidc_session(client, {})
client.request("/login")
require_oidc_session(client, {})
with self.assertRaisesRegex(AssertionError, "exposed"):
require_oidc_session(client, {"uuid": "test-session"})
cookie = next(iter(client.cookies))
cookie._rest.clear()
with self.assertRaisesRegex(AssertionError, "HttpOnly OIDC session"):
require_oidc_session(client, {})
cookie.set_nonstandard_attr("HttpOnly", None)
with self.assertRaisesRegex(AssertionError, "HTTP 403"):
client.request("/protected")
allow_loopback_http_session(client)