Author SHA1 Message Date
james.maes 4adf295c89 Add .circleci/config.yml 2026-09-26 01:09:27 -05:00
5 changed files with 175 additions and 39 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
+23 -12
View File
@@ -1,15 +1,26 @@
name: Database migrations
'on':
pull_request: {}
on:
pull_request:
paths:
- "db/**"
- "pom.xml"
- ".github/workflows/db-migrations.yml"
push:
branches:
- main
permissions:
contents: read
branches: [main]
paths:
- "db/**"
- "pom.xml"
jobs:
validate:
uses: QRun-IO/qqq-orb/.github/workflows/business-db.yml@5bcec90e330b1b4d7ec416d64b065e0aed74a6ff
concurrency:
group: business-db-${{ github.workflow }}-${{ github.event.pull_request.number ||
github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
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
+109 -9
View File
@@ -1,19 +1,119 @@
name: Required E2E validation
'on':
pull_request: null
on:
pull_request:
push:
branches:
- main
branches: [main]
schedule:
- cron: 17 6 * * *
workflow_dispatch: null
- cron: "17 6 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true
jobs:
validate:
uses: QRun-IO/qqq-orb/.github/workflows/business-e2e.yml@0965a4565369a74650c8f96683b2296435a35382
secrets:
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."
@@ -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 {