* ci(security): run deterministic eval tests in affected-area CI (Task 04) The build-check job validated build + suite structure but never ran the tests, and its summary referenced a `test:ci` script that did not exist. Make affected-area CI meaningful: - Add `test:ci` to evals/framework running the offline, deterministic Vitest allowlist (7 files / 112 tests) — no model, network, or paid execution. - Add a required "Run deterministic tests" step to build-check; a failure fails the job and the existing PR summary gate (which keys on build-check.result) fails closed. - Correct the summary note to state test:ci runs the deterministic allowlist and that model/network agent suites are excluded on PRs. The broader deterministic suite has 3 pre-existing failing files (framework-confidence, logger, test-runner; confirmed failing before this work) which are intentionally out of the allowlist and tracked separately. Validated locally: build, validate:suites:all, and test:ci all exit 0. * ci(security): least privilege permissions + pin actions to SHAs (Task 05) Harden all 9 GitHub Actions workflows: Least privilege: - Add explicit top-level `permissions: contents: read` to installer-checks, validate-test-suites, and opencode (opencode's job keeps the writes it needs; the top level now defaults to read). - Every workflow now declares an explicit top-level permissions block. - Write scopes retained only where justified (releases, doc/registry commits, follow-up PRs, and sync-docs' issues.create). Immutable action pinning: - Pin every `uses:` to a full 40-char commit SHA with a version comment, including first-party actions/* (checkout, github-script, setup-node, upload-artifact) and third-party (oven-sh/setup-bun). - Replace the two mutable refs that could change under us at any push: ludeeus/action-shellcheck@master and sst/opencode/github@latest. Record all SHAs, versions, and permission justifications in docs/maintenance/repository-recovery/action-pin-inventory.md. Verified: all 9 workflows parse as valid YAML; no floating tag/branch refs remain; every workflow has a top-level permissions block. * docs(security): add SECURITY.md and private-reporting checklist (Task 06) - Add root SECURITY.md: supported versions (0.7.x), private reporting via GitHub's "Report a vulnerability", best-effort response expectations, coordinated disclosure, and no-secret guidance. - Add private-vulnerability-reporting checklist that separates repository file work (this PR) from GitHub settings changes that require their own approval (Task 07: enabling private vulnerability reporting). No external links to validate; the reporting path uses the repo Security tab. SECURITY.md documents the private reporting path ahead of Task 07 enabling it. * ci(security): add dependency governance and ownership controls (Task 08) - .github/dependabot.yml: weekly grouped github-actions updates (keeps the Task 05 SHA pins current). Scoped to actions only — the primary lockfile is bun.lock and Dependabot's npm ecosystem would desync it; npm dependency risk is covered by dependency-review below. - .github/workflows/dependency-review.yml: runs on PRs with contents: read, SHA-pinned actions, fail-on-severity: high to block newly introduced high-severity vulnerabilities (exceptions documented inline). - .github/CODEOWNERS: default owner @darrenhinde with explicit ownership of security-sensitive paths (workflows, dependabot, CODEOWNERS, SECURITY.md, scripts/validation). Verified: dependabot.yml and dependency-review.yml are valid YAML; dependency-review actions are SHA-pinned with contents: read; CODEOWNERS owner is the repository owner. * docs(security): record GitHub security settings evidence (Task 07) Enable private vulnerability reporting and record read-back evidence. Settings changed (each approved immediately before, verified by read-back): - Private vulnerability reporting: enabled=false -> enabled=true. The Security tab now offers "Report a vulnerability", making the path documented in SECURITY.md live. - Dependency Graph / Dependabot alerts: 404 -> 204. This was an undocumented prerequisite of Task 08's dependency-review.yml, which failed with "Dependency graph is not enabled"; the Dependency Review check on PR #336 went fail -> pass after enabling it. No other repository setting was modified; secret scanning and push protection are unchanged, and dependabot_security_updates remains disabled (out of scope, would need its own approval). Both changes are reversible via the corresponding DELETE calls. Marks Part B of the private-vulnerability-reporting checklist complete.
322 lines
13 KiB
YAML
322 lines
13 KiB
YAML
name: PR Checks
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [main, dev]
|
|
types: [opened, edited, synchronize, reopened]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
pr-title-check:
|
|
name: Validate PR Title
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
title-valid: ${{ steps.validate.outputs.valid }}
|
|
|
|
steps:
|
|
- name: Check PR title format
|
|
id: validate
|
|
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
|
with:
|
|
script: |
|
|
const prTitle = context.payload.pull_request.title;
|
|
|
|
// Conventional commit patterns
|
|
const patterns = {
|
|
feat: /^feat(\(.+\))?!?:\s.+/,
|
|
fix: /^fix(\(.+\))?!?:\s.+/,
|
|
docs: /^docs(\(.+\))?:\s.+/,
|
|
style: /^style(\(.+\))?:\s.+/,
|
|
refactor: /^refactor(\(.+\))?:\s.+/,
|
|
perf: /^perf(\(.+\))?:\s.+/,
|
|
test: /^test(\(.+\))?:\s.+/,
|
|
chore: /^chore(\(.+\))?:\s.+/,
|
|
ci: /^ci(\(.+\))?:\s.+/,
|
|
build: /^build(\(.+\))?:\s.+/,
|
|
revert: /^revert(\(.+\))?:\s.+/,
|
|
alpha: /^\[alpha\]\s.+/,
|
|
beta: /^\[beta\]\s.+/,
|
|
rc: /^\[rc\]\s.+/
|
|
};
|
|
|
|
// Check if title matches any pattern
|
|
const matchedType = Object.entries(patterns).find(([type, pattern]) =>
|
|
pattern.test(prTitle)
|
|
);
|
|
|
|
if (!matchedType) {
|
|
const validExamples = [
|
|
'✅ feat(evals): add new evaluator',
|
|
'✅ fix(agents): correct delegation logic',
|
|
'✅ docs(readme): update installation guide',
|
|
'✅ test(evals): add execution-balance tests',
|
|
'✅ chore(deps): update dependencies',
|
|
'✅ feat!: breaking API change',
|
|
'✅ [alpha] experimental feature'
|
|
];
|
|
|
|
const message = `
|
|
## ❌ Invalid PR Title Format
|
|
|
|
**Current title:** \`${prTitle}\`
|
|
|
|
### Required Format
|
|
PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/) format:
|
|
|
|
\`\`\`
|
|
<type>(<scope>): <description>
|
|
\`\`\`
|
|
|
|
### Valid Types
|
|
- **feat** - New feature (triggers minor version bump: 0.3.0 → 0.4.0)
|
|
- **fix** - Bug fix (triggers patch version bump: 0.3.0 → 0.3.1)
|
|
- **docs** - Documentation changes (triggers patch bump)
|
|
- **test** - Test additions/changes (triggers patch bump)
|
|
- **refactor** - Code refactoring (triggers patch bump)
|
|
- **chore** - Maintenance tasks (triggers patch bump)
|
|
- **ci** - CI/CD changes (triggers patch bump)
|
|
- **perf** - Performance improvements (triggers patch bump)
|
|
- **style** - Code style changes (triggers patch bump)
|
|
- **build** - Build system changes (triggers patch bump)
|
|
- **revert** - Revert previous commit (triggers patch bump)
|
|
|
|
### Breaking Changes
|
|
- **feat!:** or **fix!:** - Breaking change (triggers major version bump: 0.3.0 → 1.0.0)
|
|
- **BREAKING CHANGE:** in description
|
|
|
|
### Pre-release Tags
|
|
- **[alpha]** - Alpha release (0.3.0 → 0.3.1-alpha.1)
|
|
- **[beta]** - Beta release (0.3.0 → 0.3.1-beta.1)
|
|
- **[rc]** - Release candidate (0.3.0 → 0.3.1-rc.1)
|
|
|
|
### Valid Examples
|
|
${validExamples.map(ex => `- ${ex}`).join('\n')}
|
|
|
|
### Why This Matters
|
|
- ✅ Enables automatic semantic versioning
|
|
- ✅ Generates meaningful changelogs
|
|
- ✅ Makes commit history searchable
|
|
- ✅ Clarifies the impact of changes
|
|
|
|
### How to Fix
|
|
Edit your PR title to match the format above.
|
|
`;
|
|
|
|
core.info(message);
|
|
await core.summary.addRaw(message).write();
|
|
core.setFailed(message);
|
|
} else {
|
|
const [type] = matchedType;
|
|
let versionBump = 'patch (0.3.0 → 0.3.1)';
|
|
|
|
if (type === 'feat' && prTitle.includes('!')) {
|
|
versionBump = 'major (0.3.0 → 1.0.0) - BREAKING CHANGE';
|
|
} else if (type === 'feat') {
|
|
versionBump = 'minor (0.3.0 → 0.4.0)';
|
|
} else if (type === 'fix' && prTitle.includes('!')) {
|
|
versionBump = 'major (0.3.0 → 1.0.0) - BREAKING CHANGE';
|
|
} else if (type === 'alpha') {
|
|
versionBump = 'alpha (0.3.0 → 0.3.1-alpha.1)';
|
|
} else if (type === 'beta') {
|
|
versionBump = 'beta (0.3.0 → 0.3.1-beta.1)';
|
|
} else if (type === 'rc') {
|
|
versionBump = 'rc (0.3.0 → 0.3.1-rc.1)';
|
|
}
|
|
|
|
const message = `
|
|
## ✅ PR Title Valid
|
|
|
|
**Title:** \`${prTitle}\`
|
|
**Type:** \`${type}\`
|
|
**Version bump:** ${versionBump}
|
|
|
|
When this PR is merged using **"Squash and Merge"**, the version will be automatically bumped.
|
|
`;
|
|
|
|
core.info(message);
|
|
|
|
// Set output for summary
|
|
core.summary
|
|
.addHeading('✅ PR Title Validation Passed', 2)
|
|
.addRaw(`**Title:** \`${prTitle}\``)
|
|
.addBreak()
|
|
.addRaw(`**Type:** \`${type}\``)
|
|
.addBreak()
|
|
.addRaw(`**Version bump:** ${versionBump}`)
|
|
.write();
|
|
|
|
// Set output for dependent jobs
|
|
core.setOutput('valid', 'true');
|
|
}
|
|
|
|
check-changes:
|
|
name: Detect Changed Files
|
|
runs-on: ubuntu-latest
|
|
needs: pr-title-check
|
|
if: needs.pr-title-check.outputs.title-valid == 'true'
|
|
outputs:
|
|
has-evals: ${{ steps.filter.outputs.has-evals }}
|
|
has-docs: ${{ steps.filter.outputs.has-docs }}
|
|
has-workflows: ${{ steps.filter.outputs.has-workflows }}
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Setup Bun
|
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
with:
|
|
bun-version: 1.3.14
|
|
|
|
- name: Check changed files
|
|
id: filter
|
|
run: |
|
|
echo "Changed files:" >&2
|
|
git diff --name-only -z "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" |
|
|
tee >(while IFS= read -r -d '' path; do printf ' %q\n' "$path" >&2; done) |
|
|
bun run scripts/validation/detect-pr-changes.ts
|
|
|
|
build-check:
|
|
name: Build & Validate
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
needs: [pr-title-check, check-changes]
|
|
if: |
|
|
needs.pr-title-check.outputs.title-valid == 'true' &&
|
|
needs.check-changes.outputs.has-evals == 'true'
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
with:
|
|
node-version: '20'
|
|
cache: 'npm'
|
|
cache-dependency-path: 'evals/framework/package-lock.json'
|
|
|
|
- name: Install dependencies
|
|
working-directory: evals/framework
|
|
run: npm ci
|
|
|
|
- name: Build framework
|
|
working-directory: evals/framework
|
|
run: npm run build
|
|
|
|
- name: Validate test suites
|
|
working-directory: evals/framework
|
|
run: npm run validate:suites:all
|
|
|
|
- name: Run deterministic tests
|
|
working-directory: evals/framework
|
|
run: npm run test:ci
|
|
|
|
- name: Summary
|
|
if: success()
|
|
run: |
|
|
echo "## ✅ Build Check Passed" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "- ✅ TypeScript compilation successful" >> $GITHUB_STEP_SUMMARY
|
|
echo "- ✅ Test suite validation passed" >> $GITHUB_STEP_SUMMARY
|
|
echo "- ✅ Deterministic tests passed (\`npm run test:ci\`)" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Note:** \`test:ci\` runs the offline, deterministic Vitest allowlist only." >> $GITHUB_STEP_SUMMARY
|
|
echo "Model- or network-dependent agent tests (\`eval:sdk\`, integration suites) are not run on PRs." >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Failure summary
|
|
if: failure()
|
|
run: |
|
|
echo "## ❌ Build Check Failed" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "Please check the logs above for details." >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Common fixes:**" >> $GITHUB_STEP_SUMMARY
|
|
echo "- TypeScript errors: Fix type issues in \`evals/framework/src/\`" >> $GITHUB_STEP_SUMMARY
|
|
echo "- YAML validation: Check test files in \`evals/agents/*/tests/\`" >> $GITHUB_STEP_SUMMARY
|
|
|
|
summary:
|
|
name: PR Checks Summary
|
|
runs-on: ubuntu-latest
|
|
needs: [pr-title-check, check-changes, build-check]
|
|
if: always()
|
|
|
|
steps:
|
|
- name: Generate summary
|
|
run: |
|
|
echo "## 📊 PR Checks Summary" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
|
|
# PR Title Check
|
|
if [ "${{ needs.pr-title-check.result }}" == "success" ]; then
|
|
echo "✅ **PR Title:** Valid conventional commit format" >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "❌ **PR Title:** Invalid format - please fix" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
# Changed Files Detection
|
|
if [ "${{ needs.check-changes.result }}" == "success" ]; then
|
|
echo "✅ **Changed Files:** Detected successfully" >> $GITHUB_STEP_SUMMARY
|
|
|
|
if [ "${{ needs.check-changes.outputs.has-evals }}" == "true" ]; then
|
|
echo " - 📦 Evals changes detected" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
if [ "${{ needs.check-changes.outputs.has-docs }}" == "true" ]; then
|
|
echo " - 📚 Docs changes detected" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
if [ "${{ needs.check-changes.outputs.has-workflows }}" == "true" ]; then
|
|
echo " - ⚙️ Workflow changes detected" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
elif [ "${{ needs.check-changes.result }}" == "failure" ]; then
|
|
echo "❌ **Changed Files:** Detection failed - check logs" >> $GITHUB_STEP_SUMMARY
|
|
elif [ "${{ needs.check-changes.result }}" == "cancelled" ]; then
|
|
echo "🚫 **Changed Files:** Detection cancelled" >> $GITHUB_STEP_SUMMARY
|
|
elif [ "${{ needs.check-changes.result }}" == "skipped" ]; then
|
|
echo "⏭️ **Changed Files:** Skipped (title validation failed)" >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "❓ **Changed Files:** Unknown result" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
# Build Check
|
|
if [ "${{ needs.build-check.result }}" == "success" ]; then
|
|
echo "✅ **Build & Validate:** Passed" >> $GITHUB_STEP_SUMMARY
|
|
elif [ "${{ needs.build-check.result }}" == "skipped" ]; then
|
|
if [ "${{ needs.check-changes.result }}" != "success" ]; then
|
|
echo "⏭️ **Build & Validate:** Skipped (change detection did not succeed)" >> $GITHUB_STEP_SUMMARY
|
|
elif [ "${{ needs.check-changes.outputs.has-evals }}" != "true" ]; then
|
|
echo "⏭️ **Build & Validate:** Skipped (no evals changes)" >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "⏭️ **Build & Validate:** Skipped (prerequisite not met)" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
elif [ "${{ needs.build-check.result }}" == "failure" ]; then
|
|
echo "❌ **Build & Validate:** Failed - check logs" >> $GITHUB_STEP_SUMMARY
|
|
elif [ "${{ needs.build-check.result }}" == "cancelled" ]; then
|
|
echo "🚫 **Build & Validate:** Cancelled" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
|
|
# Overall status
|
|
if [ "${{ needs.pr-title-check.result }}" == "success" ] && \
|
|
[ "${{ needs.check-changes.result }}" == "success" ] && \
|
|
{ [ "${{ needs.build-check.result }}" == "success" ] || \
|
|
{ [ "${{ needs.build-check.result }}" == "skipped" ] && \
|
|
[ "${{ needs.check-changes.outputs.has-evals }}" != "true" ]; }; }; then
|
|
echo "### ✅ All Required Checks Passed!" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "This PR is ready for review." >> $GITHUB_STEP_SUMMARY
|
|
else
|
|
echo "### ❌ Some Checks Failed" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "Please fix the failing checks before merging." >> $GITHUB_STEP_SUMMARY
|
|
exit 1
|
|
fi
|