Skip to content
Merged
Show file tree
Hide file tree
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
21 changes: 14 additions & 7 deletions .github/workflows/pr-body-autoinject.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ name: PR Body Auto-Inject (reusable)
# Bot-authored PRs (Copilot SWE agent, GitHub Apps) freehand their bodies and
# do not read .github/PULL_REQUEST_TEMPLATE.md, so they fail the pr-policy
# Verification/Closes-N requirements. This workflow runs ONCE when a bot
# opens a non-doc PR and prepends a minimal stub that satisfies pr-policy.
# opens a non-doc PR and prepends a minimal scaffold. The scaffold is
# intentionally invalid until the agent records real verification.
# Human PRs are untouched.

on:
Expand Down Expand Up @@ -77,19 +78,25 @@ jobs:
const stub = [
'<!-- AUTO-INJECTED policy stub for bot-authored PR. Replace freely. -->',
'',
'## Linked Issue',
'## Summary',
'',
`Closes ${issueRef}`,
'TODO: Fill in summary.',
'',
'## Verification',
'',
'- [x] Automated CI checks green on this PR',
'- [ ] Manual verification — N/A for bot-authored fix',
'- [ ] TODO: Run required verification and replace this line.',
'',
'### Verification Notes',
'',
'_Auto-injected for bot-authored PR. CI-green is the verification surface._',
'_Human reviewer: if this PR touches user-visible behavior, replace this block with manual smoke results before merge._',
'TODO: Replace this scaffold with concrete command output, CI check names, or manual smoke-test notes.',
'',
'## Docs / Changelog',
'',
'TODO: Record docs/changelog handling.',
'',
'## Linked Issue',
'',
`TODO: Closes ${issueRef}`,
'',
'---',
'',
Expand Down
90 changes: 56 additions & 34 deletions .github/workflows/pr-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ on:
description: "Require at least one `- [x]` checked box in the body."
type: boolean
default: true
strict-body-contract:
description: "Require the canonical Summary / Verification / Verification Notes / Docs-Changelog structure."
type: boolean
default: true
verification-section-heading:
description: "H2 heading exact text (without the `## `)."
type: string
Expand Down Expand Up @@ -155,60 +159,78 @@ jobs:
REQUIRE_LINKED_ISSUE: ${{ inputs.require-linked-issue }}
REQUIRE_VERIFICATION_SECTION: ${{ inputs.require-verification-section }}
REQUIRE_CHECKED_BOX: ${{ inputs.require-checked-box }}
STRICT_BODY_CONTRACT: ${{ inputs.strict-body-contract }}
VERIFICATION_H2: ${{ inputs.verification-section-heading }}
VERIFICATION_H3: ${{ inputs.verification-notes-heading }}
DOC_EXT_LIST: ${{ inputs.doc-only-extensions }}
DOC_PREFIXES: ${{ inputs.doc-only-path-prefixes }}
WORKFLOW_LIBRARY_REF: ${{ inputs.workflow-library-ref }}
with:
script: |
const path = require('path');
const url = require('url');

const pr = context.payload.pull_request;
if (pr.draft) {
core.info('Draft PR; ready-for-review policy is advisory until draft is cleared.');
return;
}

const validatorPath = path.resolve(
process.env.GITHUB_WORKSPACE,
'__github-workflows__/scripts/pr-contract.mjs',
);
const workflowRef = process.env.WORKFLOW_LIBRARY_REF || '(unset)';
let validatePrContract;
let formatPrContractResult;
try {
({ validatePrContract, formatPrContractResult } = await import(url.pathToFileURL(validatorPath).href));
} catch (err) {
const msg = `Could not load PR contract validator at ${validatorPath} (workflow-library-ref=${workflowRef}): ${err && err.message ? err.message : err}.`;
core.setFailed(msg);
return;
}

const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
per_page: 100,
});
const docExtRe = new RegExp(`\\.(${process.env.DOC_EXT_LIST})$`, 'i');
const prefixes = process.env.DOC_PREFIXES
.split('\n')
.map((s) => s.trim())
.filter(Boolean);
const isDocFile = (p) => docExtRe.test(p) || prefixes.some((pre) => p.startsWith(pre));
if (files.length > 0 && files.every((f) => isDocFile(f.filename))) {
core.info(`Doc-only PR (${files.length} file${files.length === 1 ? '' : 's'}); verification ceremony skipped.`);
return;
}

const body = pr.body || '';
const failures = [];

if (process.env.REQUIRE_LINKED_ISSUE === 'true') {
if (!/(Closes|Fixes|Refs)\s+#\d+/i.test(body)) {
failures.push('PR body must link an issue with `Closes #N`, `Fixes #N`, or `Refs #N`.');
}
}

