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
47 changes: 47 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Check

on:
workflow_call:
inputs:
command:
type: string
default: vp run check
install:
type: boolean
default: true
node-version-file:
type: string
default: .node-version

permissions:
contents: read

jobs:
check:
name: Check
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
timeout-minutes: 15
env:
pnpm_config_ignore_scripts: "true"
pnpm_config_ignore_pnpmfile: "true"
steps:
- name: Checkout PR merge result
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Vite+ and install dependencies
if: ${{ inputs.install }}
uses: voidzero-dev/setup-vp@68f39c78aaf39be468cabd2afb09c162574c84af # v1.21.2
with:
version: "1.1.0"
node-version-file: ${{ inputs.node-version-file }}
cache: true
run-install: |
args:
- --frozen-lockfile
- --ignore-scripts
- name: Check
env:
CHECK_COMMAND: ${{ inputs.command }}
run: bash -e -o pipefail -c "$CHECK_COMMAND"
21 changes: 21 additions & 0 deletions .github/workflows/ci-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Check

on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]

permissions:
contents: read

concurrency:
group: check-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
check:
name: Check
uses: ./.github/workflows/check.yml
with:
install: false
command: node --test tests/ready.test.mjs
19 changes: 19 additions & 0 deletions .github/workflows/ci-ready.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Ready

on:
pull_request_target:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, labeled, unlabeled]
workflow_run:
workflows: [Check, Review event]
types: [completed]

permissions:
contents: write
pull-requests: write
statuses: write
checks: read

jobs:
ready:
uses: ./.github/workflows/ready.yml
125 changes: 125 additions & 0 deletions .github/workflows/ready.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: Ready

on:
workflow_call:
inputs:
netlify-checks:
type: string
default: "[]"
check-name:
type: string
default: Check / Check

permissions:
contents: write
pull-requests: write
statuses: write
checks: read

concurrency:
group: ready-policy-${{ github.repository }}
cancel-in-progress: false

