diff --git a/README.md b/README.md index 7452c7e..e53d49f 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ your repository. ### Modify the Pull Request Template -By default, this action will append the visualization to the bottom of the PR description. +By default, the action will append the visualization to the bottom of the PR description. If you are using a [pull request template](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository), you can specify the location of the visualization in the template by adding a [HTML comment](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#hiding-content-with-comments) that contains `branch-stack` inside of it: @@ -76,7 +76,7 @@ that contains `branch-stack` inside of it: [ ] Baz ``` -This action will look for this comment and insert the visualization underneath the comment +The action will look for this comment and insert the visualization underneath the comment when it runs. It will also leave behind the comment, so that the next time it runs, it will be able to use it again to update the visualization: @@ -98,17 +98,17 @@ be able to use it again to update the visualization: > [!WARNING] > Be careful not to add content between the comment and the -> visualization, as this action will replace that content each time it +> visualization, as the action will replace that content each time it > updates your PR. Adding content above the tag, or below the list is > safe though! -### Manual Configuration +## Manual Configuration -If you are using Git Town v11 and below, or are setting up this action for a repository -that doesn't have a `.git-branches.toml`, you will need to tell this action what the +If you are using Git Town v11 and below, or are setting up the action for a repository +that doesn't have a `.git-branches.toml`, you will need to tell the action what the main branch and perennial branches are for your repository. -#### Main Branch +### Main Branch The main branch is the default parent branch for new feature branches, and can be specified using the `main-branch` input: @@ -119,10 +119,10 @@ specified using the `main-branch` input: main-branch: 'main' ``` -This action will default to your repository's default branch, which it fetches via +The action will default to your repository's default branch, which it fetches via the GitHub REST API. -#### Perennial Branches +### Perennial Branches Perennial branches are long lived branches and are never shipped. @@ -139,27 +139,70 @@ be done with the `perennial-branches` and `perennial-regex` inputs respectively: perennial-regex: '^release-.*$' ``` -Both inputs can be used at the same time. This action will merge the perennial +Both inputs can be used at the same time. The action will merge the perennial branches given into a single, de-duplicated list. ## Customization ### Skip Single Stacks -If you don't want the stack description to appear on pull requests which are not part of a stack, you can add `skip-single-stacks: true` to the job. +If you don't want the stack visualization to appear on pull requests which are **not** part +of a stack, add `skip-single-stacks: true` to the action's inputs. -This skips all pull requests which point to a main or perennial branch and have no children pull requests pointing to it. +A pull request is considered to be **not** a part of a stack if: +- It has no child pull requests. +- It's parent is the main branch or a perennial branch. ```yaml - uses: git-town/action@v1 with: - perennial-branches: | - dev - staging - prod skip-single-stacks: true ``` +### History Limit + +In order to accurately visualize stacked changes, the action needs to fetch _all_ open +and closed pull requests. This can problematic for larger/older repositories that have +a large number of closed pull requests. + +The action can be configured to fetch a limited number of closed pull requests. This is +customizable with the `history-limit` input: + +```yaml +- uses: git-town/action@v1 + with: + history-limit: '500' # Only fetch the latest 500 closed pull requests +``` + +> [!NOTE] +> This only applies to closed pull requests. Open pull requests will be completely fetched +> regardless of the `history-limit`. + +## Reference + +```yaml +inputs: + github-token: + required: true + default: ${{ github.token }} + main-branch: + required: false + default: '' + perennial-branches: + required: false + default: '' + perennial-regex: + required: false + default: '' + skip-single-stacks: + required: false + default: false + history-limit: + required: false + default: '0' +``` + + ## License The scripts and documentation in this project are released under the [MIT License](LICENSE). diff --git a/action.yml b/action.yml index c5e2f90..2fe5cc0 100644 --- a/action.yml +++ b/action.yml @@ -23,7 +23,7 @@ inputs: default: false history-limit: required: false - default: 0 + default: '0' runs: using: 'node20' diff --git a/dist/index.js b/dist/index.js index eed71ef..d96deba 100644 --- a/dist/index.js +++ b/dist/index.js @@ -47076,12 +47076,26 @@ var inputs = { return core2.getInput("github-token", { required: true, trimWhitespace: true }); }, getSkipSingleStacks() { + const input = core2.getBooleanInput("skip-single-stacks", { + required: false, + trimWhitespace: true + }); core2.startGroup("Inputs: Skip single stacks"); - const input = core2.getBooleanInput("skip-single-stacks", { required: false }); core2.info(input.toString()); core2.endGroup(); return input; }, + getHistoryLimit() { + const input = core2.getInput("history-limit", { + required: false, + trimWhitespace: true + }); + const historyLimit = Number.parseInt(input, 10); + core2.startGroup("Inputs: History limit"); + core2.info(input); + core2.endGroup(); + return historyLimit; + }, async getMainBranch(octokit, config2, context3) { const { data: { default_branch: defaultBranch } @@ -47155,24 +47169,44 @@ var inputs = { throw error; } }, - async getPullRequests(octokit, context3) { - const pullRequests = await octokit.paginate( - "GET /repos/{owner}/{repo}/pulls", - { - ...context3.repo, - state: "all", - per_page: 100 - }, - (response) => response.data.map( - (item) => ({ - number: item.number, - base: { ref: item.base.ref }, - head: { ref: item.head.ref }, - body: item.body ?? void 0, - state: item.state - }) + async getPullRequests(octokit, context3, historyLimit) { + function toPullRequest(item) { + return { + number: item.number, + base: { ref: item.base.ref }, + head: { ref: item.head.ref }, + body: item.body ?? void 0, + state: item.state + }; + } + let closedPullRequestCount = 0; + const [openPullRequests, closedPullRequests] = await Promise.all([ + octokit.paginate( + "GET /repos/{owner}/{repo}/pulls", + { + ...context3.repo, + state: "open", + per_page: 100 + }, + (response) => response.data.map(toPullRequest) + ), + octokit.paginate( + "GET /repos/{owner}/{repo}/pulls", + { + ...context3.repo, + state: "closed", + per_page: 100 + }, + (response, done) => { + closedPullRequestCount += response.data.length; + if (historyLimit > 0 && closedPullRequestCount >= historyLimit) { + done(); + } + return response.data.map(toPullRequest); + } ) - ); + ]); + const pullRequests = [...openPullRequests, ...closedPullRequests]; pullRequests.sort((a, b) => b.number - a.number); core2.startGroup("Inputs: Pull requests"); core2.info( @@ -47224,10 +47258,11 @@ async function run() { return; } const octokit = github2.getOctokit(inputs.getToken()); + const historyLimit = inputs.getHistoryLimit(); const [mainBranch, remoteBranches, pullRequests] = await Promise.all([ inputs.getMainBranch(octokit, config, github2.context), inputs.getRemoteBranches(octokit, github2.context), - inputs.getPullRequests(octokit, github2.context) + inputs.getPullRequests(octokit, github2.context, historyLimit) ]); const perennialBranches = await inputs.getPerennialBranches(config, remoteBranches); const context3 = { diff --git a/src/index.ts b/src/index.ts index ca0eaaf..db88086 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,10 +19,11 @@ async function run() { const octokit = github.getOctokit(inputs.getToken()) + const historyLimit = inputs.getHistoryLimit() const [mainBranch, remoteBranches, pullRequests] = await Promise.all([ inputs.getMainBranch(octokit, config, github.context), inputs.getRemoteBranches(octokit, github.context), - inputs.getPullRequests(octokit, github.context), + inputs.getPullRequests(octokit, github.context, historyLimit), ]) const perennialBranches = await inputs.getPerennialBranches(config, remoteBranches) diff --git a/src/inputs.ts b/src/inputs.ts index 95219ae..3b11388 100644 --- a/src/inputs.ts +++ b/src/inputs.ts @@ -11,25 +11,29 @@ export const inputs = { }, getSkipSingleStacks() { - core.startGroup('Inputs: Skip single stacks') const input = core.getBooleanInput('skip-single-stacks', { required: false, trimWhitespace: true, }) + + core.startGroup('Inputs: Skip single stacks') core.info(input.toString()) core.endGroup() + return input }, - getHistoryLimit() { - core.startGroup('Inputs: History limit') + getHistoryLimit(): number { const input = core.getInput('history-limit', { required: false, trimWhitespace: true, }) const historyLimit = Number.parseInt(input, 10) + + core.startGroup('Inputs: History limit') core.info(input) core.endGroup() + return historyLimit }, @@ -132,7 +136,11 @@ export const inputs = { } }, - async getPullRequests(octokit: Octokit, context: typeof github.context) { + async getPullRequests( + octokit: Octokit, + context: typeof github.context, + historyLimit: number + ) { function toPullRequest( item: Endpoints['GET /repos/{owner}/{repo}/pulls']['response']['data'][number] ): PullRequest { @@ -145,6 +153,8 @@ export const inputs = { } } + let closedPullRequestCount = 0 + const [openPullRequests, closedPullRequests] = await Promise.all([ octokit.paginate( 'GET /repos/{owner}/{repo}/pulls', @@ -164,6 +174,12 @@ export const inputs = { per_page: 100, }, (response, done) => { + closedPullRequestCount += response.data.length + + if (historyLimit > 0 && closedPullRequestCount >= historyLimit) { + done() + } + return response.data.map(toPullRequest) } ),