Author SHA1 Message Date
james.maes fb255928b7 Merge pull request 'ci: verify on the shared ci-workflows Maven workflow' (#8) from ci/ci-workflows into main
maven / build (push) Successful in 52s
build / maven (push) Successful in 52s
2026-11-07 20:07:57 +00:00
James MaesandClaude Opus 5.5 61aa6f64c0 build(db): bump Testcontainers to 1.21.4
maven / build (push) Successful in 1m33s
build / maven (push) Successful in 1m33s
1.20.1 cannot talk to the CI runners' current Docker Engine (it pins an
API version the daemon no longer accepts), so the migration ITs failed
with "Could not find a valid Docker environment". 1.21.4 negotiates a
supported API version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:05:53 -06:00
James MaesandClaude Opus 5.5 d95240e34f ci: verify on the shared ci-workflows Maven workflow
maven / build (push) Failing after 1m54s
build / maven (push) Failing after 1m54s
Replace the GitHub workflows that called the retired qqq-orb
(business-e2e, business-db) with KofTwentyTwo/ci-workflows
maven.yaml@v1, verify only (publish: false). Every push runs mvn verify,
which includes the db module's empty-database migration tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:01:57 -06:00
james.maesandCodex 94f5031d94 fix: restore shared live-service e2e validation (#5)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 01:46:43 -05:00
james.maesandCodex 1a6e3bf00e ci: adopt always-reporting shared database validation (#4)
Co-authored-by: Codex <noreply@openai.com>
2026-10-20 22:27:49 -05:00
james.maesandCodex b378500062 ci: delegate business checks to shared QQQ workflows (#3)
* ci: delegate business checks to shared QQQ workflows

Pin canonical database and E2E callers to the verified orb 0.7.6 source; preserve triggers, paths and the existing E2E workflow state.

Co-Authored-By: Codex <noreply@openai.com>

* test: verify the current full platform database schema

Retain exact table and changeset checks for identity, content and newsletter migrations. Isolate container state per test and fail when Docker is unavailable instead of silently skipping the migration gate.

Co-Authored-By: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
2026-10-20 21:02:43 -05:00
11 changed files with 47 additions and 170 deletions

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
.git
**/target
**/node_modules
**/.next
**/playwright-report
**/test-results
+10
View File
@@ -0,0 +1,10 @@
name: build
on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
with:
# Application: verify every push (unit, db migration and Testcontainers tests); no artifact publish.
publish: false
-26
View File
@@ -1,26 +0,0 @@
name: Database migrations
on:
pull_request:
paths:
- "db/**"
- "pom.xml"
- ".github/workflows/db-migrations.yml"
push:
branches: [main]
paths:
- "db/**"
- "pom.xml"
jobs:
empty-database-migration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven
- name: Empty PostgreSQL -> migrate -> validate, demo determinism, forward-only policy
run: mvn -B -pl db verify
-119
View File
@@ -1,119 +0,0 @@
name: Required E2E validation
on:
pull_request:
push:
branches: [main]
schedule:
- cron: "17 6 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e:
name: E2E / live services
runs-on: ubuntu-latest
timeout-minutes: 30
env:
CI: "true"
E2E_BASE_URL: http://localhost:3004
E2E_API_URL: http://localhost:18080
E2E_KEYCLOAK_URL: http://localhost:18081
NEXTAUTH_URL: http://localhost:3004
NEXTAUTH_SECRET: ci-only-e2e-secret-not-for-production
KEYCLOAK_ISSUER: http://localhost:18081/realms/qqq-platform
KEYCLOAK_CLIENT_ID: platform-web
KEYCLOAK_CLIENT_SECRET: platform-web-secret
API_BASE_URL_INTERNAL: http://localhost:18080
E2E_ALERT_WEBHOOK_URL: ${{ secrets.E2E_ALERT_WEBHOOK_URL }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: web/package-lock.json
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven
- name: Install web dependencies and Chromium
working-directory: web
run: |
npm ci
npx playwright install --with-deps chromium
- name: Start actual platform dependencies and API
run: docker compose -f deploy/compose/docker-compose.yml up -d --build postgres migrate keycloak minio minio-setup mailpit qqq-server
- name: Wait for actual services
shell: bash
run: |
set -euo pipefail
wait_for() {
local name="$1" url="$2"
for attempt in {1..90}; do
if curl --fail --silent --show-error "$url" >/dev/null; then
echo "$name is ready"
return 0
fi
sleep 2
done
echo "::error::$name did not become ready at $url"
docker compose -f deploy/compose/docker-compose.yml ps
return 1
}
wait_for Keycloak "$E2E_KEYCLOAK_URL/realms/qqq-platform/.well-known/openid-configuration"
wait_for API "$E2E_API_URL/api/public/v1/health"
- name: Run blocking E2E suite
working-directory: web
run: npm run test:e2e:ci
- name: Publish test summary
if: always()
working-directory: web
run: node scripts/report-e2e-results.mjs
- name: Upload Playwright evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-results-${{ github.run_id }}
path: |
web/playwright-report/
web/test-results/
if-no-files-found: warn
retention-days: 14
- name: Notify configured alert webhook
if: failure() && env.E2E_ALERT_WEBHOOK_URL != ''
run: |
curl --fail-with-body -X POST -H 'Content-Type: application/json' \
--data "{\"text\":\"E2E validation failed for $GITHUB_REPOSITORY ($GITHUB_REF_NAME): $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID\"}" \
"$E2E_ALERT_WEBHOOK_URL"
- name: Show service logs after failure
if: failure()
run: docker compose -f deploy/compose/docker-compose.yml logs --no-color --tail=300
- name: Stop services
if: always()
run: docker compose -f deploy/compose/docker-compose.yml down -v
deployment-gate:
name: Deployment gate / E2E passed
needs: e2e
runs-on: ubuntu-latest
steps:
- run: echo "Deployment is permitted only because the live-service E2E job passed."
+1 -1
View File
@@ -12,7 +12,7 @@
<name>Platform Database Migrations</name>
<properties>
<testcontainers.version>1.20.1</testcontainers.version>
<testcontainers.version>1.21.4</testcontainers.version>
</properties>
<dependencies>
@@ -19,7 +19,10 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
class ExternalPostgresMigrationIT {
private static final Set<String> TABLES = Set.of(
"newsletter_subscription", "newsletter_segment", "newsletter_campaign",
"outbound_email", "email_suppression", "opt_in_token");
"outbound_email", "email_suppression", "opt_in_token",
"user_profile", "local_registration", "cms_site", "cms_asset", "cms_author",
"cms_page", "cms_blog_post", "cms_tag", "cms_category", "cms_navigation",
"cms_redirect", "cms_content_revision", "cms_blog_post_category", "cms_blog_post_tag");
@Test
void emptyDatabaseMigratesValidatesAndSeedsDeterministically() throws Exception {
@@ -38,7 +41,7 @@ class ExternalPostgresMigrationIT {
liquibase.update(new Contexts("baseline,demo"), new LabelExpression());
liquibase.update(new Contexts("baseline,demo"), new LabelExpression());
assertEquals(1, count(connection, "newsletter_subscription"));
assertEquals(3, count(connection, "databasechangelog"));
assertEquals(7, count(connection, "databasechangelog"));
}
}
}
@@ -20,17 +20,20 @@ import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers(disabledWithoutDocker = true)
@Testcontainers
@DisabledIfEnvironmentVariable(named = "QQQ_EXTERNAL_DB_URL", matches = ".+")
class NewsletterMigrationIT {
private static final String CHANGELOG = "db/changelog/db.changelog-master.yaml";
private static final Set<String> NEWSLETTER_TABLES = Set.of(
private static final Set<String> PLATFORM_TABLES = Set.of(
"newsletter_subscription", "newsletter_segment", "newsletter_campaign",
"outbound_email", "email_suppression", "opt_in_token");
"outbound_email", "email_suppression", "opt_in_token",
"user_profile", "local_registration", "cms_site", "cms_asset", "cms_author",
"cms_page", "cms_blog_post", "cms_tag", "cms_category", "cms_navigation",
"cms_redirect", "cms_content_revision", "cms_blog_post_category", "cms_blog_post_tag");
@Container
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine")
final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("qqq")
.withUsername("qqq")
.withPassword("qqq");
@@ -42,10 +45,10 @@ class NewsletterMigrationIT {
liquibase.update(new Contexts("baseline"), new LabelExpression());
liquibase.validate();
assertEquals(NEWSLETTER_TABLES, applicationTables(connection));
assertEquals(PLATFORM_TABLES, applicationTables(connection));
assertEquals(0, count(connection, "newsletter_subscription"));
assertEquals(0, count(connection, "newsletter_campaign"));
assertEquals(2, appliedChangeSets(connection));
assertEquals(6, appliedChangeSets(connection));
}
}
@@ -59,16 +62,16 @@ class NewsletterMigrationIT {
assertEquals(1, count(connection, "newsletter_subscription"));
assertEquals(1, count(connection, "newsletter_segment"));
assertEquals(1, count(connection, "newsletter_campaign"));
assertEquals(3, appliedChangeSets(connection));
assertEquals(7, appliedChangeSets(connection));
liquibase.update(new Contexts("baseline,demo"), new LabelExpression());
assertEquals(1, count(connection, "newsletter_subscription"));
assertEquals(3, appliedChangeSets(connection));
assertEquals(7, appliedChangeSets(connection));
}
}
private static Connection connect() throws Exception {
return DriverManager.getConnection(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword());
private Connection connect() throws Exception {
return DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());
}
private static Liquibase liquibase(Connection connection) throws Exception {
+5 -5
View File
@@ -87,7 +87,7 @@ services:
# ========== Object Storage ==========
minio:
image: minio/minio:latest
image: cgr.dev/chainguard/minio:latest-dev@sha256:25c69c296421a54961076193a136b1b34d81e2c5c66c6fd596e16ec2d2f668be
container_name: qqq-platform-minio
environment:
MINIO_ROOT_USER: minio_admin
@@ -100,7 +100,7 @@ services:
- "19000:9000" # API
- "19001:9001" # Console
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9000/minio/health/live || exit 1"]
test: ["CMD-SHELL", "wget -q -O /dev/null http://localhost:9000/minio/health/live || exit 1"]
interval: 10s
timeout: 5s
retries: 5
@@ -110,7 +110,7 @@ services:
# Initialize MinIO buckets
minio-setup:
image: minio/mc:latest
image: cgr.dev/chainguard/minio-client:latest-dev@sha256:d0bce838369160ea7db20589040bee94bdb3e76dbf7fd10aa3eb755ae17a506b
container_name: qqq-platform-minio-setup
depends_on:
minio:
@@ -121,7 +121,7 @@ services:
mc alias set minio http://minio:9000 minio_admin minio_admin_password &&
mc mb minio/platform-assets --ignore-existing &&
mc mb minio/platform-uploads --ignore-existing &&
mc policy set public minio/platform-assets &&
mc anonymous set download minio/platform-assets &&
echo 'MinIO setup completed!'
"
networks:
@@ -275,4 +275,4 @@ volumes:
networks:
platform-network:
driver: bridge
name: qqq-platform
name: qqq-platform
@@ -2,9 +2,9 @@
spring:
datasource:
url: jdbc:postgresql://postgres:5432/qqq_platform
username: platform_app
password: qqq_platform
url: ${DB_URL:jdbc:postgresql://postgres:5432/qqq_platform}
username: ${DB_USERNAME:platform_app}
password: ${DB_PASSWORD:platform_app_password}
driver-class-name: org.postgresql.Driver
# Email configuration for newsletter QBit
@@ -24,8 +24,8 @@ spring:
oauth2:
resourceserver:
jwt:
issuer-uri: http://keycloak:8081/realms/qqq-platform
jwk-set-uri: http://keycloak:8081/realms/qqq-platform/protocol/openid-connect/certs
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://keycloak:8081/realms/qqq-platform}
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://keycloak:8081/realms/qqq-platform/protocol/openid-connect/certs}
# Newsletter configuration
newsletter:
+1 -1
View File
@@ -27,7 +27,7 @@ Requires Keycloak on `localhost:18081` and the platform API on `localhost:18080`
- `npm run test:e2e` runs Playwright against the real API and Keycloak services; it never mocks them. Playwright starts the frontend on port 3004 when needed.
- `npm run build` automatically runs type checks, unit tests, and the complete E2E suite first. A failed or unavailable live service blocks the build.
- `npm install`/`npm ci` enables the repository pre-commit hook. Commits touching platform code, infrastructure, tests, or workflows run the E2E suite and are rejected on failure.
- `.github/workflows/e2e.yml` starts the actual Compose services on every pull request, main push, nightly schedule, and manual dispatch. Its `Deployment gate / E2E passed` status cannot succeed when E2E fails.
- CI is `.gitea/workflows/build.yaml` (shared `KofTwentyTwo/ci-workflows` Maven workflow): `mvn verify` on every push, including the empty-database migration tests. The live-service Compose E2E job ran through the retired GitHub qqq-orb workflow and is not ported to Gitea yet; run `npm run test:e2e` against the live stack locally (the pre-commit hook enforces it).
- HTML, JUnit, traces, screenshots, and videos are retained as workflow artifacts. Failures appear in the Actions summary and can be sent to a JSON incoming webhook (including Slack-compatible receivers) by defining the `E2E_ALERT_WEBHOOK_URL` Actions secret.
Repository administrators should make **Required E2E validation / E2E / live services** and **Deployment gate / E2E passed** required branch-protection checks. Deployment workflows must depend on the deployment-gate job (or this workflow's successful conclusion); do not permit manual success overrides.
+1 -1
View File
@@ -13,7 +13,7 @@ export async function POST(request: NextRequest) {
}
// Call backend API from server-side (no CORS issues)
const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:18082'
const backendUrl = process.env.API_BASE_URL_INTERNAL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:18080'
const response = await fetch(`${backendUrl}/api/public/v1/newsletter/subscribe`, {
method: 'POST',
headers: {