Files
calctext/.agents/skills/bmad-testarch-ci/github-actions-template.yaml
2026-03-16 19:54:53 -04:00

329 lines
10 KiB
YAML

# GitHub Actions CI/CD Pipeline for Test Execution
# Generated by BMad TEA Agent - Test Architect Module
# Optimized for: Parallel Sharding, Burn-In Loop
# Stack: {test_stack_type} | Framework: {test_framework}
#
# Variables to customize per project:
# INSTALL_CMD - dependency install command (e.g., npm ci, pnpm install --frozen-lockfile, yarn --frozen-lockfile)
# TEST_CMD - main test command (e.g., npm run test:e2e, npm test, npx vitest)
# LINT_CMD - lint command (e.g., npm run lint)
# BROWSER_INSTALL - browser install command (frontend/fullstack only; omit for backend)
# BROWSER_CACHE_PATH - browser cache path (frontend/fullstack only; omit for backend)
name: Test Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
schedule:
# Weekly burn-in on Sundays at 2 AM UTC
- cron: "0 2 * * 0"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# Lint stage - Code quality checks
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Determine Node version
id: node-version
run: |
if [ -f .nvmrc ]; then
echo "value=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
echo "Using Node from .nvmrc"
else
echo "value=24" >> "$GITHUB_OUTPUT"
echo "Using default Node 24 (current LTS)"
fi
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ steps.node-version.outputs.value }}
cache: "npm"
- name: Install dependencies
run: npm ci # Replace with INSTALL_CMD
- name: Run linter
run: npm run lint # Replace with LINT_CMD
# Test stage - Parallel execution with sharding
test:
name: Test (Shard ${{ matrix.shard }})
runs-on: ubuntu-latest
timeout-minutes: 30
needs: lint
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- name: Determine Node version
id: node-version
run: |
if [ -f .nvmrc ]; then
echo "value=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
echo "Using Node from .nvmrc"
else
echo "value=22" >> "$GITHUB_OUTPUT"
echo "Using default Node 22 (current LTS)"
fi
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ steps.node-version.outputs.value }}
cache: "npm"
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install dependencies
run: npm ci # Replace with INSTALL_CMD
# Frontend/Fullstack only — remove this step for backend-only stacks
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium # Replace with BROWSER_INSTALL
- name: Run tests (shard ${{ matrix.shard }}/4)
run: npm run test:e2e -- --shard=${{ matrix.shard }}/4 # Replace with TEST_CMD + shard args
- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.shard }}
path: |
test-results/
playwright-report/
retention-days: 30
# Burn-in stage - Flaky test detection
burn-in:
name: Burn-In (Flaky Detection)
runs-on: ubuntu-latest
timeout-minutes: 60
needs: test
# Only run burn-in on PRs to main/develop or on schedule
if: github.event_name == 'pull_request' || github.event_name == 'schedule'
steps:
- uses: actions/checkout@v4
- name: Determine Node version
id: node-version
run: |
if [ -f .nvmrc ]; then
echo "value=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
echo "Using Node from .nvmrc"
else
echo "value=22" >> "$GITHUB_OUTPUT"
echo "Using default Node 22 (current LTS)"
fi
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ steps.node-version.outputs.value }}
cache: "npm"
# Frontend/Fullstack only — remove this step for backend-only stacks
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright # Replace with BROWSER_CACHE_PATH
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm ci # Replace with INSTALL_CMD
# Frontend/Fullstack only — remove this step for backend-only stacks
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium # Replace with BROWSER_INSTALL
# Note: Burn-in targets UI flakiness. For backend-only stacks, remove this job entirely.
- name: Run burn-in loop (10 iterations)
run: |
echo "🔥 Starting burn-in loop - detecting flaky tests"
for i in {1..10}; do
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔥 Burn-in iteration $i/10"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
npm run test:e2e || exit 1 # Replace with TEST_CMD
done
echo "✅ Burn-in complete - no flaky tests detected"
- name: Upload burn-in failure artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: burn-in-failures
path: |
test-results/
playwright-report/
retention-days: 30
# Report stage - Aggregate and publish results
report:
name: Test Report
runs-on: ubuntu-latest
needs: [test, burn-in]
if: always()
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Generate summary
run: |
echo "## Test Execution Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Status**: ${{ needs.test.result }}" >> $GITHUB_STEP_SUMMARY
echo "- **Burn-in**: ${{ needs.burn-in.result }}" >> $GITHUB_STEP_SUMMARY
echo "- **Shards**: 4" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.burn-in.result }}" == "failure" ]; then
echo "⚠️ **Flaky tests detected** - Review burn-in artifacts" >> $GITHUB_STEP_SUMMARY
fi
# ============================================================================
# EXTENSION PATTERNS — Script Injection Prevention
# ============================================================================
# When extending this template into reusable workflows, manual dispatch
# workflows, or composite actions, NEVER use ${{ inputs.* }} directly in
# run: blocks. Always pass through env: intermediaries.
#
# KEY PRINCIPLE: Inputs must be DATA, not COMMANDS.
# Pass inputs through env: and interpolate as quoted arguments into fixed
# commands. NEVER accept command-shaped inputs (e.g., install-command,
# test-command) that get executed as shell code — even through env:.
#
# --- Reusable Workflow (workflow_call) ---
#
# on:
# workflow_call:
# inputs:
# test-grep:
# description: 'Test grep filter (data only — not a command)'
# type: string
# required: false
# default: ''
# base-ref:
# description: 'Base branch for diff'
# type: string
# required: false
# default: 'main'
# burn-in-count:
# description: 'Number of burn-in iterations'
# type: string
# required: false
# default: '10'
#
# jobs:
# test:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# # Fixed command — not derived from inputs
# - name: Install dependencies
# run: npm ci
# # ✅ SAFE — input is DATA passed as an argument to a fixed command
# - name: Run tests
# env:
# TEST_GREP: ${{ inputs.test-grep }}
# run: |
# # Security: inputs passed through env: to prevent script injection
# if [ -n "$TEST_GREP" ]; then
# npx playwright test --grep "$TEST_GREP"
# else
# npx playwright test
# fi
#
# --- Manual Dispatch (workflow_dispatch) ---
#
# on:
# workflow_dispatch:
# inputs:
# test-grep:
# description: 'Test grep filter (data only — not a command)'
# type: string
# required: false
# environment:
# description: 'Target environment'
# type: choice
# options: [staging, production]
#
# jobs:
# run-tests:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# # ✅ SAFE — input is DATA interpolated into a fixed command
# - name: Run selected tests
# env:
# TEST_GREP: ${{ inputs.test-grep }}
# run: |
# # Security: inputs passed through env: to prevent script injection
# npx playwright test --grep "$TEST_GREP"
#
# --- Composite Action (action.yml) ---
#
# inputs:
# test-grep:
# description: 'Test grep filter (data only — not a command)'
# required: false
# default: ''
# burn-in-count:
# description: 'Number of burn-in iterations'
# required: false
# default: '10'
#
# runs:
# using: composite
# steps:
# # ✅ SAFE — inputs are DATA arguments to fixed commands
# - name: Run burn-in
# shell: bash
# env:
# TEST_GREP: ${{ inputs.test-grep }}
# BURN_IN_COUNT: ${{ inputs.burn-in-count }}
# run: |
# # Security: inputs passed through env: to prevent script injection
# for i in $(seq 1 "$BURN_IN_COUNT"); do
# echo "Burn-in iteration $i/$BURN_IN_COUNT"
# npx playwright test --grep "$TEST_GREP" || exit 1
# done
#
# ❌ NEVER DO THIS:
# # Direct ${{ inputs.* }} in run: — GitHub expression injection
# - run: npx playwright test --grep "${{ inputs.test-grep }}"
#
# # Executing input-derived env var as a command — still command injection
# - env:
# CMD: ${{ inputs.test-command }}
# run: $CMD
# ============================================================================