mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 09:26:52 +01:00
Guard the custom layer and flag stray top-level entries
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: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f2c2b510d8
commit
7bc9bb7fc2
5 changed files with 248 additions and 0 deletions
31
.github/custom-layer-autoclose.md
vendored
Normal file
31
.github/custom-layer-autoclose.md
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
Hey @{{AUTHOR}} 👋
|
||||||
|
|
||||||
|
First off — thank you for jumping in and experimenting! It's awesome to see people pushing on the framework. 🎉
|
||||||
|
|
||||||
|
That said, let me gently redirect you, because I think there's a small but important misunderstanding about how the `custom` layer is meant to work:
|
||||||
|
|
||||||
|
The `custom` layer in *this* repo isn't a destination for PRs — it's the designated sandbox inside **your own fork**. Think of it as the "your timeline" branch of the multiverse 🌌: this repo is canon, your fork is where you get to remix the lore without needing anyone's approval. That's the whole point of the layer existing — so you *don't* have to upstream your team-specific or experimental work.
|
||||||
|
|
||||||
|
The intended workflow is:
|
||||||
|
|
||||||
|
1. 🍴 **Fork** BCQuality to your own GitHub account
|
||||||
|
2. Clone *your fork* locally
|
||||||
|
3. Drop your custom agents and knowledge into the `custom` layer **there**
|
||||||
|
4. Commit and push to your fork — no PR back to upstream needed for custom stuff
|
||||||
|
|
||||||
|
That way you get full control, your changes survive upstream updates cleanly, and you can pull in new core releases from this repo whenever you want. ✨
|
||||||
|
|
||||||
|
**Now — here's the fun part:** if while building out your fork you discover knowledge, patterns, or agents that you think would genuinely benefit *everyone* using BCQuality (not just your team), that's exactly what the `/community` layer is for! 🌟 PRs to `/community` here in the upstream repo are absolutely welcome and encouraged — it's how the collective hive mind 🧠 levels up. So please: tinker in your fork, and when you strike gold that's worth sharing, send it our way via `/community`.
|
||||||
|
|
||||||
|
Going to close this PR for now (since it's targeting `custom` rather than `/community`), but please don't read it as a "no" — it's a "yes, but let's route it correctly." 🙏 Happy to help if you hit any snags spinning up your fork, and genuinely looking forward to seeing what you contribute to `/community` down the line.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Files in this PR that triggered the auto-close</summary>
|
||||||
|
|
||||||
|
{{FILES}}
|
||||||
|
</details>
|
||||||
|
|
||||||
|
May your merges be conflict-free. 🚀
|
||||||
|
|
||||||
|
---
|
||||||
|
<sub>🤖 This PR was closed automatically by the `Guard custom layer` workflow because it adds or changes content under `/custom/`. If you were only updating the template (`custom/README.md` or a `.gitkeep`), a maintainer can re-open it. If you think this was closed in error, just comment here.</sub>
|
||||||
10
.github/new-top-level-flag.md
vendored
Normal file
10
.github/new-top-level-flag.md
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!-- guard:new-top-level -->
|
||||||
|
👋 Heads up @{{AUTHOR}} — and cc maintainers — this PR introduces **new top-level entries** that aren't part of BCQuality's known repository structure:
|
||||||
|
|
||||||
|
{{ENTRIES}}
|
||||||
|
|
||||||
|
This isn't a block — just a flag. 🚩 New top-level folders and files are *usually* unintended (a stray export, a tool's scratch dir, or content that meant to land inside an existing layer like `/community/knowledge/`). BCQuality keeps a deliberately small root: `.github/`, `community/`, `custom/`, `microsoft/`, `skills/`, and `tools/`, plus a handful of root docs.
|
||||||
|
|
||||||
|
**If this was intentional** and the new entry genuinely belongs at the repo root, a maintainer can review and merge as normal — no action needed beyond a quick sanity check. **If it wasn't**, please move the content into the right existing layer (or drop it) and push an update. 🙏
|
||||||
|
|
||||||
|
A maintainer will take a look before merging.
|
||||||
108
.github/workflows/flag-new-top-level.yml
vendored
Normal file
108
.github/workflows/flag-new-top-level.yml
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
name: Flag new top-level entries
|
||||||
|
|
||||||
|
# BCQuality keeps a deliberately small repository root. New top-level folders
|
||||||
|
# or files are almost always unintended — a stray export, a tool's scratch
|
||||||
|
# directory, or content that meant to land inside an existing layer (e.g.
|
||||||
|
# /community/knowledge/). PR #55 leaked exactly this kind of stray folder.
|
||||||
|
#
|
||||||
|
# Unlike the custom-layer guard, this workflow does NOT close the PR. It only
|
||||||
|
# posts a single advisory comment so a maintainer (and the author) can eyeball
|
||||||
|
# the addition. It reads the PR's file LIST via the API and never checks out or
|
||||||
|
# runs PR code.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, reopened, synchronize]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
flag:
|
||||||
|
if: github.repository == 'microsoft/BCQuality'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: |
|
||||||
|
.github/new-top-level-flag.md
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: Flag unexpected new top-level entries
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Known, intended repository root. Anything else added at the root
|
||||||
|
// is flagged for a human to eyeball.
|
||||||
|
const ALLOWED_DIRS = new Set([
|
||||||
|
'.github', 'community', 'custom', 'microsoft', 'skills', 'tools',
|
||||||
|
]);
|
||||||
|
const ALLOWED_FILES = new Set([
|
||||||
|
'.gitignore', 'CODEOWNERS', 'LICENSE', 'README.md',
|
||||||
|
'SECURITY.md', 'agent-consumption.md',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const MARKER = '<!-- guard:new-top-level -->';
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only consider newly-added paths — a new top-level entry can only
|
||||||
|
// appear via an added file.
|
||||||
|
const added = files
|
||||||
|
.filter((f) => f.status === 'added')
|
||||||
|
.map((f) => f.filename);
|
||||||
|
|
||||||
|
const newDirs = new Set();
|
||||||
|
const newFiles = new Set();
|
||||||
|
for (const p of added) {
|
||||||
|
const slash = p.indexOf('/');
|
||||||
|
if (slash === -1) {
|
||||||
|
// Top-level file.
|
||||||
|
if (!ALLOWED_FILES.has(p)) newFiles.add(p);
|
||||||
|
} else {
|
||||||
|
// Top-level directory.
|
||||||
|
const dir = p.slice(0, slash);
|
||||||
|
if (!ALLOWED_DIRS.has(dir)) newDirs.add(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newDirs.size === 0 && newFiles.size === 0) {
|
||||||
|
core.info('No unexpected new top-level entries. Nothing to flag.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idempotency: don't re-flag on every synchronize.
|
||||||
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||||
|
owner, repo, issue_number: prNumber, per_page: 100,
|
||||||
|
});
|
||||||
|
if (comments.some((c) => c.body && c.body.includes(MARKER))) {
|
||||||
|
core.info('Already flagged on this PR. Skipping duplicate comment.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [];
|
||||||
|
for (const d of [...newDirs].sort()) lines.push(`- 📁 \`${d}/\` (new top-level folder)`);
|
||||||
|
for (const f of [...newFiles].sort()) lines.push(`- 📄 \`${f}\` (new top-level file)`);
|
||||||
|
const entries = lines.join('\n');
|
||||||
|
|
||||||
|
core.warning(`Unexpected new top-level entries: ${[...newDirs, ...newFiles].join(', ')}`);
|
||||||
|
|
||||||
|
let body = fs.readFileSync('.github/new-top-level-flag.md', 'utf8');
|
||||||
|
body = body
|
||||||
|
.replace(/{{AUTHOR}}/g, context.payload.pull_request.user.login)
|
||||||
|
.replace(/{{ENTRIES}}/g, entries);
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner, repo, issue_number: prNumber, body,
|
||||||
|
});
|
||||||
|
|
||||||
|
core.info(`Flagged PR #${prNumber}.`);
|
||||||
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}.`);
|
||||||
|
|
@ -65,6 +65,17 @@ Knowledge files do not contain code. Samples live as **sibling files** next to t
|
||||||
- **`/community/knowledge/<domain>/`** — shared community patterns. The default layer for contributions from outside the platform team. Content here can be promoted to `/microsoft/` once it proves itself.
|
- **`/community/knowledge/<domain>/`** — shared community patterns. The default layer for contributions from outside the platform team. Content here can be promoted to `/microsoft/` once it proves itself.
|
||||||
- **`/custom/knowledge/<domain>/`** — partner or customer overrides. Generally does not appear in the BCQuality repository itself; `/custom/` lives in consumer repositories.
|
- **`/custom/knowledge/<domain>/`** — partner or customer overrides. Generally does not appear in the BCQuality repository itself; `/custom/` lives in consumer repositories.
|
||||||
|
|
||||||
|
### Writing to `/custom/` — fork precondition
|
||||||
|
|
||||||
|
The `/custom/` layer is **empty by default** in the upstream `microsoft/BCQuality` repository — it ships as a template (`README.md` plus `.gitkeep` placeholders) and is meant to be populated only inside a **fork or consumer clone** that an organization controls. Custom content is partner- or customer-specific by definition and is never accepted upstream.
|
||||||
|
|
||||||
|
Before authoring or scaffolding any file under `/custom/knowledge/` or `/custom/skills/`, an author — human or agent — MUST confirm the working repository is **not** `microsoft/BCQuality`:
|
||||||
|
|
||||||
|
- Check the `origin` remote: `git remote get-url origin`. If it points at `github.com/microsoft/BCQuality`, stop — you are in the upstream repo, not a fork.
|
||||||
|
- If you are in the upstream repo, do not write the file. Either fork the repository (or clone it into your organization's own repo) and add the custom content there, or — if the guidance is genuinely shareable — author it in `/community/knowledge/` instead.
|
||||||
|
|
||||||
|
A pull request that adds `/custom/` content to `microsoft/BCQuality` will be **automatically closed** by the `Guard custom layer` workflow. Validate the fork precondition first so authoring effort is not wasted on a PR that cannot be merged.
|
||||||
|
|
||||||
## Pre-PR checklist
|
## Pre-PR checklist
|
||||||
|
|
||||||
Before opening a pull request:
|
Before opening a pull request:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue