Skip to content

feat: Pomodoro focus timer with custom durations, auto DND, and stats #272

feat: Pomodoro focus timer with custom durations, auto DND, and stats

feat: Pomodoro focus timer with custom durations, auto DND, and stats #272

Workflow file for this run

name: Fork PR target policy
on:
pull_request_target:
types: [opened, reopened, synchronize, edited, ready_for_review]
permissions: {}
jobs:
validate:
name: Fork PR target check
if: ${{ github.event.pull_request.head.repo.fork == true }}
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- name: Enforce target policy
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
script: |
const base = context.payload.pull_request.base.ref;
const pull_number = context.payload.pull_request.number;
const allowedPatterns = [
/^README\.md$/,
/^CONTRIBUTING\.md$/,
/^SECURITY\.md$/,
/^LICENSE$/,
/^THIRD_PARTY_LICENSES$/,
/^\.gitignore$/,
/^crowdin\.yml$/,
/^\.github\/.+$/
];
const isAllowedPath = (path) =>
allowedPatterns.some((pattern) => pattern.test(path));
const toSafeLiteral = (value) =>
JSON.stringify(String(value))
.replace(/[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, (c) =>
`\\u${c.codePointAt(0).toString(16).padStart(4, '0')}`
)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026')
.replace(/@/g, '\\u0040')
.replace(/#/g, '\\u0023')
.replace(/`/g, '\\u0060');
const describeFile = (file) => {
if (file.previous_filename) {
return `- ${toSafeLiteral(file.previous_filename)} -> ${toSafeLiteral(file.filename)}`;
}
return `- ${toSafeLiteral(file.filename)}`;
};
if (base === 'dev') {
core.info('PR targets dev: allowed.');
return;
}
if (base !== 'main') {
core.setFailed(
`PR targets ${toSafeLiteral(base)}: not allowed. Only "dev" and "main" are permitted.`
);
return;
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number,
per_page: 100
});
// Treat renames conservatively: both old and new paths must be allowed.
const disallowed = files.filter((file) => {
const pathsToCheck = [file.filename, file.previous_filename].filter(Boolean);
return pathsToCheck.some((path) => !isAllowedPath(path));
});
if (disallowed.length > 0) {
core.setFailed([
"PR targets 'main' but includes changes outside the allowed paths.",
"Only allowlisted repository metadata changes may target 'main'.",
'',
'Allowed paths:',
'- "README.md"',
'- "CONTRIBUTING.md"',
'- "SECURITY.md"',
'- "LICENSE"',
'- "THIRD_PARTY_LICENSES"',
'- ".gitignore"',
'- "crowdin.yml"',
'- ".github/**"',
'',
'Disallowed changed paths:',
...disallowed.map(describeFile)
].join('\n'));
return;
}
core.info("PR targets main and only changes allowed paths: allowed.");