Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb255928b7
|
||
|
|
61aa6f64c0 | ||
|
|
d95240e34f | ||
|
|
94f5031d94 | ||
|
|
1a6e3bf00e | ||
|
|
b378500062 |
No files matched your search
@@ -1,31 +0,0 @@
|
||||
# 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
|
||||
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
**/target
|
||||
**/node_modules
|
||||
**/.next
|
||||
**/playwright-report
|
||||
**/test-results
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in new issue
Block a user