mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 17:36:53 +01:00
Guard the custom layer and flag stray top-level entries (#58)
The /custom/ layer is a template: in upstream microsoft/BCQuality it stays empty by default and is meant to be populated only inside a fork or consumer clone. PR #55 both targeted /custom/ and leaked a new top-level folder. - skills/write.md: add a fork-precondition guard so authors (human or agent) confirm they are not in microsoft/BCQuality before scaffolding /custom/ content. - Guard custom layer workflow: auto-closes upstream PRs that add/modify /custom/ content beyond the template files, with a friendly redirect-to-fork comment. - Flag new top-level entries workflow: posts an advisory (non-blocking) comment when a PR introduces an unexpected top-level folder or file for maintainer review. Both workflows run only on microsoft/BCQuality (never on forks) and read the PR file list via the API without checking out or executing PR code. Co-authored-by: Jeremy Vyska <jeremy@sparebrained.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f2c2b510d8
commit
d3eb7d6a98
5 changed files with 248 additions and 0 deletions
88
.github/workflows/guard-custom-layer.yml
vendored
Normal file
88
.github/workflows/guard-custom-layer.yml
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
name: Guard custom layer
|
||||
|
||||
# The /custom/ layer is a template: in upstream microsoft/BCQuality it stays
|
||||
# empty by default (README.md + .gitkeep placeholders only). Custom knowledge
|
||||
# and skills are partner/customer-specific and belong in a fork, never upstream.
|
||||
#
|
||||
# This workflow auto-closes any PR that adds or changes content under /custom/
|
||||
# (anything beyond the allowed template files). It runs only on the upstream
|
||||
# repo, so forks that legitimately populate /custom/ are unaffected.
|
||||
#
|
||||
# pull_request_target is required so the workflow runs with a token that can
|
||||
# comment on and close the PR (including PRs opened from forks). It only reads
|
||||
# the PR's file LIST via the API and never checks out or executes PR code, so
|
||||
# the elevated token is not exposed to untrusted content.
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
guard:
|
||||
# Never run on forks — a fork's /custom/ content is exactly what's supposed
|
||||
# to live there.
|
||||
if: github.repository == 'microsoft/BCQuality'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/custom-layer-autoclose.md
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Close PR if it touches the custom layer
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
// Files under custom/ that ARE allowed to change (the template seed).
|
||||
const ALLOWED = new Set([
|
||||
'custom/README.md',
|
||||
]);
|
||||
// Any .gitkeep under custom/ is also allowed.
|
||||
const isAllowed = (p) =>
|
||||
ALLOWED.has(p) || /^custom\/.*\.gitkeep$/.test(p) || p === 'custom/.gitkeep';
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner, repo, pull_number: prNumber, per_page: 100,
|
||||
});
|
||||
|
||||
// Offending = added/modified/renamed/copied/changed paths under custom/
|
||||
// that are not template files. (We ignore pure deletions.)
|
||||
const offending = files
|
||||
.filter((f) => f.status !== 'removed')
|
||||
.map((f) => f.filename)
|
||||
.filter((p) => p.startsWith('custom/') && !isAllowed(p));
|
||||
|
||||
if (offending.length === 0) {
|
||||
core.info('No disallowed /custom/ changes found. Nothing to do.');
|
||||
return;
|
||||
}
|
||||
|
||||
core.warning(`PR #${prNumber} touches the custom layer: ${offending.join(', ')}`);
|
||||
|
||||
const fileList = offending.map((p) => `- \`${p}\``).join('\n');
|
||||
let body = fs.readFileSync('.github/custom-layer-autoclose.md', 'utf8');
|
||||
body = body
|
||||
.replace(/{{AUTHOR}}/g, context.payload.pull_request.user.login)
|
||||
.replace(/{{FILES}}/g, fileList);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: prNumber, body,
|
||||
});
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner, repo, pull_number: prNumber, state: 'closed',
|
||||
});
|
||||
|
||||
core.info(`Closed PR #${prNumber}.`);
|
||||
Loading…
Add table
Add a link
Reference in a new issue