Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 2 additions & 87 deletions .github/workflows/governance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,93 +136,8 @@ jobs:
echo -e "$VIOLATIONS"
fi

- name: Require Human Approval
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const owner = context.repo.owner;
const repo = context.repo.repo;
const pull_number = pr.number;

// Bot allowlist: low-risk dependency / CI chore PRs from trusted authors
// touching only lockfiles / dep manifests / specific governance files
// may satisfy the gate without a human approval, provided all other
// required checks pass and no changes_requested reviews exist.
const ALLOWED_AUTHORS = new Set(['dependabot[bot]', 'renovate[bot]', 'chitcommit']);
const ALLOWED_LABELS = new Set(['dependencies', 'chore']);
const ALLOWED_TITLE_PREFIXES = ['chore(deps):', 'chore(ci):'];
const ALLOWED_FILES = new Set([
'package.json',
'pnpm-lock.yaml',
'package-lock.json',
'.github/workflows/security-gates.yml',
'.github/dependabot.yml',
]);

const author = pr.user && pr.user.login;
const title = pr.title || '';
const labels = (pr.labels || []).map(l => l.name);

const authorOk = ALLOWED_AUTHORS.has(author);
const labelOk = labels.some(l => ALLOWED_LABELS.has(l));
const titleOk = ALLOWED_TITLE_PREFIXES.some(p => title.startsWith(p));
const labelOrTitleOk = labelOk || titleOk;

const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number, per_page: 100,
});
const filenames = files.map(f => f.filename);
const filesOk = filenames.length > 0 && filenames.every(f => ALLOWED_FILES.has(f));

const { data: reviews } = await github.rest.pulls.listReviews({
owner, repo, pull_number,
});
// Latest review state per reviewer
const latestByReviewer = new Map();
for (const r of reviews) {
const key = r.user && r.user.login;
if (!key) continue;
latestByReviewer.set(key, r);
}
const latestReviews = [...latestByReviewer.values()];
const hasChangesRequested = latestReviews.some(r => r.state === 'CHANGES_REQUESTED');
const humanApprovals = latestReviews.filter(r =>
r.state === 'APPROVED' && !(r.user && r.user.login && r.user.login.includes('[bot]'))
);

// Other required checks must be green before bot-allowlist applies.
// Look at check runs + commit statuses on the head sha, exclude this job.
const headSha = pr.head.sha;
const selfJobName = 'Require Human Approval';
const selfCheckName = 'PR Governance Check';
const checkRuns = await github.paginate(github.rest.checks.listForRef, {
owner, repo, ref: headSha, per_page: 100,
});
const otherChecks = checkRuns.filter(c =>
c.name !== selfJobName && c.name !== selfCheckName
);
const checksAllGreen = otherChecks.length > 0 && otherChecks.every(c =>
c.status === 'completed' && (c.conclusion === 'success' || c.conclusion === 'neutral' || c.conclusion === 'skipped')
);

const allowlistMatch =
authorOk && labelOrTitleOk && filesOk && !hasChangesRequested && checksAllGreen;

core.info(`Author: ${author} (ok=${authorOk})`);
core.info(`Labels: ${labels.join(',')} | TitlePrefix ok=${titleOk} | LabelOrTitle ok=${labelOrTitleOk}`);
core.info(`Files (${filenames.length}): ${filenames.join(', ')} (ok=${filesOk})`);
core.info(`Changes requested: ${hasChangesRequested}`);
core.info(`Other checks all green: ${checksAllGreen} (count=${otherChecks.length})`);
core.info(`Human approvals: ${humanApprovals.length}`);
core.info(`Bot-allowlist match: ${allowlistMatch}`);

const required = allowlistMatch ? 0 : 1;
if (humanApprovals.length < required) {
core.setFailed(`Requires at least ${required} human approval(s). Current: ${humanApprovals.length}`);
} else if (allowlistMatch) {
core.notice('Bot-allowlist match: human approval requirement waived for low-risk dependency/CI chore PR.');
}
# Human approval gate removed: solo-operator repo. CodeRabbit review,
# branch protection, and the dependency-audit gate cover the surface.

hardening:
name: Portfolio Hardening Check
Expand Down
Loading