smart-ci.ymlβ’12.5 kB
name: Smart CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:
# Prevent concurrent runs for the same branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
issues: write
env:
NODE_VERSION_MATRIX: '["20.x", "22.x"]'
jobs:
# Job 1: Analyze Changes for Smart Execution
change-analysis:
name: Analyze Changes
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
docs-only: ${{ steps.changes.outputs.docs-only }}
tests-only: ${{ steps.changes.outputs.tests-only }}
needs-integration: ${{ steps.changes.outputs.needs-integration }}
test-strategy: ${{ steps.changes.outputs.test-strategy }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Analyze changed files
id: changes
run: |
# Get changed files
if [ "${{ github.event_name }}" = "pull_request" ]; then
changed_files=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.number }}/files --jq '.[].filename')
else
changed_files=$(git diff --name-only HEAD~1 HEAD)
fi
echo "Changed files:"
echo "$changed_files"
# Analyze file types
docs_only=true
tests_only=true
needs_integration=false
while IFS= read -r file; do
case "$file" in
*.md|docs/*|README*|CHANGELOG*|LICENSE)
# Documentation files don't affect other flags
;;
test/*|*.test.*)
docs_only=false
;;
src/api/*|src/services/*|src/handlers/*)
docs_only=false
tests_only=false
needs_integration=true
;;
src/*)
docs_only=false
tests_only=false
;;
*)
docs_only=false
tests_only=false
;;
esac
done <<< "$changed_files"
# Determine test strategy
if [ "$docs_only" = "true" ]; then
test_strategy="smoke"
elif [ "$tests_only" = "true" ]; then
test_strategy="affected"
else
test_strategy="core"
fi
echo "docs-only=$docs_only" >> $GITHUB_OUTPUT
echo "tests-only=$tests_only" >> $GITHUB_OUTPUT
echo "needs-integration=$needs_integration" >> $GITHUB_OUTPUT
echo "test-strategy=$test_strategy" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Job 2: Lint and Type Check (always runs)
lint-and-typecheck:
name: Lint & Type Check
runs-on: ubuntu-latest
timeout-minutes: 10
needs: change-analysis
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter (src only)
run: npm run lint:src
- name: Enforce no new src warnings
run: npm run lint:guard
- name: Run type checker
run: npm run typecheck
- name: Check format
run: npm run check:format
# Optional: Lint tests (non-blocking)
lint-tests:
name: Lint Tests (non-blocking)
runs-on: ubuntu-latest
timeout-minutes: 10
needs: change-analysis
if: always()
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter (tests)
run: npm run lint:test
# Job 3: Smart Test Execution
smart-tests:
name: Smart Tests (${{ matrix.node-version }})
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [change-analysis, lint-and-typecheck]
strategy:
matrix:
node-version: [20.x, 22.x]
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Clean npm cache and install dependencies
run: |
# Fix for npm optional dependencies bug with Rollup
rm -rf node_modules package-lock.json
npm cache clean --force
npm install
- name: Build project
run: npm run build
- name: Execute smart test strategy
run: |
strategy="${{ needs.change-analysis.outputs.test-strategy }}"
echo "π§ͺ Executing test strategy: $strategy"
case "$strategy" in
smoke)
echo "π Documentation changes detected - running smoke tests"
npm run test:smoke
;;
affected)
echo "π― Test-only changes detected - running affected tests"
npm run test:affected
;;
core)
echo "π§ Source changes detected - running core tests"
npm run test:core
;;
full)
echo "π API/Service changes detected - running extended tests"
npm run test:extended
;;
esac
env:
SKIP_INTEGRATION_TESTS: true
- name: Performance budget check
if: matrix.node-version == '22.x'
run: |
strategy="${{ needs.change-analysis.outputs.test-strategy }}"
if [ "$strategy" != "smoke" ]; then
npm run perf:budgets -- --tests-only
fi
# Job 4: Integration Tests (conditional)
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [change-analysis]
if: |
needs.change-analysis.outputs.needs-integration == 'true' ||
contains(github.event.pull_request.labels.*.name, 'run-integration-tests')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Run integration tests
env:
ATTIO_API_KEY: ${{ secrets.ATTIO_TEST_API_KEY }}
ATTIO_WORKSPACE_ID: ${{ secrets.ATTIO_TEST_WORKSPACE_ID }}
run: |
if [ -z "$ATTIO_API_KEY" ]; then
echo "β οΈ Integration tests skipped: ATTIO_TEST_API_KEY not available"
exit 0
fi
npm run test:integration
# Job 5: Build Verification (skip for docs-only changes)
build:
name: Build Verification
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [change-analysis]
if: needs.change-analysis.outputs.docs-only != 'true'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Verify build artifacts
run: |
test -d dist/
test -f dist/index.js
test -f dist/cli/discover.js
- name: Test CLI execution
run: |
chmod +x dist/index.js
chmod +x dist/cli/discover.js
node dist/index.js --help || echo "CLI help test completed"
# Job 6: Performance Tests (main branch only)
performance:
name: Performance Tests
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [change-analysis]
if: |
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
contains(github.event.pull_request.labels.*.name, 'run-performance-tests')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Run performance budget checks
run: npm run perf:budgets -- --report --regression
- name: Upload performance results
if: always()
uses: actions/upload-artifact@v4
with:
name: performance-results
retention-days: 30
path: performance-results/
# Job 7: Smart Summary
smart-summary:
name: Smart CI Summary
runs-on: ubuntu-latest
needs: [change-analysis, lint-and-typecheck, smart-tests, build]
if: always() && github.event_name == 'pull_request'
steps:
- name: Create smart summary
uses: actions/github-script@v7
with:
script: |
const strategy = '${{ needs.change-analysis.outputs.test-strategy }}';
const docsOnly = '${{ needs.change-analysis.outputs.docs-only }}' === 'true';
let summary = '# π§ Smart CI Results\n\n';
summary += `**Strategy**: ${strategy.toUpperCase()}\n`;
summary += `**Docs Only**: ${docsOnly ? 'β
' : 'β'}\n\n`;
// Strategy explanation
summary += '## π Execution Strategy\n\n';
switch(strategy) {
case 'smoke':
summary += 'π **Documentation Changes**: Only smoke tests executed for safety\n';
break;
case 'affected':
summary += 'π― **Test Changes**: Affected tests executed based on impact analysis\n';
break;
case 'core':
summary += 'π§ **Source Changes**: Core test suite executed\n';
break;
case 'full':
summary += 'π **API/Service Changes**: Extended test suite executed\n';
break;
}
// Job results
const jobs = {
'Lint & Type Check': '${{ needs.lint-and-typecheck.result }}',
'Smart Tests': '${{ needs.smart-tests.result }}',
'Build Verification': '${{ needs.build.result || 'skipped' }}'
};
summary += '\n## π Job Results\n\n';
for (const [job, result] of Object.entries(jobs)) {
const emoji = result === 'success' ? 'β
' : result === 'failure' ? 'β' : result === 'skipped' ? 'βοΈ' : 'β οΈ';
summary += `${emoji} **${job}**: ${result}\n`;
}
// Time savings
summary += '\n## β‘ Efficiency Gains\n\n';
if (docsOnly) {
summary += 'π **Major time savings**: Skipped unnecessary builds and extended tests\n';
} else if (strategy === 'affected') {
summary += 'π― **Targeted execution**: Only affected tests run based on changes\n';
} else {
summary += 'βοΈ **Balanced approach**: Full validation for significant changes\n';
}
// Post comment
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(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('# π§ Smart CI Results')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: summary
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: summary
});
}