diff --git a/.gitattributes b/.gitattributes index b8beb0f1f..55f14568b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,6 +12,7 @@ /.github/workflows/codecov.yml linguist-generated /.github/workflows/integ.yml linguist-generated /.github/workflows/issue-label-assign.yml linguist-generated +/.github/workflows/large-pr-checker.yml linguist-generated /.github/workflows/pr-labeler.yml linguist-generated /.github/workflows/pull-request-lint.yml linguist-generated /.github/workflows/release.yml linguist-generated diff --git a/.github/workflows/large-pr-checker.yml b/.github/workflows/large-pr-checker.yml new file mode 100644 index 000000000..fe3462172 --- /dev/null +++ b/.github/workflows/large-pr-checker.yml @@ -0,0 +1,44 @@ +# ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". + +name: large-pr-checker +on: + pull_request: + branches: + - main + types: + - labeled + - edited + - opened + - reopened + - unlabeled +jobs: + check: + name: Check PR size + runs-on: ubuntu-latest + permissions: + pull-requests: write + if: ${{ !contains(github.event.pull_request.labels.*.name, 'pr/exempt-size-check') }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - id: fetch_target_branch + run: git fetch origin main + - id: get_total_lines_changed + run: |- + size=$(git diff --shortstat origin/main ':(exclude)*.md' ':(exclude)*.test.ts' ':(exclude)*.yml' \ + | awk '{ print $4+$6 }' \ + | awk -F- '{print $NF}' \ + | bc) + + echo "Total lines changed: $size" + echo "total_lines_changed=$size" >> $GITHUB_OUTPUT + - id: comment_pr + if: ${{ fromJSON(steps.get_total_lines_changed.outputs.total_lines_changed) > fromJSON(1000) }} + uses: thollander/actions-comment-pull-request@v2 + with: + comment_tag: pr_size + mode: recreate + message: Total lines changed ${{ steps.get_total_lines_changed.outputs.total_lines_changed }} is greater than 1000. Please consider breaking this PR down. + - id: fail + if: ${{ fromJSON(steps.get_total_lines_changed.outputs.total_lines_changed) > fromJSON(1000) }} + run: exit 1 diff --git a/.gitignore b/.gitignore index 9892d6ebb..9a5c7ad8a 100644 --- a/.gitignore +++ b/.gitignore @@ -51,5 +51,6 @@ jspm_packages/ !/.github/workflows/codecov.yml !/.github/workflows/issue-label-assign.yml !/.github/workflows/pr-labeler.yml +!/.github/workflows/large-pr-checker.yml !/.projenrc.ts !/.github/workflows/release.yml diff --git a/.projen/files.json b/.projen/files.json index 12528934f..9b83c55db 100644 --- a/.projen/files.json +++ b/.projen/files.json @@ -10,6 +10,7 @@ ".github/workflows/codecov.yml", ".github/workflows/integ.yml", ".github/workflows/issue-label-assign.yml", + ".github/workflows/large-pr-checker.yml", ".github/workflows/pr-labeler.yml", ".github/workflows/pull-request-lint.yml", ".github/workflows/release.yml", diff --git a/.projenrc.ts b/.projenrc.ts index 67aa2692f..42f5a2090 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -9,6 +9,7 @@ import { CodeCovWorkflow } from './projenrc/codecov'; import { ESLINT_RULES } from './projenrc/eslint'; import { IssueLabeler } from './projenrc/issue-labeler'; import { JsiiBuild } from './projenrc/jsii'; +import { LargePrChecker } from './projenrc/large-pr-checker'; import { PrLabeler } from './projenrc/pr-labeler'; import { RecordPublishingTimestamp } from './projenrc/record-publishing-timestamp'; import { S3DocsPublishing } from './projenrc/s3-docs-publishing'; @@ -1514,4 +1515,8 @@ new CodeCovWorkflow(repo, { new IssueLabeler(repo); new PrLabeler(repo); +new LargePrChecker(repo, { + excludeFiles: ['*.md', '*.test.ts', '*.yml'], +}); + repo.synth(); diff --git a/projenrc/large-pr-checker.ts b/projenrc/large-pr-checker.ts new file mode 100644 index 000000000..af4fac4a6 --- /dev/null +++ b/projenrc/large-pr-checker.ts @@ -0,0 +1,85 @@ +import { github, Component } from 'projen'; +import { JobPermission } from 'projen/lib/github/workflows-model'; +import type { TypeScriptProject } from 'projen/lib/typescript'; + +export interface LargePrCheckerProps { + /** + * The number of lines changed in the PR that will trigger a comment and a failure. + * + * @default 1000 + */ + readonly maxLinesChanged?: number; + + /** + * A list of files to exclude from the line count. + * + * @default - none + */ + readonly excludeFiles?: string[]; +} + +export class LargePrChecker extends Component { + private readonly workflow: github.GithubWorkflow; + + constructor(repo: TypeScriptProject, props: LargePrCheckerProps = {}) { + super(repo); + + if (!repo.github) { + throw new Error('Given repository does not have a GitHub component'); + } + + const maxLinesChanged = props.maxLinesChanged ?? 1000; + const excludeFiles = (props.excludeFiles ?? []) + .map((pattern) => `':(exclude)${pattern}'`) + .join(' '); + + this.workflow = repo.github.addWorkflow('large-pr-checker'); + this.workflow.on({ + pullRequest: { + branches: ['main'], + types: ['labeled', 'edited', 'opened', 'reopened', 'unlabeled'], + }, + }); + + this.workflow.addJob('check', { + name: 'Check PR size', + if: '${{ !contains(github.event.pull_request.labels.*.name, \'pr/exempt-size-check\') }}', + runsOn: ['ubuntu-latest'], + permissions: { + pullRequests: JobPermission.WRITE, + }, + steps: [ + github.WorkflowSteps.checkout(), + { + id: 'fetch_target_branch', + run: 'git fetch origin main', + }, + { + id: 'get_total_lines_changed', + run: `size=$(git diff --shortstat origin/main ${excludeFiles} \\ + | awk '{ print $4+$6 }' \\ + | awk -F- '{print $NF}' \\ + | bc) + + echo "Total lines changed: $size" + echo "total_lines_changed=$size" >> $GITHUB_OUTPUT`, + }, + { + id: 'comment_pr', + if: `$\{{ fromJSON(steps.get_total_lines_changed.outputs.total_lines_changed) > fromJSON(${maxLinesChanged}) }}`, + uses: 'thollander/actions-comment-pull-request@v2', + with: { + comment_tag: 'pr_size', + mode: 'recreate', + message: `Total lines changed $\{{ steps.get_total_lines_changed.outputs.total_lines_changed }} is greater than ${maxLinesChanged}. Please consider breaking this PR down.`, + }, + }, + { + id: 'fail', + if: `$\{{ fromJSON(steps.get_total_lines_changed.outputs.total_lines_changed) > fromJSON(${maxLinesChanged}) }}`, + run: 'exit 1', + }, + ], + }); + } +}