name: CI on: push: branches: - main paths-ignore: - "conductor-clients/**" pull_request: paths-ignore: - "conductor-clients/**" workflow_dispatch: inputs: redis_es8: description: "Redis + Elasticsearch 8" type: boolean default: true postgres: description: "PostgreSQL" type: boolean default: false mysql: description: "MySQL" type: boolean default: false redis_os3: description: "Redis + OpenSearch 3" type: boolean default: false redis_es7: description: "Redis + Elasticsearch 7" type: boolean default: false cassandra_es7: description: "Cassandra + Elasticsearch 7" type: boolean default: false jobs: detect-changes: runs-on: ubuntu-latest outputs: heavy-persistence: ${{ steps.filter.outputs.heavy-persistence }} steps: - uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - uses: dorny/paths-filter@v3 id: filter with: filters: | heavy-persistence: - 'cassandra-persistence/**' - 'es6-persistence/**' - 'es7-persistence/**' - 'es8-persistence/**' - 'mysql-persistence/**' - 'os-persistence/**' - 'os-persistence-v2/**' - 'os-persistence-v3/**' - 'scheduler/cassandra-persistence/**' - 'scheduler/mysql-persistence/**' build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Free disk space run: | sudo rm -rf /usr/share/dotnet sudo rm -rf /usr/local/lib/android sudo rm -rf /opt/ghc - name: Gradle wrapper validation uses: gradle/wrapper-validation-action@v3 - name: Set up Zulu JDK 21 uses: actions/setup-java@v5 with: distribution: "zulu" java-version: "21" - name: Cache SonarCloud packages uses: actions/cache@v5 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Gradle packages uses: actions/cache@v5 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: ${{ runner.os }}-gradle- - name: Build with Gradle if: github.ref != 'refs/heads/main' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | ./gradlew build -x :conductor-test-harness:test -x test - name: Build and Publish snapshot if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' run: | echo "Running build for commit ${{ github.sha }}" ./gradlew build -x :conductor-test-harness:test -x test - name: Generate aggregated coverage report if: always() run: ./gradlew jacocoAggregatedReport -x test || true - name: Publish Test Report uses: mikepenz/action-junit-report@v6 if: always() with: report_paths: "**/build/test-results/test/TEST-*.xml" - name: Upload build artifacts uses: actions/upload-artifact@v7 with: name: build-artifacts path: "**/build/reports" - name: Upload coverage report uses: actions/upload-artifact@v7 if: always() with: name: coverage-report path: build/reports/jacoco/aggregated unit-test: needs: detect-changes runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Free disk space run: | sudo rm -rf /usr/share/dotnet sudo rm -rf /usr/local/lib/android sudo rm -rf /opt/ghc - name: Set up Zulu JDK 21 uses: actions/setup-java@v5 with: distribution: "zulu" java-version: "21" - name: Cache Gradle packages uses: actions/cache@v5 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: ${{ runner.os }}-gradle- - name: Force Docker API Version run: echo 'api.version=1.44' > ~/.docker-java.properties - name: Run unit tests run: | # On main (post-merge): run everything if [ "${{ github.event_name }}" != "pull_request" ]; then ./gradlew test -x :conductor-test-harness:test exit 0 fi # On PRs: skip heavy container tests (cassandra, es, mysql, opensearch) # unless their code changed. Redis, postgres, sqlite always run. if [ "${{ needs.detect-changes.outputs.heavy-persistence }}" == "true" ]; then ./gradlew test -x :conductor-test-harness:test else ./gradlew test \ -x :conductor-test-harness:test \ -x :conductor-cassandra-persistence:test \ -x :conductor-scheduler-cassandra-persistence:test \ -x :conductor-es6-persistence:test \ -x :conductor-es7-persistence:test \ -x :conductor-es8-persistence:test \ -x :conductor-mysql-persistence:test \ -x :conductor-scheduler-mysql-persistence:test \ -x :conductor-os-persistence:test \ -x :conductor-os-persistence-v2:test \ -x :conductor-os-persistence-v3:test fi - name: Publish Test Report uses: mikepenz/action-junit-report@v6 if: always() with: report_paths: "**/build/test-results/test/TEST-*.xml" check_name: Unit Test Report - name: Upload unit-test reports uses: actions/upload-artifact@v7 if: always() with: name: unit-test-reports path: "**/build/reports/tests" test-harness: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Free disk space run: | sudo rm -rf /usr/share/dotnet sudo rm -rf /usr/local/lib/android sudo rm -rf /opt/ghc - name: Set up Zulu JDK 21 uses: actions/setup-java@v5 with: distribution: "zulu" java-version: "21" - name: Cache Gradle packages uses: actions/cache@v5 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: ${{ runner.os }}-gradle- - name: Force Docker API Version run: echo 'api.version=1.44' > ~/.docker-java.properties - name: Cache Docker images uses: actions/cache@v5 id: docker-cache with: path: /tmp/docker-images-test-harness.tar key: docker-test-harness-v2 - name: Load cached Docker images if: steps.docker-cache.outputs.cache-hit == 'true' run: docker load -i /tmp/docker-images-test-harness.tar || true - name: Run test-harness tests run: | ./gradlew :conductor-test-harness:test - name: Save Docker images for cache if: steps.docker-cache.outputs.cache-hit != 'true' run: | set -euo pipefail docker image prune -f mapfile -t images < <((docker images --format '{{.Repository}}:{{.Tag}}' \ | grep -v '' \ | grep -E '(^|/)(elasticsearch|redis|postgres|mysql|mongo|cassandra)(:|/)|mockserver/mockserver|opensearchproject/opensearch|testcontainers/|orkesio/') || true) if [ "${#images[@]}" -eq 0 ]; then echo "No Testcontainers-related images to cache; writing empty tar for cache action." tar -cf /tmp/docker-images-test-harness.tar --files-from /dev/null exit 0 fi printf '%s\n' "${images[@]}" docker save -o /tmp/docker-images-test-harness.tar "${images[@]}" - name: Publish Test Report uses: mikepenz/action-junit-report@v6 if: always() with: report_paths: "test-harness/build/test-results/test/TEST-*.xml" - name: Upload test-harness reports uses: actions/upload-artifact@v7 if: always() with: name: test-harness-reports path: "test-harness/build/reports" - name: Upload test-harness coverage report uses: actions/upload-artifact@v7 if: always() with: name: test-harness-coverage-report path: "test-harness/build/reports/jacoco" generate-e2e-matrix: runs-on: ubuntu-latest outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - id: set-matrix shell: bash run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then items="" [ "${{ inputs.redis_es8 }}" = "true" ] && items="${items}{\"name\":\"redis-es8\",\"script\":\"./e2e/run_tests-es8.sh\"}," [ "${{ inputs.postgres }}" = "true" ] && items="${items}{\"name\":\"postgres\",\"script\":\"./e2e/run_tests-postgres.sh\"}," [ "${{ inputs.mysql }}" = "true" ] && items="${items}{\"name\":\"mysql\",\"script\":\"./e2e/run_tests-mysql.sh\"}," [ "${{ inputs.redis_os3 }}" = "true" ] && items="${items}{\"name\":\"redis-os3\",\"script\":\"./e2e/run_tests-redis-os3.sh\"}," [ "${{ inputs.redis_es7 }}" = "true" ] && items="${items}{\"name\":\"redis-es7\",\"script\":\"./e2e/run_tests-redis-es7.sh\"}," [ "${{ inputs.cassandra_es7 }}" = "true" ] && items="${items}{\"name\":\"cassandra-es7\",\"script\":\"./e2e/run_tests-cassandra-es7.sh\"}," items="${items%,}" echo "matrix={\"include\":[${items}]}" >> "$GITHUB_OUTPUT" else echo 'matrix={"include":[{"name":"redis-es8","script":"./e2e/run_tests-es8.sh"}]}' >> "$GITHUB_OUTPUT" fi e2e: needs: generate-e2e-matrix if: ${{ needs.generate-e2e-matrix.outputs.matrix != '{"include":[]}' }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: ${{ fromJson(needs.generate-e2e-matrix.outputs.matrix) }} steps: - uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Free disk space run: | sudo rm -rf /usr/share/dotnet sudo rm -rf /usr/local/lib/android sudo rm -rf /opt/ghc - name: Set up Zulu JDK 21 uses: actions/setup-java@v5 with: distribution: "zulu" java-version: "21" - name: Cache Gradle packages uses: actions/cache@v5 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: ${{ runner.os }}-gradle- - name: Run E2E tests (${{ matrix.name }}) run: ${{ matrix.script }} - name: Publish Test Report uses: mikepenz/action-junit-report@v6 if: always() with: report_paths: "e2e/build/test-results/test/TEST-*.xml" - name: Generate test summary table if: always() shell: python3 {0} run: | import os, glob, xml.etree.ElementTree as ET xml_files = glob.glob("e2e/build/test-results/test/TEST-*.xml") rows = [] totals = {"passed": 0, "failed": 0, "skipped": 0} for f in sorted(xml_files): try: root = ET.parse(f).getroot() except ET.ParseError: continue for tc in root.iter("testcase"): name = tc.get("name", "?") classname = tc.get("classname", "").split(".")[-1] duration = float(tc.get("time", 0)) if tc.find("skipped") is not None: status, totals["skipped"] = "⏭ skip", totals["skipped"] + 1 elif tc.find("failure") is not None or tc.find("error") is not None: node = tc.find("failure") if tc.find("failure") is not None else tc.find("error") msg = (node.get("message") or "")[:120] status, totals["failed"] = f"❌ `{msg}`", totals["failed"] + 1 else: status, totals["passed"] = "✅", totals["passed"] + 1 rows.append((classname, name, f"{duration:.1f}s", status)) backend = "${{ matrix.name }}" lines = [ f"## E2E results — {backend}", f"**✅ {totals['passed']} passed · ❌ {totals['failed']} failed · ⏭ {totals['skipped']} skipped**", "", "| Class | Test | Duration | Result |", "|-------|------|----------|--------|", ] for cls, name, dur, status in rows: lines.append(f"| {cls} | {name} | {dur} | {status} |") summary = os.environ.get("GITHUB_STEP_SUMMARY", "/dev/null") with open(summary, "a") as fh: fh.write("\n".join(lines) + "\n") - name: Upload E2E reports uses: actions/upload-artifact@v7 if: always() with: name: e2e-reports-${{ matrix.name }} path: "e2e/build/reports" build-ui: runs-on: ubuntu-latest defaults: run: working-directory: ui steps: - uses: actions/checkout@v7 - name: Set up Node uses: actions/setup-node@v6 with: node-version: "22" cache: "yarn" cache-dependency-path: ui/yarn.lock - name: Install Dependencies run: yarn install - name: Build UI run: yarn run build - name: Cache Playwright browsers uses: actions/cache@v5 id: playwright-cache with: path: ~/.cache/ms-playwright key: playwright-chromium-${{ hashFiles('ui/yarn.lock') }} - name: Install Playwright browsers if: steps.playwright-cache.outputs.cache-hit != 'true' run: npx playwright install --with-deps chromium - name: Install Playwright browser deps (cached) if: steps.playwright-cache.outputs.cache-hit == 'true' run: npx playwright install-deps chromium - name: Run Playwright E2E Tests run: yarn test:e2e - name: Upload Playwright report uses: actions/upload-artifact@v7 if: failure() with: name: playwright-report path: ui/playwright-report retention-days: 7