b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
154 lines
6.5 KiB
YAML
154 lines
6.5 KiB
YAML
name: PR Compliance
|
|
|
|
on:
|
|
pull_request_target:
|
|
branches: [main]
|
|
types: [opened, edited, synchronize]
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
|
|
jobs:
|
|
check-pr:
|
|
name: Validate PR
|
|
# Filter on the PR author, not github.actor: actor becomes a maintainer on
|
|
# synchronize/edited events triggered by pushes or merges into a bot branch,
|
|
# which lets bot PRs slip past an actor-based check.
|
|
if: >-
|
|
github.event.pull_request.user.login != 'dependabot[bot]'
|
|
&& github.event.pull_request.user.login != 'renovate[bot]'
|
|
&& github.event.pull_request.user.login != 'emdashbot[bot]'
|
|
&& github.event.pull_request.user.login != 'ask-bonk[bot]'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Check PR template
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
script: |
|
|
const body = context.payload.pull_request.body || '';
|
|
const pr = context.payload.pull_request;
|
|
|
|
// Maintainers (repo owners, org members, and collaborators) are trusted
|
|
// to land features without a pre-approved Discussion link.
|
|
const association = pr.author_association || '';
|
|
const isMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(association);
|
|
|
|
const errors = [];
|
|
|
|
// Check if the PR body looks like it used the template at all
|
|
const hasTemplate = body.includes('## What does this PR do?') || body.includes('## Type of change');
|
|
|
|
if (!hasTemplate) {
|
|
errors.push('This PR does not use the required PR template. Please edit the description to use the [PR template](https://github.com/emdash-cms/emdash/blob/main/.github/PULL_REQUEST_TEMPLATE.md). Copy it into your PR description and fill out all sections.');
|
|
} else {
|
|
// Must have a description (not just the raw template placeholder)
|
|
const descriptionSection = body.match(/## What does this PR do\?\s*\n([\s\S]*?)(?=\n## )/);
|
|
const description = descriptionSection?.[1]?.replace(/<!--[\s\S]*?-->/g, '').trim() || '';
|
|
if (!description || description === 'Closes #') {
|
|
errors.push('Fill out the "What does this PR do?" section with a description of your change.');
|
|
}
|
|
|
|
// Must check at least one type
|
|
const typeChecks = [
|
|
/- \[x\] Bug fix/i,
|
|
/- \[x\] Feature/i,
|
|
/- \[x\] Refactor/i,
|
|
/- \[x\] Translation/i,
|
|
/- \[x\] Documentation/i,
|
|
/- \[x\] Performance/i,
|
|
/- \[x\] Tests/i,
|
|
/- \[x\] Chore/i,
|
|
];
|
|
const hasType = typeChecks.some(re => re.test(body));
|
|
if (!hasType) {
|
|
errors.push('Check at least one "Type of change" checkbox.');
|
|
}
|
|
|
|
// If Feature is checked, require a discussion link.
|
|
// Maintainers are exempt: they can approve their own features.
|
|
const isFeature = /- \[x\] Feature/i.test(body);
|
|
if (isFeature && !isMaintainer) {
|
|
const hasDiscussionLink = /github\.com\/emdash-cms\/emdash\/discussions\/\d+/.test(body);
|
|
if (!hasDiscussionLink) {
|
|
errors.push('Feature PRs require a link to an approved Discussion (https://github.com/emdash-cms/emdash/discussions/categories/ideas). Open a Discussion first, get approval, then link it in the PR.');
|
|
}
|
|
}
|
|
|
|
// Must check the "I have read CONTRIBUTING.md" box. Be lenient about
|
|
// formatting: accept the box whether or not CONTRIBUTING.md is still a
|
|
// markdown link (authors often strip the link or tweak the wording).
|
|
const hasReadContributing = /- \[x\][^\n]*\bI have read\b[^\n]*CONTRIBUTING/i.test(body);
|
|
if (!hasReadContributing) {
|
|
errors.push('Check the "I have read CONTRIBUTING.md" checkbox.');
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
const message = [
|
|
'## PR template validation failed',
|
|
'',
|
|
'Please fix the following issues by editing your PR description:',
|
|
'',
|
|
...errors.map(e => `- ${e}`),
|
|
'',
|
|
'See [CONTRIBUTING.md](https://github.com/emdash-cms/emdash/blob/main/CONTRIBUTING.md) for the full contribution policy.',
|
|
].join('\n');
|
|
|
|
// Leave a comment so the author sees the errors directly
|
|
// Find and update existing bot comment, or create a new one
|
|
const marker = '<!-- pr-compliance-check -->';
|
|
const commentBody = `${marker}\n${message}`;
|
|
|
|
const { data: comments } = await github.rest.issues.listComments({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: pr.number,
|
|
});
|
|
|
|
const existing = comments.find(c =>
|
|
c.user?.login === 'github-actions[bot]' &&
|
|
c.body?.includes(marker)
|
|
);
|
|
|
|
if (existing) {
|
|
await github.rest.issues.updateComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: existing.id,
|
|
body: commentBody,
|
|
});
|
|
} else {
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: pr.number,
|
|
body: commentBody,
|
|
});
|
|
}
|
|
|
|
core.setFailed(message);
|
|
} else {
|
|
// If passing now, remove any previous failure comment
|
|
const marker = '<!-- pr-compliance-check -->';
|
|
const { data: comments } = await github.rest.issues.listComments({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: pr.number,
|
|
});
|
|
|
|
const existing = comments.find(c =>
|
|
c.user?.login === 'github-actions[bot]' &&
|
|
c.body?.includes(marker)
|
|
);
|
|
|
|
if (existing) {
|
|
await github.rest.issues.deleteComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: existing.id,
|
|
});
|
|
}
|
|
}
|