Skip to content

feat(docsite): add 'Use this theme' install block on /themes #7816

feat(docsite): add 'Use this theme' install block on /themes

feat(docsite): add 'Use this theme' install block on /themes #7816

# Copyright (c) Meta Platforms, Inc. and affiliates.
# Code owner gate: blocks merge unless a code owner has approved
# Code owners themselves can merge freely (auto-pass)
#
# Design owners (.github/DESIGNOWNERS) can merge sandbox, storybook,
# and CLI template changes without code owner approval. Everyone else
# needs it.
#
# Uses the Checks API to write a single "Code Owner Gate" status per
# commit SHA, so re-runs from pull_request_review events UPDATE the
# existing check instead of creating a competing one.
name: Code Owner Gate
on:
pull_request:
branches: ["main"]
types: [opened, synchronize, reopened]
pull_request_review:
types: [submitted]
permissions: {}
jobs:
codeowner-gate:
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: read
contents: read
steps:
- name: Checkout CODEOWNERS and DESIGNOWNERS
uses: actions/checkout@v6
with:
sparse-checkout: |
.github/CODEOWNERS
.github/DESIGNOWNERS
sparse-checkout-cone-mode: false
- name: Check code owner status
uses: actions/github-script@v9
with:
script: |
const fs = require('fs');
// --- Configuration ---
// Read code owners from .github/CODEOWNERS
let CODE_OWNERS = [];
try {
const codeownersContent = fs.readFileSync('.github/CODEOWNERS', 'utf8');
CODE_OWNERS = [...new Set(
codeownersContent
.split('\n')
.map(line => line.replace(/#.*$/, '').trim()) // strip comments
.filter(line => line.length > 0)
.flatMap(line => {
// Each line is: <pattern> <owners...>
// Owners start with @, strip the @ prefix
const parts = line.split(/\s+/);
return parts.slice(1).map(owner => owner.replace(/^@/, ''));
})
)];
} catch (e) {
console.log('No CODEOWNERS file found, falling back to empty list');
}
console.log(`Code owners: ${CODE_OWNERS.join(', ') || '(none)'}`);
// Read design owners from file
let DESIGNOWNERS = [];
try {
const content = fs.readFileSync('.github/DESIGNOWNERS', 'utf8');
DESIGNOWNERS = content
.split('\n')
.map(line => line.replace(/#.*$/, '').trim()) // strip comments
.filter(line => line.length > 0);
} catch (e) {
console.log('No DESIGNOWNERS file found, skipping design owner check');
}
console.log(`Design owners: ${DESIGNOWNERS.join(', ') || '(none)'}`);
// Paths that design owners can merge without code owner review
const DESIGN_PATHS = [
'apps/sandbox/',
'apps/storybook/',
'packages/cli/templates/',
];
// File patterns that design owners can also merge (glob-style)
// .doc.mjs files are doc metadata, not component logic
const DESIGN_FILE_PATTERNS = [
/\.doc\.mjs$/,
];
// --- Resolve PR context ---
const pr = context.payload.pull_request;
const prNumber = pr.number;
const prAuthor = pr.user.login;
const headSha = pr.head.sha;
console.log(`PR #${prNumber} by @${prAuthor} (${headSha.slice(0, 7)})`);
// Fetch changed files (paginated — PRs with 100+ files)
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
per_page: 100,
});
// Collect all affected paths — includes previous_filename
// for renames so a rename FROM core TO sandbox is caught.
const allPaths = files.flatMap(f =>
f.previous_filename ? [f.filename, f.previous_filename] : [f.filename]
);
const changedFiles = files.map(f => f.filename);
const isDesignOwned = (p) =>
DESIGN_PATHS.some(prefix => p.startsWith(prefix)) ||
DESIGN_FILE_PATTERNS.some(re => re.test(p));
const onlyDesignPaths = allPaths.length > 0 && allPaths.every(isDesignOwned);
// --- Determine result ---
let conclusion = 'failure';
let summary = '';
if (CODE_OWNERS.includes(prAuthor)) {
conclusion = 'success';
summary = `@${prAuthor} is a code owner — approved automatically.`;
} else if (onlyDesignPaths && DESIGNOWNERS.includes(prAuthor)) {
conclusion = 'success';
summary = `@${prAuthor} is a design owner and only design-owned paths changed — approved automatically.\nFiles: ${changedFiles.join(', ')}`;
} else {
// Check for code owner approval
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
const latestReviews = new Map();
for (const review of reviews) {
latestReviews.set(review.user.login, review.state);
}
const approvedBy = [...latestReviews.entries()]
.filter(([user, state]) => state === 'APPROVED' && CODE_OWNERS.includes(user))
.map(([user]) => user);
if (approvedBy.length > 0) {
conclusion = 'success';
summary = `Approved by code owner(s): ${approvedBy.map(u => '@' + u).join(', ')}`;
} else {
// Tailor the message based on context
if (onlyDesignPaths && !DESIGNOWNERS.includes(prAuthor)) {
summary = `@${prAuthor} is not a design owner. These paths require code owner approval (${CODE_OWNERS.map(u => '@' + u).join(', ')}) or being listed in .github/DESIGNOWNERS.`;
} else {
summary = `Requires approval from a code owner (${CODE_OWNERS.map(u => '@' + u).join(', ')}).`;
}
}
}
console.log(`${conclusion === 'success' ? '✅' : '❌'} ${summary}`);
// --- Write check run (upsert) ---
const checkName = 'Code Owner Gate';
const { data: existing } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: headSha,
check_name: checkName,
});
const params = {
owner: context.repo.owner,
repo: context.repo.repo,
name: checkName,
head_sha: headSha,
status: 'completed',
conclusion,
output: {
title: conclusion === 'success' ? 'Approved' : 'Waiting for code owner approval',
summary,
},
};
if (existing.total_count > 0) {
await github.rest.checks.update({
...params,
check_run_id: existing.check_runs[0].id,
});
console.log(`Updated check run ${existing.check_runs[0].id}`);
} else {
const { data: created } = await github.rest.checks.create(params);
console.log(`Created check run ${created.id}`);
}