Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b557603
chore(codex): bootstrap PR for issue #153
github-actions[bot] Dec 25, 2025
1d9572c
chore(codex-autofix): apply updates (PR #154)
github-actions[bot] Dec 25, 2025
04cb924
Add guard utils tests
Dec 25, 2025
9738168
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 25, 2025
53de4ce
Add agents PR meta orchestrator tests
Dec 25, 2025
d8d4367
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 25, 2025
aff756b
test: extend agents guard coverage
Dec 25, 2025
7f8e4ec
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 25, 2025
e49b898
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 25, 2025
7bff4c5
test: add keepalive gate runner coverage
Dec 25, 2025
82b4dc1
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 25, 2025
dc92e53
test: cover keepalive instruction template
Dec 25, 2025
50eaa20
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 25, 2025
d4c7a9b
test: cover keepalive post work and merge manager
Dec 25, 2025
0fa7aad
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 25, 2025
bcbea30
test: extend keepalive gate runner coverage
Dec 25, 2025
3465038
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 25, 2025
7f47768
test: cover missing keepalive labels
Dec 25, 2025
d61f23b
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 25, 2025
58f30d8
test(keepalive): add skip comment edge case coverage
Dec 26, 2025
717bc71
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
0674d1c
test: extend agents guard coverage
Dec 26, 2025
4764966
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
c314db3
test: expand merge manager label coverage
codex Dec 26, 2025
29c1c2c
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
ddbd440
test: extend merge-manager decision comment coverage
Dec 26, 2025
b297840
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
a19e682
test: expand guard coverage
Dec 26, 2025
46dc711
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
b5e84f8
test: cover keepalive gate retry cap
codex Dec 26, 2025
d812f1b
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
f6b4757
test: extend agents-guard safety coverage
codex Dec 26, 2025
4f04289
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
c534e09
test: expand keepalive guard utils coverage
Dec 26, 2025
bf5dea6
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
6e5aab9
test: cover team codeowner guard bypass
Dec 26, 2025
ac339f5
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
90c991f
test: extend keepalive guard utils coverage
Dec 26, 2025
a40e8f9
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
6db6fe3
test: expand agents pr meta orchestrator coverage
Dec 26, 2025
582d302
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
04d075a
test: extend keepalive guard coverage
codex Dec 26, 2025
80d01a7
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
34adfd4
test: cover case-insensitive allow-change label
codex Dec 26, 2025
dca8ab1
chore(codex-keepalive): apply updates (PR #154)
github-actions[bot] Dec 26, 2025
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
229 changes: 229 additions & 0 deletions .github/scripts/__tests__/agents-guard.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
'use strict';

const test = require('node:test');
const assert = require('node:assert/strict');

const { evaluateGuard, validatePullRequestTargetSafety } = require('../agents-guard');

const protectedFile = {
filename: '.github/workflows/agents-foo.yml',
status: 'modified',
};

const codeownersContent = '.github/workflows/agents-foo.yml @owner';

test('blocks protected workflow edits without label or approval', () => {
const result = evaluateGuard({
files: [protectedFile],
codeownersContent,
authorLogin: 'someone',
});

assert.equal(result.blocked, true);
assert.equal(result.needsLabel, true);
assert.equal(result.needsApproval, true);
assert.equal(result.hasAllowLabel, false);
assert.ok(result.failureReasons.some((reason) => reason.includes('Missing `agents:allow-change` label.')));
assert.ok(result.failureReasons.some((reason) => reason.includes('@owner')));
});

test('blocks protected edits with a comment body that lists next steps', () => {
const result = evaluateGuard({
files: [protectedFile],
codeownersContent,
authorLogin: 'someone',
});

assert.ok(result.commentBody);
assert.ok(result.commentBody.startsWith(result.marker));
assert.ok(result.commentBody.includes('**Next steps**'));
assert.ok(result.commentBody.includes('Apply the `agents:allow-change` label'));
assert.ok(result.commentBody.includes('Ask a CODEOWNER'));
assert.ok(result.commentBody.includes('.github/workflows/agents-foo.yml (modified)'));
});

test('allows protected workflow edits when the author is a codeowner', () => {
const result = evaluateGuard({
files: [protectedFile],
codeownersContent,
authorLogin: 'owner',
});

assert.equal(result.blocked, false);
assert.equal(result.hasCodeownerApproval, true);
assert.equal(result.needsLabel, false);
assert.equal(result.needsApproval, false);
});

test('allows protected workflow edits with codeowner review approval', () => {
const result = evaluateGuard({
files: [protectedFile],
codeownersContent,
authorLogin: 'someone',
reviews: [{
user: { login: 'owner' },
state: 'APPROVED',
}],
});

assert.equal(result.blocked, false);
assert.equal(result.hasCodeownerApproval, true);
assert.equal(result.needsLabel, false);
assert.equal(result.needsApproval, false);
assert.equal(result.hasAllowLabel, false);
});

test('treats allow-change labels as case-insensitive', () => {
const result = evaluateGuard({
files: [protectedFile],
codeownersContent,
authorLogin: 'someone',
labels: [{ name: 'Agents:Allow-Change' }],
reviews: [{
user: { login: 'owner' },
state: 'APPROVED',
}],
});

assert.equal(result.blocked, false);
assert.equal(result.hasAllowLabel, true);
assert.equal(result.hasCodeownerApproval, true);
});

test('blocks deletion of protected workflows that are not allowlisted', () => {
const result = evaluateGuard({
files: [{
filename: '.github/workflows/agents-foo.yml',
status: 'removed',
}],
});

assert.equal(result.blocked, true);
assert.ok(result.fatalViolations.some((reason) => reason.includes('was deleted')));
});

test('blocks renames of protected workflows that are not allowlisted', () => {
const result = evaluateGuard({
files: [{
filename: '.github/workflows/agents-foo-new.yml',
previous_filename: '.github/workflows/agents-foo.yml',
status: 'renamed',
}],
});

assert.equal(result.blocked, true);
assert.ok(result.fatalViolations.some((reason) => reason.includes('was renamed')));
});

test('allows removal of allowlisted workflow paths', () => {
const result = evaluateGuard({
files: [{
filename: '.github/workflows/agents-75-keepalive-on-gate.yml',
status: 'removed',
}],
});

assert.equal(result.blocked, false);
assert.equal(result.fatalViolations.length, 0);
});

test('does not allow label-only bypass without codeowner approval', () => {
const result = evaluateGuard({
files: [protectedFile],
codeownersContent,
labels: [{ name: 'agents:allow-change' }],
authorLogin: 'someone',
});

assert.equal(result.blocked, true);
assert.equal(result.hasAllowLabel, true);
assert.equal(result.needsApproval, true);
assert.ok(result.failureReasons.some((reason) => reason.includes('Request approval from a CODEOWNER')));
});

test('requires explicit approval when codeowners only list a team', () => {
const result = evaluateGuard({
files: [protectedFile],
codeownersContent: '.github/workflows/agents-foo.yml @octo/security',
labels: [{ name: 'agents:allow-change' }],
authorLogin: 'someone',
});

assert.equal(result.blocked, true);
assert.equal(result.hasAllowLabel, true);
assert.equal(result.needsApproval, true);
assert.ok(result.failureReasons.some((reason) => reason.includes('Request approval from a CODEOWNER.')));
});

test('validatePullRequestTargetSafety skips checks for non pull_request_target events', () => {
const result = validatePullRequestTargetSafety({
eventName: 'pull_request',
fsModule: {
readFileSync() {
throw new Error('unexpected read');
},
},
});

assert.deepEqual(result, { checked: false, violations: [] });
});

test('validatePullRequestTargetSafety blocks unsafe checkout and secrets usage', () => {
const workflowSource = [
'on: pull_request_target',
'jobs:',
' test:',
' runs-on: ubuntu-latest',
' steps:',
' - uses: actions/checkout@v4',
' with:',
' ref: ${{ github.event.pull_request.head.sha }}',
' - run: |',
' echo ${{ secrets.MY_SECRET }}',
].join('\n');

assert.throws(
() => validatePullRequestTargetSafety({
eventName: 'pull_request_target',
workflowPath: '.github/workflows/agents-guard.yml',
workspaceRoot: process.cwd(),
fsModule: { readFileSync: () => workflowSource },
}),
/Unsafe pull_request_target usage detected/,
);
});

test('validatePullRequestTargetSafety allows safe pull_request_target workflow', () => {
const workflowSource = [
'on: pull_request_target',
'jobs:',
' test:',
' runs-on: ubuntu-latest',
' steps:',
' - uses: actions/checkout@v4',
' with:',
' fetch-depth: 1',
' - run: echo "hello"',
].join('\n');

const result = validatePullRequestTargetSafety({
eventName: 'pull_request_target',
workflowPath: '.github/workflows/agents-guard.yml',
workspaceRoot: process.cwd(),
fsModule: { readFileSync: () => workflowSource },
});

assert.deepEqual(result, { checked: true, violations: [] });
});

test('validatePullRequestTargetSafety throws when workflow file cannot be read', () => {
assert.throws(
() => validatePullRequestTargetSafety({
eventName: 'pull_request_target',
workflowPath: '.github/workflows/agents-guard.yml',
workspaceRoot: process.cwd(),
fsModule: { readFileSync: () => { throw new Error('no access'); } },
}),
/Failed to read \.github\/workflows\/agents-guard\.yml: no access/,
);
});
Loading
Loading