fix(policy): allow sync merges and longer PR titles (#117)
Some checks failed
ci / ci / ci (push) Has been cancelled
Release on reusable change / version (push) Has been cancelled
Release on reusable change / release (push) Has been cancelled

Refs: PLAT-62
This commit is contained in:
Adam Moussa 2026-08-04 14:41:48 -04:00 • committed by GitHub
parent 12e70a2279
commit 7ac3528750
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 75 additions and 7 deletions

View file

@ -2,6 +2,7 @@
PR conventions
- Title format: type(scope): description (DEV-123)
- type ∈ feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, release
- Maximum 120 characters, including the Jira suffix.
- Active Jira projects: DEV (product), PLAT (platform), SEC (security). INFRA is a closed archive.
- The Jira key is required at the end of the title in parentheses.
- Jira-exempt only: Dependabot PRs and permission-controlled emergency reverts.

View file

@ -83,7 +83,7 @@ jobs:
function validateTitle(title, isDependabot, jiraMaybeExempt) {
const errs = [];
if (title.length > 72) errs.push('Title is ' + title.length + ' chars — max 72');
if (title.length > 120) errs.push('Title is ' + title.length + ' chars — max 120');
const m = title.match(/^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|release)(\([^)]+\))?(!)?: (.+?)(\s+\((DEV|PLAT|SEC)-\d+\))?$/);
if (!m) {
errs.push('Title must match: type(scope): description (KEY-NNN). Allowed types: ' + CONV_TYPES.join(' '));
@ -185,6 +185,12 @@ jobs:
return errs;
}
function isSyncMergeCommit(commit) {
if (!Array.isArray(commit.parents) || commit.parents.length < 2) return false;
const subject = (commit.commit && commit.commit.message ? commit.commit.message : '').split('\n')[0];
return /^Merge (?:branch|remote-tracking branch) '[^']+' into \S.+$/.test(subject);
}
function detectAiFooter(text) {
return AI_FOOTER_RE.test(text);
}
@ -579,6 +585,7 @@ jobs:
validateBranch,
validateBody,
validateCommitSubject,
isSyncMergeCommit,
detectAiFooter,
isWorkflowFilename,
isActionManifestFilename,
@ -706,7 +713,9 @@ jobs:
if (!link.includes('rel="next"') || commits.length === 0) commitMore = false;
for (const c of commits) {
const subject = c.commit.message.split('\n')[0];
if (!isSyncMergeCommit(c)) {
for (const e of validateCommitSubject(subject)) addViolation('Commit ' + c.sha.slice(0, 8) + ': ' + e);
}
if (detectAiFooter(c.commit.message)) addViolation('Commit ' + c.sha.slice(0, 8) + ': AI attribution footer detected');
}
commitPage++;

View file

@ -14,7 +14,7 @@ Organization-level GitHub configuration for Sea Haven Industries.
### PR title
`type(scope): description (DEV-123)` — the Jira key is required at the end in parentheses. Active projects: **DEV** (product), **PLAT** (platform), **SEC** (security). INFRA is a closed archive. Jira-exempt only: Dependabot PRs and permission-controlled emergency reverts.
`type(scope): description (DEV-123)` — maximum 120 characters, including the Jira suffix. The Jira key is required at the end in parentheses. Active projects: **DEV** (product), **PLAT** (platform), **SEC** (security). INFRA is a closed archive. Jira-exempt only: Dependabot PRs and permission-controlled emergency reverts.
### PR body

View file

@ -92,7 +92,7 @@ async function loadValidators() {
assert.ok(v && typeof v === 'object', 'PR_POLICY_TEST escape must return an object');
for (const fn of [
'validateTitle', 'getJiraKey', 'validateBranch', 'validateBody',
'validateCommitSubject', 'detectAiFooter', 'isWorkflowFilename',
'validateCommitSubject', 'isSyncMergeCommit', 'detectAiFooter', 'isWorkflowFilename',
'validateWorkflowContent', 'parseRetryAfterMs', 'checkCommitLimit',
'checkFilesLimit', 'normalizeViolationFingerprint', 'filterNewViolations',
'fnv1a32', 'stripHash', 'classifyFileStatus',
@ -132,10 +132,30 @@ describe('validateTitle', () => {
assert.ok(errs.some(e => e.includes('type') || e.includes('match')), 'should mention type');
});
it('rejects title over 72 chars', () => {
const long = 'feat: ' + 'a'.repeat(60) + ' (DEV-1)';
it('accepts a human PR title at exactly 120 chars', () => {
const long = 'feat: ' + 'a'.repeat(106) + ' (DEV-1)';
assert.equal(long.length, 120);
assert.deepEqual(v.validateTitle(long, false), []);
});
it('accepts a Dependabot PR title at exactly 120 chars', () => {
const long = 'chore(deps): ' + 'a'.repeat(107);
assert.equal(long.length, 120);
assert.deepEqual(v.validateTitle(long, true), []);
});
it('rejects a human PR title at exactly 121 chars', () => {
const long = 'feat: ' + 'a'.repeat(107) + ' (DEV-1)';
assert.equal(long.length, 121);
const errs = v.validateTitle(long, false);
assert.ok(errs.some(e => e.includes('72')), 'should mention 72 char limit');
assert.ok(errs.some(e => e.includes('120')), 'should mention 120 char limit');
});
it('rejects a Dependabot PR title at exactly 121 chars', () => {
const long = 'chore(deps): ' + 'a'.repeat(108);
assert.equal(long.length, 121);
const errs = v.validateTitle(long, true);
assert.ok(errs.some(e => e.includes('120')), 'should mention 120 char limit');
});
it('rejects title ending with a period (Dependabot, no Jira required)', () => {
@ -382,6 +402,44 @@ describe('validateCommitSubject', () => {
});
});
describe('isSyncMergeCommit', () => {
it('recognizes a generated branch synchronization merge', () => {
const commit = {
parents: [{ sha: 'a' }, { sha: 'b' }],
commit: { message: "Merge branch 'main' into chore/pr-policy-rollout" },
};
assert.equal(v.isSyncMergeCommit(commit), true);
});
it('recognizes a generated remote-tracking synchronization merge', () => {
const commit = {
parents: [{ sha: 'a' }, { sha: 'b' }],
commit: { message: "Merge remote-tracking branch 'origin/main' into fix/example" },
};
assert.equal(v.isSyncMergeCommit(commit), true);
});
it('does not exempt an arbitrary multi-parent commit', () => {
const commit = {
parents: [{ sha: 'a' }, { sha: 'b' }],
commit: { message: 'Update policy files' },
};
assert.equal(v.isSyncMergeCommit(commit), false);
});
it('does not exempt a single-parent commit with a merge-shaped subject', () => {
const commit = {
parents: [{ sha: 'a' }],
commit: { message: "Merge branch 'main' into fix/example" },
};
assert.equal(v.isSyncMergeCommit(commit), false);
});
it('does not exempt commits with missing parent metadata', () => {
assert.equal(v.isSyncMergeCommit({}), false);
});
});
// ── detectAiFooter ────────────────────────────────────────────────────────────
describe('detectAiFooter', () => {