* fix(ci): harden PR gate enforcement * fix(schema): allow compatible positional evolution
96 lines
3.4 KiB
YAML
96 lines
3.4 KiB
YAML
name: AI Behavior Check
|
|
|
|
on:
|
|
pull_request_target:
|
|
types: [opened, synchronize, reopened, labeled, unlabeled]
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
statuses: write
|
|
|
|
jobs:
|
|
ai-behavior-check:
|
|
name: AI Behavior Policy Evaluator
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
# Deliberately do not check out or execute pull-request code here.
|
|
# pull_request_target keeps this policy anchored to the base branch.
|
|
- name: Check AI-generated PR boundaries
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const sha = context.payload.pull_request.head.sha;
|
|
const setStatus = (state, description) =>
|
|
github.rest.repos.createCommitStatus({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
sha,
|
|
state,
|
|
context: 'AI Behavior Check',
|
|
description,
|
|
});
|
|
|
|
await setStatus('pending', 'Evaluating AI-generated PR boundaries');
|
|
|
|
const labels = context.payload.pull_request.labels.map(({ name }) => name);
|
|
if (!labels.includes('ai-generated')) {
|
|
await setStatus('success', 'Not labeled ai-generated');
|
|
core.notice('Not an ai-generated PR; no AI-only policy applied.');
|
|
return;
|
|
}
|
|
|
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: context.issue.number,
|
|
per_page: 100,
|
|
});
|
|
|
|
const maxChangedFiles = 30;
|
|
if (files.length > maxChangedFiles) {
|
|
await setStatus(
|
|
'failure',
|
|
`Changes ${files.length} files; limit is ${maxChangedFiles}`
|
|
);
|
|
core.setFailed(
|
|
`AI-generated PR changes ${files.length} files; limit is ${maxChangedFiles}.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
const isProtectedPath = (filename) =>
|
|
typeof filename === 'string' &&
|
|
(
|
|
filename.startsWith('.github/workflows/') ||
|
|
filename.startsWith('scripts/policy/') ||
|
|
filename.startsWith('scripts/release/') ||
|
|
filename === 'test/fixtures/cli-interface-baseline.txt' ||
|
|
filename === '.goreleaser.yaml' ||
|
|
filename === 'Makefile'
|
|
);
|
|
const protectedPaths = [...new Set(
|
|
files
|
|
.flatMap(({ filename, previous_filename }) => [filename, previous_filename])
|
|
.filter(isProtectedPath)
|
|
)];
|
|
|
|
if (protectedPaths.length > 0) {
|
|
await setStatus('failure', 'Modifies protected release/CI infrastructure');
|
|
core.setFailed(
|
|
'AI-generated PR modifies protected release/CI infrastructure:\n' +
|
|
protectedPaths.map((filename) => ` - ${filename}`).join('\n') +
|
|
'\nSplit these changes into a human-owned PR with explicit review.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
await setStatus(
|
|
'success',
|
|
`Passed with ${files.length} changed files (limit ${maxChangedFiles})`
|
|
);
|
|
core.notice(
|
|
`AI behavior check passed (${files.length} changed files; limit ${maxChangedFiles}).`
|
|
);
|