diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 7f745119..56db5e5f 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -1,36 +1,245 @@ -name: Continuous Integration +# Dual-stack CI: auto-detects Java 8 (develop) vs Java 21 (breaking/) based +# on whether dev.cljs.edn exists in the checkout. After breaking/ merges to +# develop, the Java 8 path becomes dead code and can be removed. -on: +name: CI + +on: + push: + branches: [develop, main] pull_request: - branches: [develop] + branches: [develop, main] workflow_dispatch: +permissions: + contents: read + pull-requests: write + checks: write + jobs: - lint: - name: Run Linter and Tests + detect-stack: + name: Detect Stack runs-on: ubuntu-latest + outputs: + java-version: ${{ steps.detect.outputs.java-version }} + java-distribution: ${{ steps.detect.outputs.java-distribution }} + lein-version: ${{ steps.detect.outputs.lein-version }} + cljs-command: ${{ steps.detect.outputs.cljs-command }} + needs-datomic-pro: ${{ steps.detect.outputs.needs-datomic-pro }} + stack-label: ${{ steps.detect.outputs.stack-label }} steps: - name: Checkout - uses: actions/checkout@v3 - - name: Prepare java - uses: actions/setup-java@v3 + uses: actions/checkout@v4 + + - name: Detect stack from project files + id: detect + run: | + # dev.cljs.edn is the figwheel-main build config — only exists on + # breaking/2026-stack-modernization (Java 21, Datomic Pro, fig:build). + if [ -f "dev.cljs.edn" ]; then + echo "Detected: figwheel-main stack (Java 21, Datomic Pro)" + echo "java-version=21" >> $GITHUB_OUTPUT + echo "java-distribution=temurin" >> $GITHUB_OUTPUT + echo "lein-version=2.11.2" >> $GITHUB_OUTPUT + echo "cljs-command=fig:build" >> $GITHUB_OUTPUT + echo "needs-datomic-pro=true" >> $GITHUB_OUTPUT + echo "stack-label=Java 21 / figwheel-main / Datomic Pro" >> $GITHUB_OUTPUT + else + echo "Detected: legacy stack (Java 8, Datomic Free)" + echo "java-version=8" >> $GITHUB_OUTPUT + echo "java-distribution=zulu" >> $GITHUB_OUTPUT + echo "lein-version=2.9.10" >> $GITHUB_OUTPUT + echo "cljs-command=cljsbuild once dev" >> $GITHUB_OUTPUT + echo "needs-datomic-pro=false" >> $GITHUB_OUTPUT + echo "stack-label=Java 8 / cljsbuild / Datomic Free" >> $GITHUB_OUTPUT + fi + + test: + name: Test & Lint (${{ needs.detect-stack.outputs.stack-label }}) + runs-on: ubuntu-latest + needs: detect-stack + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK ${{ needs.detect-stack.outputs.java-version }} + uses: actions/setup-java@v4 with: - distribution: 'zulu' - java-version: 8.0.292 - - name: Load pdfbox from /lib - run: mkdir ~/.m2/repository/ && mkdir ~/.m2/repository/org/ && cp -rv ./lib/* ~/.m2/repository - - name: ls .m2 - run: ls -la ~/.m2/repository/org/ - - name: Install clojure tools - uses: DeLaGuardo/setup-clojure@11.0 + distribution: ${{ needs.detect-stack.outputs.java-distribution }} + java-version: ${{ needs.detect-stack.outputs.java-version }} + + - name: Set up Leiningen + uses: DeLaGuardo/setup-clojure@12.5 + with: + lein: ${{ needs.detect-stack.outputs.lein-version }} + + - name: Cache Maven dependencies + uses: actions/cache@v4 with: - # Install just one or all simultaneously - cli: 1.10.1.693 # Clojure CLI based on tools.deps - lein: 2.9.1 # or use 'latest' to always provision latest version of leiningen - boot: 2.8.3 # or use 'latest' to always provision latest version of boot - - name: Get leiningen version - run: lein -v + path: ~/.m2/repository + key: ${{ runner.os }}-maven-j${{ needs.detect-stack.outputs.java-version }}-${{ hashFiles('project.clj') }} + restore-keys: | + ${{ runner.os }}-maven-j${{ needs.detect-stack.outputs.java-version }}- + + - name: Load local libs (pdfbox) + run: | + mkdir -p ~/.m2/repository/org/ + cp -rv ./lib/org/* ~/.m2/repository/org/ + + # Datomic Pro jars are not committed to git. Download and maven-install + # them the same way .devcontainer/post-create.sh does. + - name: Install Datomic Pro + if: needs.detect-stack.outputs.needs-datomic-pro == 'true' + run: | + DATOMIC_VERSION=1.0.7482 + TARGET_DIR="lib/com/datomic/datomic-pro/${DATOMIC_VERSION}" + ZIP_PATH="/tmp/datomic-pro-${DATOMIC_VERSION}.zip" + DOWNLOAD_URL="https://datomic-pro-downloads.s3.amazonaws.com/${DATOMIC_VERSION}/datomic-pro-${DATOMIC_VERSION}.zip" + + echo "Downloading Datomic Pro ${DATOMIC_VERSION}..." + curl --fail --location --silent --show-error -o "$ZIP_PATH" "$DOWNLOAD_URL" + + mkdir -p "${TARGET_DIR}" + unzip -q "$ZIP_PATH" -d "${TARGET_DIR}" + + # Flatten nested directory if present (zip contains datomic-pro-VERSION/ subdir) + TOP_SUBDIR=$(find "${TARGET_DIR}" -mindepth 1 -maxdepth 1 -type d -print -quit || true) + if [ -n "${TOP_SUBDIR}" ] && [ -z "$(find "${TARGET_DIR}" -maxdepth 1 -type f -print -quit)" ]; then + mv "${TOP_SUBDIR}"/* "${TARGET_DIR}/" + rmdir "${TOP_SUBDIR}" + fi + + # Populate ~/.m2 with Datomic Pro artifacts + (cd "${TARGET_DIR}" && bash bin/maven-install) + echo "Datomic Pro ${DATOMIC_VERSION} installed to local Maven repo" + + - name: Install dependencies + run: lein deps + - name: Run linter - run: lein lint + id: lint + run: | + echo "## Lint Results" >> $GITHUB_STEP_SUMMARY + if lein lint 2>&1 | tee lint-output.txt; then + echo "**Lint passed** - no errors" >> $GITHUB_STEP_SUMMARY + else + echo "**Lint failed**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat lint-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + - name: Run tests - run: lein test \ No newline at end of file + id: test + run: | + echo "## Test Results" >> $GITHUB_STEP_SUMMARY + if lein test 2>&1 | tee test-output.txt; then + SUMMARY=$(grep -E "^Ran [0-9]+ tests" test-output.txt || echo "Tests completed") + echo "**Tests passed**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY + else + echo "**Tests failed**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -50 test-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Build ClojureScript + id: cljs + run: | + echo "## ClojureScript Build" >> $GITHUB_STEP_SUMMARY + if lein ${{ needs.detect-stack.outputs.cljs-command }} 2>&1 | tee cljs-output.txt; then + echo "**CLJS build succeeded**" >> $GITHUB_STEP_SUMMARY + else + echo "**CLJS build failed**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -50 cljs-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Post PR comment with results + if: github.event_name == 'pull_request' && always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + const lintOutput = fs.existsSync('lint-output.txt') ? fs.readFileSync('lint-output.txt', 'utf8') : 'No output'; + const testOutput = fs.existsSync('test-output.txt') ? fs.readFileSync('test-output.txt', 'utf8') : 'No output'; + const cljsOutput = fs.existsSync('cljs-output.txt') ? fs.readFileSync('cljs-output.txt', 'utf8') : 'No output'; + + const lintOk = '${{ steps.lint.outcome }}' === 'success'; + const testOk = '${{ steps.test.outcome }}' === 'success'; + const cljsOk = '${{ steps.cljs.outcome }}' === 'success'; + const allOk = lintOk && testOk && cljsOk; + const stackLabel = '${{ needs.detect-stack.outputs.stack-label }}'; + + const testMatch = testOutput.match(/Ran (\d+) tests containing (\d+) assertions/); + const testSummary = testMatch ? `${testMatch[1]} tests, ${testMatch[2]} assertions` : 'See logs'; + + const status = allOk ? 'All checks passed' : 'Some checks failed'; + const body = [ + `## ${status}`, + '', + '| Check | Status | Details |', + '|-------|--------|---------|', + `| Lint | ${lintOk ? 'Pass' : 'Fail'} | ${lintOk ? 'No errors' : 'See workflow logs'} |`, + `| Tests | ${testOk ? 'Pass' : 'Fail'} | ${testOk ? testSummary : 'See workflow logs'} |`, + `| CLJS Build | ${cljsOk ? 'Pass' : 'Fail'} | ${cljsOk ? 'Compiled' : 'See workflow logs'} |`, + '', + `**Stack**: ${stackLabel}`, + '', + '
', + 'Full test output', + '', + '```', + testOutput.slice(-2000), + '```', + '', + '
', + '', + '---', + `*[Workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})*` + ].join('\n'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + const botComment = comments.find(c => + c.user.type === 'Bot' && (c.body.includes('All checks passed') || c.body.includes('Some checks failed')) + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + + - name: Upload artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: ci-failure-logs + path: | + lint-output.txt + test-output.txt + cljs-output.txt + retention-days: 7 diff --git a/.github/workflows/docker-integration.yml b/.github/workflows/docker-integration.yml index 4ecf2ca0..0c9c86c6 100644 --- a/.github/workflows/docker-integration.yml +++ b/.github/workflows/docker-integration.yml @@ -1,3 +1,7 @@ +# Docker integration tests: script validation always runs, container tests +# only run when Docker Hub images are available (orcpub/orcpub:latest may +# not exist for all release cycles). + name: Docker Integration Test on: @@ -22,6 +26,8 @@ jobs: - name: Checkout uses: actions/checkout@v4 + # ── Script validation (always runs) ────────────────────────── + - name: Lint shell scripts run: | sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck @@ -71,13 +77,21 @@ jobs: fi echo "OK: --force regenerated .env with consistent passwords" + # ── Container tests (only if images are available) ─────────── + + - name: Pull container images + id: pull + continue-on-error: true + run: docker compose pull + - name: Start datomic (no deps) + if: steps.pull.outcome == 'success' run: | - docker compose pull docker compose up -d --no-deps datomic echo "Datomic container started, waiting for health..." - name: Wait for datomic healthy + if: steps.pull.outcome == 'success' run: | for i in $(seq 1 90); do CID=$(docker compose ps -q datomic 2>/dev/null) || true @@ -110,11 +124,13 @@ jobs: done - name: Start orcpub and web + if: steps.pull.outcome == 'success' run: | docker compose up -d docker compose ps - name: Wait for orcpub healthy + if: steps.pull.outcome == 'success' run: | for i in $(seq 1 90); do CID=$(docker compose ps -q orcpub 2>/dev/null) || true @@ -145,11 +161,13 @@ jobs: docker compose ps - name: Test — create user + if: steps.pull.outcome == 'success' run: | ./docker-user.sh create testadmin admin@test.local SecurePass123 echo "Exit code: $?" - name: Test — check user exists + if: steps.pull.outcome == 'success' run: | OUTPUT=$(./docker-user.sh check testadmin) echo "$OUTPUT" @@ -158,12 +176,14 @@ jobs: echo "$OUTPUT" | grep -q "true" # verified - name: Test — list includes user + if: steps.pull.outcome == 'success' run: | OUTPUT=$(./docker-user.sh list) echo "$OUTPUT" echo "$OUTPUT" | grep -q "testadmin" - name: Test — duplicate user fails + if: steps.pull.outcome == 'success' run: | if ./docker-user.sh create testadmin admin@test.local SecurePass123 2>&1; then echo "FAIL: Should have rejected duplicate user" @@ -172,9 +192,11 @@ jobs: echo "OK: Duplicate user correctly rejected" - name: Test — create second user + if: steps.pull.outcome == 'success' run: ./docker-user.sh create player2 player2@test.local AnotherPass456 - name: Test — list shows both users + if: steps.pull.outcome == 'success' run: | OUTPUT=$(./docker-user.sh list) echo "$OUTPUT" @@ -182,12 +204,14 @@ jobs: echo "$OUTPUT" | grep -q "player2" - name: Test — verify already-verified user is idempotent + if: steps.pull.outcome == 'success' run: | OUTPUT=$(./docker-user.sh verify testadmin) echo "$OUTPUT" echo "$OUTPUT" | grep -q "already verified" - name: Test — batch create users (with duplicates) + if: steps.pull.outcome == 'success' run: | cat > /tmp/test-users.txt <<'TXT' # Test batch file @@ -207,6 +231,7 @@ jobs: echo "OK: Batch created 2 new, skipped 1 duplicate" - name: Test — batch users appear in list + if: steps.pull.outcome == 'success' run: | OUTPUT=$(./docker-user.sh list) echo "$OUTPUT" @@ -214,6 +239,7 @@ jobs: echo "$OUTPUT" | grep -q "batch2" - name: Test — init creates admin from .env + if: steps.pull.outcome == 'success' run: | # Append INIT_ADMIN_* vars to .env printf '\nINIT_ADMIN_USER=initadmin\nINIT_ADMIN_EMAIL=initadmin@test.local\nINIT_ADMIN_PASSWORD=InitPass789\n' >> .env @@ -232,6 +258,7 @@ jobs: echo "OK: init created admin from .env" - name: Test — init is idempotent (re-run skips existing) + if: steps.pull.outcome == 'success' run: | # Running init again should not fail — duplicate is handled if ./docker-user.sh init 2>&1; then @@ -241,6 +268,7 @@ jobs: echo "OK: init correctly reports duplicate on re-run" - name: Test — check nonexistent user fails + if: steps.pull.outcome == 'success' run: | if ./docker-user.sh check nobody@nowhere.com 2>&1; then echo "FAIL: Should have reported user not found" @@ -249,6 +277,7 @@ jobs: echo "OK: Nonexistent user correctly not found" - name: Test — created user can log in via HTTP + if: steps.pull.outcome == 'success' run: | # Use nginx (port 443) since orcpub:8890 is not exposed to host RESPONSE=$(curl -sk -X POST https://localhost/login \ @@ -272,6 +301,7 @@ jobs: fi - name: Test — wrong password is rejected + if: steps.pull.outcome == 'success' run: | HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" \ -X POST https://localhost/login \ @@ -286,6 +316,15 @@ jobs: exit 1 fi + # ── Always-run steps ───────────────────────────────────────── + + - name: Container tests skipped (images unavailable) + if: steps.pull.outcome == 'failure' + run: | + echo "## Docker Integration" >> $GITHUB_STEP_SUMMARY + echo "Container images not available on Docker Hub." >> $GITHUB_STEP_SUMMARY + echo "Script validation passed. Container tests skipped." >> $GITHUB_STEP_SUMMARY + - name: Collect logs on failure if: failure() run: |