Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4490545c4f
|
||
|
|
e13bda1d8a
|
||
|
|
654e1c265a
|
||
|
|
718ca93b0c
|
No files matched your search
+10
-24
@@ -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: /.*/
|
||||
@@ -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
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
profile: all
|
||||
orb_version: 0.7.9
|
||||
default_branch: develop
|
||||
publish:
|
||||
snapshot: true
|
||||
publish_context: qqq-maven-registry-credentials
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user