Author SHA1 Message Date
james.maes 4adf295c89 Add .circleci/config.yml 2026-09-26 01:09:27 -05:00
12 changed files with 201 additions and 47 deletions

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/reference/configuration-reference/
version: 2.1
# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/guides/orchestrate/jobs-steps/#jobs-overview & https://circleci.com/docs/reference/configuration-reference/#jobs
jobs:
say-hello:
# Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub.
# See: https://circleci.com/docs/guides/execution-managed/executor-intro/ & https://circleci.com/docs/reference/configuration-reference/#executor-job
docker:
# Specify the version you desire here
# See: https://circleci.com/developer/images/image/cimg/base
- image: cimg/base:current
# Add steps to the job
# See: https://circleci.com/docs/guides/orchestrate/jobs-steps/#steps-overview & https://circleci.com/docs/reference/configuration-reference/#steps
steps:
# Checkout the code as the first step.
- checkout
- run:
name: "Say hello"
command: "echo Hello, World!"
# Orchestrate jobs using workflows
# See: https://circleci.com/docs/guides/orchestrate/workflows/ & https://circleci.com/docs/reference/configuration-reference/#workflows
workflows:
say-hello-workflow: # This is the name of the workflow, feel free to change it to better match your workflow.
# Inside the workflow, you define the jobs you want to run.
jobs:
- say-hello
-6
View File
@@ -1,6 +0,0 @@
.git
**/target
**/node_modules
**/.next
**/playwright-report
**/test-results
-10
View File
@@ -1,10 +0,0 @@
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
@@ -0,0 +1,26 @@
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
@@ -0,0 +1,119 @@
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.21.4</testcontainers.version>
<testcontainers.version>1.20.1</testcontainers.version>
</properties>
<dependencies>
@@ -19,10 +19,7 @@ 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",
"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");
"outbound_email", "email_suppression", "opt_in_token");
@Test
void emptyDatabaseMigratesValidatesAndSeedsDeterministically() throws Exception {
@@ -41,7 +38,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(7, count(connection, "databasechangelog"));
assertEquals(3, count(connection, "databasechangelog"));
}
}
}
@@ -20,20 +20,17 @@ import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
@Testcontainers(disabledWithoutDocker = true)
@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> PLATFORM_TABLES = Set.of(
private static final Set<String> NEWSLETTER_TABLES = Set.of(
"newsletter_subscription", "newsletter_segment", "newsletter_campaign",
"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");
"outbound_email", "email_suppression", "opt_in_token");
@Container
final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("qqq")
.withUsername("qqq")
.withPassword("qqq");
@@ -45,10 +42,10 @@ class NewsletterMigrationIT {
liquibase.update(new Contexts("baseline"), new LabelExpression());
liquibase.validate();
assertEquals(PLATFORM_TABLES, applicationTables(connection));
assertEquals(NEWSLETTER_TABLES, applicationTables(connection));
assertEquals(0, count(connection, "newsletter_subscription"));
assertEquals(0, count(connection, "newsletter_campaign"));
assertEquals(6, appliedChangeSets(connection));
assertEquals(2, appliedChangeSets(connection));
}
}
@@ -62,16 +59,16 @@ class NewsletterMigrationIT {
assertEquals(1, count(connection, "newsletter_subscription"));
assertEquals(1, count(connection, "newsletter_segment"));
assertEquals(1, count(connection, "newsletter_campaign"));
assertEquals(7, appliedChangeSets(connection));
assertEquals(3, appliedChangeSets(connection));
liquibase.update(new Contexts("baseline,demo"), new LabelExpression());
assertEquals(1, count(connection, "newsletter_subscription"));
assertEquals(7, appliedChangeSets(connection));
assertEquals(3, appliedChangeSets(connection));
}
}
private Connection connect() throws Exception {
return DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());
private static 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: cgr.dev/chainguard/minio:latest-dev@sha256:25c69c296421a54961076193a136b1b34d81e2c5c66c6fd596e16ec2d2f668be
image: minio/minio:latest
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", "wget -q -O /dev/null http://localhost:9000/minio/health/live || exit 1"]
test: ["CMD-SHELL", "curl -f 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: cgr.dev/chainguard/minio-client:latest-dev@sha256:d0bce838369160ea7db20589040bee94bdb3e76dbf7fd10aa3eb755ae17a506b
image: minio/mc:latest
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 anonymous set download minio/platform-assets &&
mc policy set public 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: ${DB_URL:jdbc:postgresql://postgres:5432/qqq_platform}
username: ${DB_USERNAME:platform_app}
password: ${DB_PASSWORD:platform_app_password}
url: jdbc:postgresql://postgres:5432/qqq_platform
username: platform_app
password: qqq_platform
driver-class-name: org.postgresql.Driver
# Email configuration for newsletter QBit
@@ -24,8 +24,8 @@ spring:
oauth2:
resourceserver:
jwt:
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}
issuer-uri: http://keycloak:8081/realms/qqq-platform
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.
- 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).
- `.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.
- 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.API_BASE_URL_INTERNAL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:18080'
const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:18082'
const response = await fetch(`${backendUrl}/api/public/v1/newsletter/subscribe`, {
method: 'POST',
headers: {