- Convert 17 hand-rolled tests to PHPUnit TestCases (tests: stubs pattern) P0 security tests preserved: key-leak/encryption (KeyStoreTest), capture-allowlist/executor-path sentinel (CaptureWriterTest) - Add phpunit.xml.dist + update tests/bootstrap.php (Composer autoloader first) - Add phpunit/phpunit ^9.6 + yoast/phpunit-polyfills ^2.0 to require-dev - composer test -> vendor/bin/phpunit --configuration phpunit.xml.dist - ci.yml: thin caller of peptide-e2e reusable CI (tests: stubs, has_js: true, workflow_call trigger, permissions: contents: write) - deploy.yml: estate pattern caller of peptide-e2e deploy-app.yml@main (target_path: wp-content/plugins/pr-vision, gated on reusable CI) Pull-deploy on Hostinger stays as break-glass - Add cto-review.yml (Haiku-only, A/B panel dropped 2026-06-12) - Add main-push-audit.yml (direct-push tripwire) Agent-Session: claude-adopt-ci-20260616
74 lines
3.0 KiB
YAML
74 lines
3.0 KiB
YAML
name: Audit — Direct push to main
|
|
|
|
# Tripwire: fires on every push to main and flags pushes that did not come
|
|
# from a merged PR. We run GitHub on the free plan, which means we can't use
|
|
# branch protection on private repos. This is the soft-enforcement layer.
|
|
#
|
|
# Behavior:
|
|
# - If the pushed commit is associated with a merged PR, exit quietly.
|
|
# - If not, open a GitHub issue tagging the pusher so it's visible.
|
|
#
|
|
# This never blocks the push. It's an audit, not a gate.
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
|
|
permissions:
|
|
contents: read
|
|
issues: write
|
|
pull-requests: read
|
|
|
|
jobs:
|
|
audit:
|
|
name: Check commit came from PR
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Check for associated PR
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const sha = context.sha;
|
|
// Squash-merge -> PR association is eventually-consistent; poll briefly
|
|
// before concluding this was a direct push (avoids false positives on merges).
|
|
async function findMergedPR() {
|
|
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
commit_sha: sha,
|
|
});
|
|
const m = prs.find(p => p.merged_at);
|
|
if (m) return m;
|
|
if (attempt < 5) await new Promise(r => setTimeout(r, 10000));
|
|
}
|
|
return null;
|
|
}
|
|
const merged = await findMergedPR();
|
|
if (merged) {
|
|
core.info(`OK: commit ${sha.substring(0,7)} came from PR #${merged.number}`);
|
|
return;
|
|
}
|
|
const pusher = context.payload.pusher?.name || context.actor;
|
|
const msg = context.payload.head_commit?.message?.split('\n')[0] || '(no message)';
|
|
const body = [
|
|
`**Direct push to \`main\` detected** — commit was not associated with a merged PR.`,
|
|
``,
|
|
`- Commit: \`${sha.substring(0,7)}\` — ${msg}`,
|
|
`- Pusher: @${pusher}`,
|
|
`- Workflow run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
|
|
``,
|
|
`The project policy is that all changes to \`main\` go through a pull request so they can be reviewed before deploying to production. If this push was intentional (hotfix, infra change) close this issue with a comment explaining why.`,
|
|
``,
|
|
`_This issue was opened automatically by \`.github/workflows/main-push-audit.yml\`._`,
|
|
].join('\n');
|
|
await github.rest.issues.create({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
title: `Direct push to main: ${sha.substring(0,7)}`,
|
|
body,
|
|
labels: ['policy-violation', 'audit'],
|
|
});
|
|
core.warning(`Opened audit issue for direct push ${sha.substring(0,7)}`);
|
|
|