Skip to content

Commit

Permalink
feat(democrat): Allow pull pull request parameters to be configurable (
Browse files Browse the repository at this point in the history
  • Loading branch information
deuzu authored Apr 9, 2021
1 parent 80a8cb3 commit b4052cc
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 55 deletions.
5 changes: 1 addition & 4 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
version: 2
updates:
# Enable version updates for npm
- package-ecosystem: 'npm'
# Look for `package.json` and `lock` files in the `root` directory
directory: '/'
# Check the npm registry for updates every day (weekdays)
schedule:
interval: 'daily'
interval: 'weekly'
7 changes: 7 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ jobs:
- uses: actions/checkout@v2
- run: npm install
- run: npm run all
- uses: ./
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
- uses: ./
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
dryRun: true
prMinimumReviewScore: 1
prMaturity: 24
prMarkAsMegeableLabel: ready
prTargetBranch: main
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<br />
<p align="center">
<a href="#">
<img src="./logo.svg" alt="Logo" width="100" height="67">
<!-- logomakr.com/6hgBHr && picsvg.com -->
</a>
<img src="./logo.svg" alt="Logo" width="100" height="67">
<!-- logomakr.com/6hgBHr && picsvg.com -->

<h3 align="center">Github Democrat Action</h3>

Expand Down Expand Up @@ -62,16 +60,22 @@ jobs:
- uses: deuzu/github-democrat-action
with:
githubToken: ${{ secrets.GITHUB_TOKEN }} # GitHub automatically creates the GITHUB_TOKEN secret
# dryRun: true # the Github democrat will process but won't merge pull requests
# dryRun: true
# prMinimumReviewScore: 1
# prMaturity: 24
# prMarkAsMegeableLabel: ready
# prTargetBranch: main
```

The job will hunt pull requests and merge ones that fit the following constraints:
Cf. [./action.yaml](./action.yaml) for action inputs.

The job will look for open pull requests and merge ones that satisfy the following constraints (configurable):
- receive more than half of the majority vote cast (votes are review approves and request changes)
- is ready to be merged (with a `ready` label)
- is mature (last commit is older than 24h)
- target is the `main` branch
- is ready to be merged (with a configurable label)
- is mature (last commit is older than a configurable delay)
- target is the configured branch

To avoid fraud, it's advised to add protections on the main branch:
To avoid fraud, it's advised to add protections on the target branch:
- [X] Require pull request reviews before merging
- [X] Dismiss stale pull request approvals when new commits are pushed
- [X] Require status checks to pass before merging
Expand Down
18 changes: 16 additions & 2 deletions __tests__/democrat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ describe('Democrat', () => {
})

test('Enforce democracy', async () => {
const democrat = new Democrat({ token: '12345', owner: 'org', repo: 'repo', dryRun: false, logFunction: () => {} })
const democratParameters = { token: '12345abc', owner: 'org', repo: 'repo', dryRun: false, logFunction: () => {} }
const pullRequestParameters = {
minimumReviewScore: 1,
maturity: 24,
markAsMergeableLabel: 'ready',
targetBranch: 'main',
}
const democrat = new Democrat(democratParameters, pullRequestParameters)
await democrat.enforceDemocracy()

expect(pullListMock).toHaveBeenCalledTimes(1)
Expand All @@ -45,7 +52,14 @@ describe('Democrat', () => {
})

test('Enforce democracy - DryRun', async () => {
const democrat = new Democrat({ token: '12345', owner: 'org', repo: 'repo', dryRun: true, logFunction: () => {} })
const democratParameters = { token: '12345abc', owner: 'org', repo: 'repo', dryRun: true, logFunction: () => {} }
const pullRequestParameters = {
minimumReviewScore: 1,
maturity: 24,
markAsMergeableLabel: 'ready',
targetBranch: 'main',
}
const democrat = new Democrat(democratParameters, pullRequestParameters)
await democrat.enforceDemocracy()

expect(pullListMock).toHaveBeenCalledTimes(1)
Expand Down
17 changes: 16 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,22 @@ inputs:
required: false
description: "When `dryRun: true` the Github democrat won't merge any pull requests"
default: false

prMinimumReviewScore:
required: false
description: The minimum pull request review score needed to be eligible for merge (approves +1, request changes -1)
default: 1
prMaturity:
required: false
description: The delay (in hours) needed before a pull request is eligible for merge
default: 24
prMarkAsMegeableLabel:
required: false
description: The pull request label needed to be eligible for merge
default: ready
prTargetBranch:
required: false
description: The pull request target branch
default: main
runs:
using: node12
main: dist/index.js
51 changes: 35 additions & 16 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

56 changes: 42 additions & 14 deletions src/democrat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@ import * as github from '@actions/github'
import { GitHub } from '@actions/github/lib/utils'
import { Endpoints } from '@octokit/types'

type logFunction = (level: string, message: string) => void

export interface DemocratParameters {
readonly token: string
readonly owner: string
readonly repo: string
readonly dryRun?: boolean
readonly logFunction?: (level: string, message: string) => void
readonly dryRun: boolean
readonly logFunction: logFunction
}

export interface PullRequestParameters {
readonly minimumReviewScore: number
readonly maturity: number
readonly markAsMergeableLabel: string
readonly targetBranch: string
}

interface PullCandidate {
Expand All @@ -25,15 +34,17 @@ type listReviewsData = Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}/

export default class Democrat {
private democratParameters: DemocratParameters
private pullRequestParameters: PullRequestParameters
private octokit: InstanceType<typeof GitHub>
private logger: (level: string, message: string) => void
private logger: logFunction

constructor(democratParameters: DemocratParameters) {
constructor(democratParameters: DemocratParameters, pullRequestParameters: PullRequestParameters) {
this.democratParameters = democratParameters
this.octokit = github.getOctokit(democratParameters.token)
this.pullRequestParameters = pullRequestParameters
/* eslint-disable no-console */
this.logger = democratParameters.logFunction || ((level, message) => console.log(`${level} - ${message}`))
/* eslint-enable no-console */
this.octokit = github.getOctokit(democratParameters.token)
}

async enforceDemocracy(): Promise<void> {
Expand All @@ -44,7 +55,17 @@ export default class Democrat {
this.logger('info', `${pulls.length} pull request(s) is/are candidate(s) for merge.`)
const pullsAndReviews = await this.fetchPullDetailsAndReviews(pulls)
const pullCandidates = this.buildPullCandidates(pullsAndReviews)
const electedPullCandidates = pullCandidates.filter(this.validatePullCandidate)
const electedPullCandidates = pullCandidates.filter((pull) => {
const errors = this.validatePullCandidate(pull)

if (errors.length > 0) {
this.logger('info', `Pull request #${pull.number} did not pass validation. Errors: ${errors.join(', ')}.`)

return false
}

