Skip to content

Merge branch 'main' of https://github.com/bbc/bug #26

Merge branch 'main' of https://github.com/bbc/bug

Merge branch 'main' of https://github.com/bbc/bug #26

name: Module Container Tests
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
permissions:
contents: read
on:
push:
branches:
- main
paths:
- "src/modules/**"
- "src/server/core/**"
- ".github/workflows/test-module-container.yml"
workflow_dispatch:
jobs:
slack-start:
name: Slack start message
runs-on: ubuntu-latest
outputs:
ts: ${{ steps.slack-start.outputs.ts }}
steps:
- name: checkout repository
uses: actions/checkout@v6
- name: send slack start message
id: slack-start
uses: ./.github/actions/slack-run-start
with:
message_prefix: Workflow running
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 }}
module_count: ${{ steps.discover.outputs.module_count }}
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`);
fs.appendFileSync(out, `module_count=${matrix.length}\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. Module count: ${{ needs.discover-modules.outputs.module_count }}"
slack-finish:
name: Slack final status
if: ${{ always() && needs.slack-start.result == 'success' && needs.slack-start.outputs.ts != '' }}
needs:
- slack-start
- discover-modules
- module-tests
- no-modules-changed
runs-on: ubuntu-latest
steps:
- name: checkout repository
uses: actions/checkout@v6
- name: set end timestamp
id: end-time
run: |
echo "value=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT"
- name: determine workflow status
id: workflow-status
run: |
workflow_status="success"
if [[ "${{ needs.discover-modules.result }}" == "failure" || "${{ needs.discover-modules.result }}" == "cancelled" || "${{ needs.module-tests.result }}" == "failure" || "${{ needs.module-tests.result }}" == "cancelled" || "${{ needs.no-modules-changed.result }}" == "failure" || "${{ needs.no-modules-changed.result }}" == "cancelled" ]]; then
workflow_status="failure"
fi
echo "status=$workflow_status" >> "$GITHUB_OUTPUT"
- name: update slack message
uses: ./.github/actions/slack-run-finish
with:
ts: ${{ needs.slack-start.outputs.ts }}
status: ${{ steps.workflow-status.outputs.status }}
message_prefix: Workflow
extra_text: ". Modules selected: ${{ needs.discover-modules.outputs.module_count }}"