fix workflow fail when no test files #8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Module Container Tests | |
| permissions: | |
| contents: read | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - "src/modules/**" | |
| - "src/server/core/**" | |
| - ".github/workflows/test-module-container.yml" | |
| workflow_dispatch: | |
| jobs: | |
| discover-modules: | |
| name: Discover modules to test | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.discover.outputs.matrix }} | |
| has_modules: ${{ steps.discover.outputs.has_modules }} | |
| selected_modules: ${{ steps.discover.outputs.selected_modules }} | |
| steps: | |
| - name: checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: set commit range | |
| id: commit-range | |
| shell: bash | |
| run: | | |
| base_sha="${{ github.event.before }}" | |
| head_sha="${{ github.sha }}" | |
| if [[ "$base_sha" =~ ^0+$ ]]; then | |
| base_sha="$(git rev-parse "$head_sha^")" | |
| fi | |
| echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT" | |
| echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT" | |
| - name: discover changed and testable modules | |
| id: discover | |
| env: | |
| BASE_SHA: ${{ steps.commit-range.outputs.base_sha }} | |
| HEAD_SHA: ${{ steps.commit-range.outputs.head_sha }} | |
| GITHUB_EVENT_NAME: ${{ github.event_name }} | |
| run: | | |
| node <<'NODE' | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| const { execSync } = require("child_process"); | |
| const baseSha = process.env.BASE_SHA; | |
| const headSha = process.env.HEAD_SHA; | |
| const eventName = process.env.GITHUB_EVENT_NAME; | |
| let changedFiles = []; | |
| try { | |
| const diff = execSync(`git diff --name-only ${baseSha} ${headSha}`, { encoding: "utf8" }); | |
| changedFiles = diff.split("\n").map((s) => s.trim()).filter(Boolean); | |
| } catch { | |
| changedFiles = []; | |
| } | |
| const changedModules = Array.from( | |
| new Set( | |
| changedFiles | |
| .map((file) => { | |
| const match = file.match(/^src\/modules\/([^/]+)\//); | |
| return match ? match[1] : null; | |
| }) | |
| .filter(Boolean) | |
| ) | |
| ); | |
| const coreChanged = | |
| eventName === "workflow_dispatch" || | |
| changedFiles.some((file) => file.startsWith("src/server/core/")); | |
| const modulesRoot = path.join("src", "modules"); | |
| const testableModules = []; | |
| function hasTestFiles(dirPath) { | |
| const stack = [dirPath]; | |
| while (stack.length > 0) { | |
| const current = stack.pop(); | |
| const entries = fs.readdirSync(current, { withFileTypes: true }); | |
| for (const entry of entries) { | |
| if (entry.name === "node_modules" || entry.name.startsWith(".")) { | |
| continue; | |
| } | |
| const fullPath = path.join(current, entry.name); | |
| if (entry.isDirectory()) { | |
| stack.push(fullPath); | |
| continue; | |
| } | |
| if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(entry.name)) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| for (const moduleName of fs.readdirSync(modulesRoot)) { | |
| const containerPath = path.join(modulesRoot, moduleName, "container"); | |
| const packagePath = path.join(modulesRoot, moduleName, "container", "package.json"); | |
| if (!fs.existsSync(packagePath)) { | |
| continue; | |
| } | |
| try { | |
| const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8")); | |
| if (pkg?.scripts?.test && hasTestFiles(containerPath)) { | |
| testableModules.push(moduleName); | |
| } | |
| } catch { | |
| // Skip invalid package.json files. | |
| } | |
| } | |
| const selected = coreChanged | |
| ? testableModules | |
| : testableModules.filter((moduleName) => changedModules.includes(moduleName)); | |
| const matrix = selected.map((moduleName) => ({ | |
| module: moduleName, | |
| })); | |
| const out = process.env.GITHUB_OUTPUT; | |
| fs.appendFileSync(out, `matrix=${JSON.stringify(matrix)}\n`); | |
| fs.appendFileSync(out, `has_modules=${matrix.length > 0}\n`); | |
| fs.appendFileSync(out, `selected_modules=${selected.join(",")}\n`); | |
| NODE | |
| module-tests: | |
| name: Module Tests | |
| needs: discover-modules | |
| if: needs.discover-modules.outputs.has_modules == 'true' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJson(needs.discover-modules.outputs.matrix) }} | |
| steps: | |
| - name: checkout repository | |
| uses: actions/checkout@v6 | |
| - name: module under test | |
| run: | | |
| echo "Testing module: ${{ matrix.module }}" | |
| - name: setup node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| - name: ensure module test Dockerfile exists | |
| run: | | |
| dockerfile_path="src/modules/${{ matrix.module }}/container/Dockerfile.test" | |
| if [[ -f "$dockerfile_path" ]]; then | |
| echo "Using existing test Dockerfile at $dockerfile_path" | |
| exit 0 | |
| fi | |
| echo "No test Dockerfile found at $dockerfile_path, creating CI test Dockerfile" | |
| cat > "$dockerfile_path" <<EOF_DOCKER | |
| FROM node:22-alpine | |
| WORKDIR /home/node/module | |
| COPY src/server/core ./core | |
| COPY src/modules/${{ matrix.module }}/container ./ | |
| RUN npm install | |
| CMD ["npm", "run", "development"] | |
| EOF_DOCKER | |
| - name: run tests for module | |
| run: | | |
| cd "src/modules/${{ matrix.module }}/container" | |
| npm ci | |
| npm run test | |
| no-modules-changed: | |
| name: No module tests required | |
| needs: discover-modules | |
| if: needs.discover-modules.outputs.has_modules != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: print skip reason | |
| run: echo "No changed modules with both a test script and test files were detected." |