return true
})
this.logger('info', `${electedPullCandidates.length} pull request(s) left after validation.`)

await this.mergePulls(electedPullCandidates)
Expand Down Expand Up @@ -138,14 +159,21 @@ export default class Democrat {
}
}

private validatePullCandidate = (pullCandidate: PullCandidate): boolean => {
return (
pullCandidate.mergeable &&
pullCandidate.reviewScore >= 1 &&
(+new Date() - pullCandidate.updatedAt.getTime()) / (1000 * 60 * 60) > 24 &&
-1 !== pullCandidate.labels.indexOf('ready') &&
'main' === pullCandidate.base
)
private validatePullCandidate(pullCandidate: PullCandidate): string[] {
const errors = []
const { minimumReviewScore, maturity, markAsMergeableLabel, targetBranch } = this.pullRequestParameters
const lastCommitSinceHours = (+new Date() - pullCandidate.updatedAt.getTime()) / (1000 * 60 * 60)
const hasMergeableLabel = -1 !== pullCandidate.labels.indexOf(markAsMergeableLabel)

pullCandidate.mergeable || errors.push('not mergeable')
pullCandidate.reviewScore >= minimumReviewScore || errors.push(`review score too low: ${pullCandidate.reviewScore}`)
lastCommitSinceHours > maturity ||
errors.push(`not mature enough (last commit ${lastCommitSinceHours.toPrecision(1)}h ago)`)
hasMergeableLabel || errors.push(`missing \`${markAsMergeableLabel}\` label`)
targetBranch === pullCandidate.base ||
errors.push(`wrong target branch: ${pullCandidate.base} instead of ${targetBranch}`)

return errors
}

private async mergePulls(pulls: PullCandidate[]): Promise<void[]> {
Expand Down
19 changes: 12 additions & 7 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import dotenv from 'dotenv'
import * as core from '@actions/core'
import * as github from '@actions/github'
import Democrat, { DemocratParameters } from './democrat'
import Democrat, { DemocratParameters, PullRequestParameters } from './democrat'

dotenv.config()

async function run(): Promise<void> {
try {
const context = github.context
const token = core.getInput('githubToken') || process.env.GITHUB_TOKEN || ''
const [owner, repo] = (context.payload.repository?.full_name || process.env.GITHUB_REPOSITORY || '/').split('/')
const dryRun = (core.getInput('dryRun') || process.env.DRY_RUN) === 'true'

const democratParameter: DemocratParameters = {
token,
const democratParameters: DemocratParameters = {
token: core.getInput('githubToken') || process.env.GITHUB_TOKEN || '',
owner,
repo,
dryRun,
dryRun: (core.getInput('dryRun') || process.env.DRY_RUN) === 'true',
logFunction: (level: string, message: string) => {
/* eslint-disable @typescript-eslint/no-explicit-any */
const coreUntyped = core as any
Expand All @@ -25,7 +23,14 @@ async function run(): Promise<void> {
},
}

const democrat = new Democrat(democratParameter)
const pullRequestParameters: PullRequestParameters = {
minimumReviewScore: parseInt(core.getInput('prMinimumReviewScore') || process.env.PR_MINIMUM_REVIEW_SCORE || ''),
maturity: parseInt(core.getInput('prMaturity') || process.env.PR_MATURITY || ''),
markAsMergeableLabel: core.getInput('prMarkAsMegeableLabel') || process.env.PR_MARK_AS_MERGEABLE_LABEL || '',
targetBranch: core.getInput('prTargetBranch') || process.env.PR_TARGET_BRANCH || '',
}

const democrat = new Democrat(democratParameters, pullRequestParameters)
await democrat.enforceDemocracy()
} catch (error) {
core.setFailed(error.message)
Expand Down

0 comments on commit b4052cc

Please sign in to comment.