name: PR Validation
on:
pull_request:
types: [opened, synchronize, reopened]
branches:
- main
- develop
env:
NODE_ENV: test
permissions:
contents: read
pull-requests: write
issues: write
actions: read
jobs:
pr-quality-gates:
name: Pull Request Quality Gates (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [18.x, 20.x, 22.x, 24.x]
defaults:
run:
shell: bash
working-directory: server
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
cache-dependency-path: server/package-lock.json
- name: Install dependencies
run: npm ci --prefer-offline --no-audit
- name: Validate generated artifacts (contracts)
run: npm run validate:contracts
- name: Validate action inventory (metadata)
run: npm run validate:metadata
- name: Typecheck
run: npm run typecheck
- name: Lint (ratchet)
run: npm run lint:ratchet
- name: Build
run: npm run build
- name: Startup smoke test (built dist)
run: npm run start:test
- name: Core validation (tests)
run: npm run test:ci
timeout-minutes: 15
- name: Plugin & hooks validation (E2E)
run: npm run test:e2e
- name: Changed files analysis
if: matrix.node == '24.x'
working-directory: .
run: |
echo "Analyzing changed files in this PR..."
if git diff --name-only origin/${{ github.base_ref }}...HEAD > changed_files.txt 2>/dev/null; then
echo "✅ Successfully analyzed changed files"
else
echo "⚠️ Could not determine changed files, using fallback"
git diff --name-only HEAD~1 HEAD > changed_files.txt 2>/dev/null || echo "No changes detected" > changed_files.txt
fi
echo "Files changed in this PR:"
cat changed_files.txt || echo "No changed files detected"
if grep -q "\\.ts$" changed_files.txt; then echo "✅ TypeScript files modified - validation completed"; fi
if grep -q "frameworks/" changed_files.txt; then echo "⚠️ Framework modules changed - confirm methodology switching"; fi
if grep -q "mcp-tools/" changed_files.txt; then echo "⚠️ MCP tools changed - ensure protocol compliance"; fi
if grep -q "runtime/" changed_files.txt; then echo "⚠️ Runtime changed - run startup smoke tests locally"; fi
- name: Comment PR with validation results
uses: actions/github-script@v8
if: always() && matrix.node == '24.x'
with:
script: |
const fs = require('fs');
let changedFiles = '';
try {
changedFiles = fs.readFileSync('changed_files.txt', 'utf8').trim();
if (!changedFiles) {
changedFiles = 'No changes detected';
}
} catch (e) {
console.log(`Warning: Could not read changed_files.txt: ${e.message}`);
changedFiles = 'Unable to read changed files (this can happen on force-pushes)';
}
const status = '${{ job.status }}';
const runUrl = `${context.payload.repository.html_url}/actions/runs/${context.runId}`;
let message;
if (status === 'success') {
message = [
'## ✅ PR Validation Passed',
'',
'The automated gates completed successfully.',
'',
'### Checks executed',
'- `npm run validate:contracts`',
'- `npm run validate:metadata`',
'- `npm run typecheck`',
'- `npm run lint:ratchet`',
'- `npm run build`',
'- `npm run start:test`',
'- `npm run test:ci`',
'',
'### Files changed',
'```',
changedFiles,
'```',
'',
`[View workflow run](${runUrl})`
].join('\n');
} else {
message = [
'## ❌ PR Validation Failed',
'',
'At least one automated gate failed. Review the logs and fix the issues before merging.',
'',
'### Checks executed',
'- `npm run validate:contracts`',
'- `npm run validate:metadata`',
'- `npm run typecheck`',
'- `npm run lint:ratchet`',
'- `npm run build`',
'- `npm run start:test`',
'- `npm run test:ci`',
'',
'### Files changed',
'```',
changedFiles,
'```',
'',
`[View workflow run](${runUrl})`,
'',
'**Common fixes**',
'- Run `cd server && npm run test:ci`'
].join('\n');
}
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const botComment = comments.data.find(comment => comment.user.type === 'Bot' && comment.body.includes('PR Validation'));
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: message
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: message
});
}