mirror of
https://github.com/stoatchat/action-git-town.git
synced 2026-06-30 21:47:56 -04:00
feat: stack visualization in PR comment (#65)
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
# Git Town Action V1
|
||||
# Git Town Action v1
|
||||
|
||||
This action visualizes your stacked changes when proposing pull requests on GitHub:
|
||||
|
||||
@@ -96,48 +96,25 @@ be able to use it again to update the visualization:
|
||||
[ ] Baz
|
||||
```
|
||||
|
||||
## Manual Configuration
|
||||
|
||||
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
|
||||
|
||||
The main branch is the default parent branch for new feature branches, and can be
|
||||
specified using the `main-branch` input:
|
||||
|
||||
```yaml
|
||||
- uses: git-town/action@v1
|
||||
with:
|
||||
main-branch: 'main'
|
||||
```
|
||||
|
||||
The action will default to your repository's default branch, which it fetches via
|
||||
the GitHub REST API.
|
||||
|
||||
### Perennial Branches
|
||||
|
||||
Perennial branches are long lived branches and are never shipped.
|
||||
|
||||
There are two ways to specify perennial branches: explicitly or via regex. This can
|
||||
be done with the `perennial-branches` and `perennial-regex` inputs respectively:
|
||||
|
||||
```yaml
|
||||
- uses: git-town/action@v1
|
||||
with:
|
||||
perennial-branches: |
|
||||
dev
|
||||
staging
|
||||
prod
|
||||
perennial-regex: '^release-.*$'
|
||||
```
|
||||
|
||||
Both inputs can be used at the same time. The action will merge the perennial
|
||||
branches given into a single, de-duplicated list.
|
||||
|
||||
## Customization
|
||||
|
||||
### Visualization Location
|
||||
|
||||
The location of the stack visualization can be customized using the `location` input.
|
||||
Valid options for this input include:
|
||||
|
||||
- `description`: This is the default option. The stack visualization will appear within the
|
||||
pull request description. This will require granting `pull-requests: write` permissions to the
|
||||
action.
|
||||
- `comment`: The stack visualization will appear in a separate comment. No additional permissions
|
||||
are required for this option.
|
||||
|
||||
```yaml
|
||||
- uses: git-town/action@v1
|
||||
with:
|
||||
location: comment
|
||||
```
|
||||
|
||||
### Skip Single Stacks
|
||||
|
||||
If you don't want the stack visualization to appear on pull requests which are **not** part
|
||||
@@ -160,12 +137,12 @@ and closed pull requests. However, this can increase the runtime of the action f
|
||||
larger/older repositories.
|
||||
|
||||
If you're experiencing long runtimes, the `history-limit` input can be configured to
|
||||
limit the total number of closed pull requests fetched by the action:
|
||||
limit the total number of pull requests fetched by the action:
|
||||
|
||||
```yaml
|
||||
- uses: git-town/action@v1
|
||||
with:
|
||||
history-limit: '500' # Only fetch the latest 500 closed pull requests
|
||||
history-limit: 500 # Only fetch the latest 500 pull requests
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
@@ -195,6 +172,46 @@ it into the actions's `github-token` input to grant it sufficient permissions:
|
||||
github-token: ${{ secrets.GIT_TOWN_PAT }} # 👈 Add this to `git-town.yml`
|
||||
```
|
||||
|
||||
## Manual Configuration
|
||||
|
||||
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
|
||||
|
||||
The main branch is the default parent branch for new feature branches, and can be
|
||||
specified using the `main-branch` input:
|
||||
|
||||
```yaml
|
||||
- uses: git-town/action@v1
|
||||
with:
|
||||
main-branch: main
|
||||
```
|
||||
|
||||
The action will default to your repository's default branch, which it fetches via
|
||||
the GitHub REST API.
|
||||
|
||||
### Perennial Branches
|
||||
|
||||
Perennial branches are long lived branches and are never shipped.
|
||||
|
||||
There are two ways to specify perennial branches: explicitly or via regex. This can
|
||||
be done with the `perennial-branches` and `perennial-regex` inputs respectively:
|
||||
|
||||
```yaml
|
||||
- uses: git-town/action@v1
|
||||
with:
|
||||
perennial-branches: |
|
||||
dev
|
||||
staging
|
||||
prod
|
||||
perennial-regex: '^release-.*$'
|
||||
```
|
||||
|
||||
Both inputs can be used at the same time. The action will merge the perennial
|
||||
branches given into a single, de-duplicated list.
|
||||
|
||||
## Reference
|
||||
|
||||
```yaml
|
||||
@@ -211,6 +228,9 @@ inputs:
|
||||
perennial-regex:
|
||||
required: false
|
||||
default: ''
|
||||
location:
|
||||
required: false
|
||||
default: 'description'
|
||||
skip-single-stacks:
|
||||
required: false
|
||||
default: false
|
||||
@@ -219,7 +239,6 @@ inputs:
|
||||
default: '0'
|
||||
```
|
||||
|
||||
|
||||
## License
|
||||
|
||||
The scripts and documentation in this project are released under the [MIT License](LICENSE).
|
||||
|
||||
@@ -18,6 +18,9 @@ inputs:
|
||||
perennial-regex:
|
||||
required: false
|
||||
default: ''
|
||||
location:
|
||||
required: false
|
||||
default: 'description'
|
||||
skip-single-stacks:
|
||||
required: false
|
||||
default: false
|
||||
|
||||
Vendored
+408
-348
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -3,6 +3,7 @@ import * as github from '@actions/github'
|
||||
import { main } from './main'
|
||||
import { inputs } from './inputs'
|
||||
import { config } from './config'
|
||||
import type { Context } from './types'
|
||||
|
||||
void run()
|
||||
|
||||
@@ -19,6 +20,8 @@ async function run() {
|
||||
|
||||
const octokit = github.getOctokit(inputs.getToken())
|
||||
|
||||
const location = inputs.getLocation()
|
||||
const skipSingleStacks = inputs.getSkipSingleStacks()
|
||||
const historyLimit = inputs.getHistoryLimit()
|
||||
const [mainBranch, remoteBranches, pullRequests] = await Promise.all([
|
||||
inputs.getMainBranch(octokit, config, github.context),
|
||||
@@ -32,10 +35,10 @@ async function run() {
|
||||
currentPullRequest: inputs.getCurrentPullRequest(github.context),
|
||||
pullRequests,
|
||||
mainBranch,
|
||||
remoteBranches,
|
||||
perennialBranches,
|
||||
skipSingleStacks: inputs.getSkipSingleStacks(),
|
||||
}
|
||||
skipSingleStacks,
|
||||
location,
|
||||
} satisfies Context
|
||||
|
||||
void main(context)
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,12 +4,24 @@ import type { Endpoints } from '@octokit/types'
|
||||
import { pullRequestSchema } from './types'
|
||||
import type { PullRequest, Octokit } from './types'
|
||||
import type { Config } from './config'
|
||||
import { locationInputSchema, type LocationInput } from './locations/types'
|
||||
|
||||
export const inputs = {
|
||||
getToken() {
|
||||
return core.getInput('github-token', { required: true, trimWhitespace: true })
|
||||
},
|
||||
|
||||
getLocation(): LocationInput {
|
||||
const location = core.getInput('location', { required: false, trimWhitespace: true })
|
||||
|
||||
try {
|
||||
return locationInputSchema.parse(location)
|
||||
} catch {
|
||||
core.setFailed(`Invalid 'location' input: ${location}`)
|
||||
process.exit(1)
|
||||
}
|
||||
},
|
||||
|
||||
getSkipSingleStacks() {
|
||||
const input = core.getBooleanInput('skip-single-stacks', {
|
||||
required: false,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import { ANCHOR, injectVisualization } from '../renderer'
|
||||
import type { Context, Octokit, PullRequest } from '../types'
|
||||
import type { AbstractLocationAdapter } from './types'
|
||||
|
||||
export class CommentLocationAdapter implements AbstractLocationAdapter {
|
||||
private octokit: Octokit
|
||||
|
||||
constructor(context: Context) {
|
||||
this.octokit = context.octokit
|
||||
}
|
||||
|
||||
async update(pullRequest: PullRequest, visualization: string) {
|
||||
core.startGroup(`Update: PR #${pullRequest.number} (COMMENT)`)
|
||||
core.info('Visualization:')
|
||||
core.info(visualization)
|
||||
|
||||
const { data: comments } = await this.octokit.rest.issues.listComments({
|
||||
...github.context.repo,
|
||||
issue_number: pullRequest.number,
|
||||
})
|
||||
|
||||
const existingComment = comments.find((comment) => comment.body?.includes(ANCHOR))
|
||||
if (existingComment) {
|
||||
const content = injectVisualization(visualization, existingComment.body ?? '')
|
||||
|
||||
await this.octokit.rest.issues.updateComment({
|
||||
...github.context.repo,
|
||||
comment_id: existingComment.id,
|
||||
issue_number: pullRequest.number,
|
||||
body: content,
|
||||
})
|
||||
} else {
|
||||
const content = injectVisualization(visualization, '')
|
||||
|
||||
await this.octokit.rest.issues.createComment({
|
||||
...github.context.repo,
|
||||
issue_number: pullRequest.number,
|
||||
body: content,
|
||||
})
|
||||
}
|
||||
|
||||
core.endGroup()
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import { injectVisualization } from '../renderer'
|
||||
import type { Context, Octokit, PullRequest } from '../types'
|
||||
import type { Location } from './types'
|
||||
import type { AbstractLocationAdapter } from './types'
|
||||
|
||||
export class DescriptionLocation implements Location {
|
||||
export class DescriptionLocationAdapter implements AbstractLocationAdapter {
|
||||
private octokit: Octokit
|
||||
|
||||
constructor(context: Context) {
|
||||
@@ -12,7 +12,7 @@ export class DescriptionLocation implements Location {
|
||||
}
|
||||
|
||||
async update(pullRequest: PullRequest, visualization: string) {
|
||||
core.startGroup(`Update: PR #${pullRequest.number}`)
|
||||
core.startGroup(`Update: PR #${pullRequest.number} (DESCRIPTION)`)
|
||||
core.info('Visualization:')
|
||||
core.info(visualization)
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Context } from '../types'
|
||||
import { CommentLocationAdapter } from './comment'
|
||||
import { DescriptionLocationAdapter } from './description'
|
||||
import type { AbstractLocationAdapter } from './types'
|
||||
|
||||
export function createLocationAdapter(context: Context): AbstractLocationAdapter {
|
||||
switch (context.location) {
|
||||
case 'description':
|
||||
return new DescriptionLocationAdapter(context)
|
||||
case 'comment':
|
||||
return new CommentLocationAdapter(context)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { infer as InferType } from 'zod'
|
||||
import { z } from 'zod'
|
||||
import type { PullRequest } from '../types'
|
||||
|
||||
export type Location = {
|
||||
update: (pullRequest: PullRequest, visualization: string) => Promise<void>
|
||||
export const locationInputSchema = z.enum(['description', 'comment'])
|
||||
export type LocationInput = InferType<typeof locationInputSchema>
|
||||
|
||||
export abstract class AbstractLocationAdapter {
|
||||
abstract update(pullRequest: PullRequest, visualization: string): Promise<void>
|
||||
}
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ import { DirectedGraph } from 'graphology'
|
||||
import { bfsFromNode, dfsFromNode } from 'graphology-traversal'
|
||||
import type { PullRequest, Context, StackNodeAttributes } from './types'
|
||||
import { renderVisualization } from './renderer'
|
||||
import { DescriptionLocation } from './locations/description'
|
||||
import { createLocationAdapter } from './locations/factory'
|
||||
|
||||
export async function main(context: Context) {
|
||||
const {
|
||||
@@ -103,8 +103,8 @@ export async function main(context: Context) {
|
||||
const stackGraph = getStackGraph(stackNode, repoGraph)
|
||||
const visualization = renderVisualization(stackGraph, terminatingRefs)
|
||||
|
||||
const target = new DescriptionLocation(context)
|
||||
await target.update(stackNode, visualization)
|
||||
const location = createLocationAdapter(context)
|
||||
await location.update(stackNode, visualization)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { getOctokit } from '@actions/github'
|
||||
import type { infer as InferType } from 'zod'
|
||||
import { object, number, string } from 'zod'
|
||||
import type { LocationInput } from './locations/types'
|
||||
|
||||
export type Octokit = ReturnType<typeof getOctokit>
|
||||
|
||||
@@ -24,6 +25,7 @@ export type Context = {
|
||||
pullRequests: PullRequest[]
|
||||
perennialBranches: string[]
|
||||
skipSingleStacks: boolean
|
||||
location: LocationInput
|
||||
}
|
||||
|
||||
export type StackNode =
|
||||
|
||||
Reference in New Issue
Block a user