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}.`);