jobs:
ready:
name: Ready
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
# Only GitHub metadata is read. Never check out or execute PR code here.
- name: Require selected review and enable auto-merge
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
NETLIFY_CHECKS: ${{ inputs.netlify-checks }}
CHECK_NAME: ${{ inputs.check-name }}
with:
script: |
const {owner, repo} = context.repo;
const repository = (await github.rest.repos.get({owner, repo})).data;
const candidates = await github.paginate(github.rest.pulls.list, {owner, repo, state: 'open',
base: repository.default_branch, per_page: 100});
async function processPull(number) {
const getPull = async () => (await github.rest.pulls.get({owner, repo, pull_number: number})).data;
const pull = await getPull();
if (pull.state !== 'open' || pull.base.ref !== repository.default_branch) return;
const sha = pull.head.sha;
const selected = pull.labels.some(label => label.name === 'review-ready');
async function hasApproval() {
if (!selected) return true;
const reviews = await github.paginate(github.rest.pulls.listReviews, {owner, repo, pull_number: number, per_page: 100});
const decisions = reviews.filter(review => review.user?.type === 'Bot'
&& review.user.login === 'coderabbitai[bot]'
&& ['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(review.state));
const latest = decisions.sort((a, b) => b.id - a.id)[0];
return latest?.state === 'APPROVED' && latest.commit_id === sha;
}
async function hasTrustedNotifier() {
if (!selected) return true;
try {
const path = '.github/workflows/review-event.yml';
const trusted = (await github.rest.repos.getContent({owner, repo, path,
ref: repository.default_branch})).data;
const merged = (await github.rest.repos.getContent({owner, repo, path,
ref: `refs/pull/${number}/merge`})).data;
return trusted.type === 'file' && merged.type === 'file' && trusted.sha === merged.sha;
} catch (error) {
if (error.status === 404) return false;
throw error;
}
}
const approved = await hasTrustedNotifier() && await hasApproval();
const current = await getPull();
if (current.base.ref !== pull.base.ref || current.head.sha !== sha || current.draft !== pull.draft
|| current.labels.some(label => label.name === 'review-ready') !== selected) return;
const reviewPassed = !pull.draft && approved && await hasTrustedNotifier() && await hasApproval();
await github.rest.repos.createCommitStatus({owner, repo, sha, context: 'Review',
state: reviewPassed ? 'success' : 'pending',
description: pull.draft ? 'Draft: publication is not authorized.'
: approved ? (selected ? 'CodeRabbit approved this revision.' : 'No CodeRabbit review selected.')
: 'Waiting for CodeRabbit approval of this revision.',
target_url: pull.html_url});
if (!reviewPassed) return;
// A skipped draft Check must never authorize automatic publication.
const checks = await github.paginate(github.rest.checks.listForRef, {owner, repo, ref: sha, per_page: 100});
const check = checks.filter(run => run.name === process.env.CHECK_NAME && run.app?.id === 15368)
.sort((a, b) => b.id - a.id)[0];
if (check?.conclusion !== 'success') return core.info('Waiting for a successful Ready Check.');
// Rollout must install central requirements before enabling auto-merge.
const {data: rules} = await github.request('GET /repos/{owner}/{repo}/rules/branches/{branch}',
{owner, repo, branch: pull.base.ref});
const required = rules.filter(rule => rule.type === 'required_status_checks')
.flatMap(rule => rule.parameters.required_status_checks.map(status => status.context));
const expected = [process.env.CHECK_NAME, 'Review', ...JSON.parse(process.env.NETLIFY_CHECKS)];
if (!expected.every(name => required.includes(name))) {
return core.info('The complete central Check, Review and Netlify requirements are not active yet.');
}
if (!repository.allow_auto_merge) return core.info('Repository auto-merge is not enabled yet.');
const final = await getPull();
if (final.state !== 'open' || final.draft || final.base.ref !== pull.base.ref || final.head.sha !== sha
|| final.labels.some(label => label.name === 'review-ready') !== selected) return;
if (selected && (!await hasTrustedNotifier() || !await hasApproval())) {
await github.rest.repos.createCommitStatus({owner, repo, sha, context: 'Review',
state: 'pending', description: 'CodeRabbit approval changed; waiting for review.', target_url: pull.html_url});
return;
}
if (final.mergeable_state === 'clean') {
await github.rest.pulls.merge({owner, repo, pull_number: number, sha, merge_method: 'squash'});
return;
}
if (final.auto_merge) return core.info('Auto-merge is already enabled.');
await github.graphql(`mutation($id: ID!, $sha: GitObjectID!) {
enablePullRequestAutoMerge(input: {pullRequestId: $id, expectedHeadOid: $sha, mergeMethod: SQUASH}) {
pullRequest { number }
}
}`, {id: final.node_id, sha});
}
let failed = false;
for (const pull of candidates) {
try {
await processPull(pull.number);
} catch (error) {
failed = true;
core.error(`PR #${pull.number}: ${error.message}`);
}
}
if (failed) core.setFailed('Some PRs could not be reconciled; see their errors above.');
14 changes: 14 additions & 0 deletions .github/workflows/review-event.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Review event

on:
pull_request_review:
types: [submitted, dismissed]

permissions: {}

jobs:
notify:
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- run: echo "Review changed; the trusted Ready workflow will inspect GitHub metadata."
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# fulldotdev/.github

Organization-wide GitHub defaults. CI workflows belong to each repository; workflows here are not inherited automatically.
Organization-wide GitHub defaults and reusable workflows. Repository callers select their own check command; workflows here are not inherited automatically.

New projects take their CI from [astro-template](https://github.com/fulldotdev/astro-template) or [vite-template](https://github.com/fulldotdev/vite-template). [fulldev](https://github.com/fulldotdev/fulldev) owns the monorepo workflows.
New projects take their callers from [astro-template](https://github.com/fulldotdev/astro-template) or [vite-template](https://github.com/fulldotdev/vite-template).

- **Finalize (Prepare)** leaves a complete draft PR. Previews build; full checks wait for Ready.
- **Dispatch (Release)** validates the affected behavior and marks the PR Ready, authorizing automatic merge once the required checks and previews pass. Choose `review-ready` before marking Ready when CodeRabbit approval is needed.
- **Settle (Close)** follows production verification and preserves the result before closing the task.

Organization rules require `Check / Check` and `Review`, without requiring synchronization with main. Netlify requirements are managed centrally for the repositories with those sites. Production keeps its scoped check and build. A skipped or outdated CodeRabbit review cannot satisfy a selected review.
Loading
Loading