| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- # 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
- # ============================================================================
|