if (process.env.REQUIRE_VERIFICATION_SECTION === 'true') {
const h2 = new RegExp(`##\\s+${process.env.VERIFICATION_H2}`, 'i');
const h3 = new RegExp(`###\\s+${process.env.VERIFICATION_H3}`, 'i');
if (!h2.test(body) || !h3.test(body)) {
failures.push(`PR body must include \`## ${process.env.VERIFICATION_H2}\` and \`### ${process.env.VERIFICATION_H3}\` sections.`);
}
}

if (process.env.REQUIRE_CHECKED_BOX === 'true') {
if (!/-\s+\[[xX]\]/.test(body)) {
failures.push('PR body must record completed verification with at least one checked checkbox.');
}
}
const strict = process.env.STRICT_BODY_CONTRACT === 'true';
const requireVerification = process.env.REQUIRE_VERIFICATION_SECTION === 'true';
const result = validatePrContract({
title: pr.title || '',
body: pr.body || '',
branch: pr.head.ref || '',
files: files.map((file) => file.filename),
}, {
requireTitle: false,
requireBranch: false,
docOnlyExtensions: process.env.DOC_EXT_LIST,
docOnlyPathPrefixes: process.env.DOC_PREFIXES,
requireIssueLink: process.env.REQUIRE_LINKED_ISSUE === 'true',
requiredHeadings: strict
? undefined
: requireVerification
? [`## ${process.env.VERIFICATION_H2}`, `### ${process.env.VERIFICATION_H3}`]
: [],
requireSummary: strict && requireVerification,
requireDocsChangelog: strict && requireVerification,
requireSubstantiveVerificationNotes: strict && requireVerification,
rejectGenericVerificationNotes: strict && requireVerification,
requireCheckedVerification: process.env.REQUIRE_CHECKED_BOX === 'true',
requireEvidenceBlocks: false,
});

if (failures.length > 0) {
core.setFailed(failures.join('\n'));
const report = formatPrContractResult(result);
await core.summary
.addHeading(result.ok ? 'PR policy passed' : 'PR policy failed', 3)
.addCodeBlock(report, 'text')
.write();
if (!result.ok) {
core.setFailed(report);
}

# ----------------------------------------------------------------------
Expand Down
155 changes: 120 additions & 35 deletions .github/workflows/repo-required-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ on:
require-checked-box:
type: boolean
default: true
doc-only-extensions:
description: "Pipe-separated extensions (no dots) considered doc-only for the PR contract."
type: string
default: "md|txt|png|jpg|jpeg|gif|svg|webp|bmp|ico|avif"
doc-only-path-prefixes:
description: "Newline-separated path prefixes also treated as doc-only for the PR contract."
type: string
default: |
.changelog/
run-pr-contract:
type: boolean
default: true
Expand Down Expand Up @@ -295,55 +304,121 @@ jobs:
if: inputs.run-pr-contract && github.event_name == 'pull_request' && needs.detect.outputs.ok == 'true'
runs-on: ubuntu-latest
permissions:
pull-requests: read
contents: read
pull-requests: write
steps:
- name: Check out github-workflows for PR contract validator
uses: actions/checkout@v4
with:
repository: ArchonVII/github-workflows
ref: ${{ inputs.workflow-library-ref }}
path: __github-workflows__

- name: Enforce PR policy
uses: actions/github-script@v7
env:
DOCS_ONLY: ${{ needs.detect.outputs.docs-only }}
BRANCH_PATTERN: ${{ inputs.branch-pattern }}
REQUIRE_LINKED_ISSUE: ${{ inputs.require-linked-issue }}
REQUIRE_VERIFICATION_SECTION: ${{ inputs.require-verification-section }}
REQUIRE_CHECKED_BOX: ${{ inputs.require-checked-box }}
DOC_EXT_LIST: ${{ inputs.doc-only-extensions }}
DOC_PREFIXES: ${{ inputs.doc-only-path-prefixes }}
WORKFLOW_LIBRARY_REF: ${{ inputs.workflow-library-ref }}
with:
script: |
const path = require('path');
const url = require('url');

const pr = context.payload.pull_request;
if (pr.draft) {
core.info('Draft PR; pr contract is advisory until ready_for_review.');
return;
}

const failures = [];
const branch = pr.head.ref || '';
const branchRe = new RegExp(process.env.BRANCH_PATTERN);
if (!branchRe.test(branch)) {
failures.push(`Head branch \`${branch}\` does not match required pattern.`);
}
const validatorPath = path.resolve(
process.env.GITHUB_WORKSPACE,
'__github-workflows__/scripts/pr-contract.mjs',
);
const ref = process.env.WORKFLOW_LIBRARY_REF || '(unset)';

if (!/^(feat|fix|refactor|test|docs|style|chore|perf|ci|build|revert)(\([^)]+\))?: .+/.test(pr.title || '')) {
failures.push('PR title must use Conventional Commits format.');
let validatePrContract;
let formatPrContractResult;
try {
({ validatePrContract, formatPrContractResult } = await import(url.pathToFileURL(validatorPath).href));
} catch (err) {
const msg = `Could not load PR contract validator at ${validatorPath} (workflow-library-ref=${ref}): ${err && err.message ? err.message : err}.`;
core.setFailed(msg);
return;
}

const docsOnly = process.env.DOCS_ONLY === 'true';
if (!docsOnly) {
const body = pr.body || '';
if (process.env.REQUIRE_LINKED_ISSUE === 'true' && !/(Closes|Fixes|Refs)\s+#\d+/i.test(body)) {
failures.push('PR body must link an issue with `Closes #N`, `Fixes #N`, or `Refs #N`.');
}
if (process.env.REQUIRE_VERIFICATION_SECTION === 'true' && !(/##\s+Verification/i.test(body) && /###\s+Verification Notes/i.test(body))) {
failures.push('PR body must include `## Verification` and `### Verification Notes` sections.');
}
if (process.env.REQUIRE_CHECKED_BOX === 'true' && !/-\s+\[[xX]\]/.test(body)) {
failures.push('PR body must record completed verification with at least one checked checkbox.');
const filesPaged = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
per_page: 100,
});

const requireVerification = process.env.REQUIRE_VERIFICATION_SECTION === 'true';
const result = validatePrContract({
title: pr.title || '',
body: pr.body || '',
branch: pr.head.ref || '',
files: filesPaged.map((file) => file.filename),
}, {
branchPattern: process.env.BRANCH_PATTERN,
docOnlyExtensions: process.env.DOC_EXT_LIST,
docOnlyPathPrefixes: process.env.DOC_PREFIXES,
requireIssueLink: process.env.REQUIRE_LINKED_ISSUE === 'true',
requiredHeadings: requireVerification ? undefined : [],
requireSummary: requireVerification,
requireDocsChangelog: requireVerification,
requireSubstantiveVerificationNotes: requireVerification,
rejectGenericVerificationNotes: requireVerification,
requireCheckedVerification: process.env.REQUIRE_CHECKED_BOX === 'true',
requireEvidenceBlocks: process.env.REQUIRE_CHECKED_BOX === 'true',
});

const report = formatPrContractResult(result);
await core.summary
.addHeading(result.ok ? 'PR contract passed' : 'PR contract failed', 3)
.addCodeBlock(report, 'text')
.write();

if (!result.ok) {
const marker = '<!-- archon-pr-contract -->';
const repairBody = [
marker,
'',
report,
'',
'Run locally:',
'',
'```bash',
`npm run pr:contract -- --repo ${context.repo.owner}/${context.repo.repo} --pr ${pr.number}`,
'```',
].join('\n');

try {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
event: 'COMMENT',
body: repairBody,
});
} catch (err) {
core.warning(`Could not post PR contract repair comment: ${err && err.message ? err.message : err}`);
}
}

if (failures.length) core.setFailed(failures.join('\n'));
core.setFailed(report);
}

workflow-validation:
name: workflow and hook validation
needs: detect
if: inputs.run-workflow-validation && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-workflow-validation == 'true'
needs:
- detect
- pr-contract
if: always() && inputs.run-workflow-validation && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-workflow-validation == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success')
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -372,8 +447,10 @@ jobs:

policy-validation:
name: policy validation
needs: detect
if: inputs.run-policy-validation && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-policy-validation == 'true'
needs:
- detect
- pr-contract
if: always() && inputs.run-policy-validation && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-policy-validation == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success')
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -390,8 +467,10 @@ jobs:

dependency-review:
name: dependency review
needs: detect
if: inputs.run-dependency-review && github.event_name == 'pull_request' && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-dependency-review == 'true'
needs:
- detect
- pr-contract
if: always() && inputs.run-dependency-review && github.event_name == 'pull_request' && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-dependency-review == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success')
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -407,8 +486,10 @@ jobs:

node-ci:
name: node ci
needs: detect
if: needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-node-ci == 'true'
needs:
- detect
- pr-contract
if: always() && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-node-ci == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success')
uses: ArchonVII/github-workflows/.github/workflows/node-ci.yml@v1
with:
node-versions: ${{ inputs.node-versions }}
Expand All @@ -422,8 +503,10 @@ jobs:

python-ci:
name: python ci
needs: detect
if: needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-python-ci == 'true'
needs:
- detect
- pr-contract
if: always() && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-python-ci == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success')
uses: ArchonVII/github-workflows/.github/workflows/python-ci.yml@v1
with:
python-versions: ${{ inputs.python-versions }}
Expand All @@ -437,8 +520,10 @@ jobs:

snapshot-validation:
name: snapshot validation
needs: detect
if: needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-snapshot-validation == 'true'
needs:
- detect
- pr-contract
if: always() && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-snapshot-validation == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success')
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
Loading
Loading