-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
330 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
node_modules | ||
.idea | ||
build | ||
package-lock.json | ||
coverage | ||
junit.xml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Install | ||
FROM node:lts-slim as install-stage | ||
WORKDIR /app | ||
COPY package.json /app/ | ||
RUN npm install | ||
|
||
# Build | ||
FROM node:lts-slim as build-stage | ||
WORKDIR /app | ||
COPY --from=install-stage /app /app | ||
COPY src /app/src | ||
COPY tsconfig.json /app/tsconfig.json | ||
RUN npm run build:main | ||
|
||
# Bundle | ||
FROM node:lts-slim | ||
LABEL "maintainer"="Travelperk <engineering@travelperk.com>" | ||
LABEL "com.github.actions.name"="Label requires reviews" | ||
LABEL "com.github.actions.description"="Require a number of reviews for a certain label" | ||
COPY --from=build-stage /app /app | ||
ENTRYPOINT ["node", "/app/build/entrypoint.js"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
name: Label requires reviews | ||
author: TravelPerk <engineering@travelperk.com> | ||
description: This Github action will require a minimum number of reviews if a label is present. | ||
runs: | ||
using: 'docker' | ||
image: 'docker://travelperk/label-requires-reviews:0.0.1' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
module.exports = { | ||
coverageDirectory: './coverage', | ||
coveragePathIgnorePatterns: [ | ||
'/node_modules/', | ||
'/lib/', | ||
'/esm/', | ||
], | ||
coverageReporters: [ | ||
'lcov', | ||
], | ||
reporters: ["default", "jest-junit"], | ||
globals: { | ||
__DEV__: true, | ||
'ts-jest': { | ||
babelConfig: true, | ||
}, | ||
}, | ||
roots: [ | ||
'./src', | ||
'./tests', | ||
], | ||
setupFiles: [], | ||
setupFilesAfterEnv: null, | ||
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', | ||
transform: { | ||
'^.+\\.ts?$': 'ts-jest', | ||
}, | ||
verbose: false, | ||
preset: 'ts-jest', | ||
testMatch: null, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"name": "label-requires-reviews-action", | ||
"version": "0.0.1", | ||
"description": "Require a number of reviews for a certain label", | ||
"main": "build/entrypoint.js", | ||
"scripts": { | ||
"start": "node ./build/entrypoint.js", | ||
"test": "jest --collect-coverage", | ||
"test:watch": "jest --watchAll", | ||
"build:main": "tsc -p tsconfig.json" | ||
}, | ||
"dependencies": { | ||
"actions-toolkit": "2.1.0", | ||
"jest-junit": "^6.4.0" | ||
}, | ||
"devDependencies": { | ||
"@octokit/rest": "16.28.1", | ||
"@types/jest": "^24.0.15", | ||
"awesome-typescript-loader": "^5.2.1", | ||
"jest": "^24.8.0", | ||
"signale": "^1.4.0", | ||
"source-map-loader": "^0.2.4", | ||
"ts-jest": "^24.0.2", | ||
"tslint": "^5.17.0", | ||
"tslint-config-prettier": "^1.18.0", | ||
"tslint-immutable": "^6.0.0", | ||
"typescript": "^3.5.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { | ||
IssuesListLabelsOnIssueParams, | ||
PullsListReviewsParams | ||
} from '@octokit/rest' | ||
import { | ||
getRulesForLabels, | ||
getMaxReviewNumber, | ||
getCurrentReviewCount, | ||
findRepositoryInformation | ||
} from './main' | ||
import {Toolkit, ToolkitOptions} from 'actions-toolkit' | ||
import {GitHub} from 'actions-toolkit/lib/github' | ||
import {Rule} from './types' | ||
|
||
const args: ToolkitOptions = { | ||
event: [ | ||
'pull_request.labeled', | ||
'pull_request.unlabeled', | ||
'pull_request.ready_for_review', | ||
'pull_request_review.submitted', | ||
'pull_request_review.edited', | ||
'pull_request_review.dismissed' | ||
], | ||
secrets: ['GITHUB_TOKEN'] | ||
} | ||
|
||
Toolkit.run(async (toolkit: Toolkit) => { | ||
toolkit.log.info('Running Action') | ||
const configPath: string = process.env.CONFIG_PATH ?? '.github/label-requires-reviews.yml' | ||
const rules: Rule[] = toolkit.config(configPath) | ||
toolkit.log.info("Configured rules: ", rules) | ||
|
||
// Get the repository information | ||
if (!process.env.GITHUB_EVENT_PATH) { | ||
toolkit.exit.failure('Process env GITHUB_EVENT_PATH is undefined') | ||
} | ||
const {owner, issue_number, repo}: IssuesListLabelsOnIssueParams = findRepositoryInformation( | ||
process.env.GITHUB_EVENT_PATH, | ||
toolkit.log, | ||
toolkit.exit | ||
) | ||
const client: GitHub = toolkit.github | ||
|
||
// Get the list of configuration rules for the labels on the issue | ||
const matchingRules: Rule[] = await getRulesForLabels({owner, issue_number, repo}, client, rules) | ||
toolkit.log.info("Matching rules: ", matchingRules) | ||
|
||
// Get the required number of required reviews from the rules | ||
const requiredReviews: number = getMaxReviewNumber(matchingRules) | ||
|
||
// Get the actual number of reviews from the issue | ||
const reviewCount: number = await getCurrentReviewCount(<PullsListReviewsParams>{owner, pull_number:issue_number, repo}, client) | ||
if (reviewCount < requiredReviews) { | ||
toolkit.exit.failure(`Labels require ${requiredReviews} reviews but the PR only has ${reviewCount}`) | ||
} | ||
toolkit.exit.success(`Labels require ${requiredReviews} the PR has ${reviewCount}`) | ||
}, | ||
args | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { | ||
IssuesListLabelsOnIssueParams, | ||
IssuesListLabelsOnIssueResponse, | ||
PullsListReviewsParams, | ||
PullsListReviewsResponse, | ||
Response | ||
} from '@octokit/rest' | ||
import {WebhookPayloadWithRepository} from 'actions-toolkit/lib/context' | ||
import {Exit} from 'actions-toolkit/lib/exit' | ||
import {LoggerFunc, Signale} from 'signale' | ||
import {Rule} from './types' | ||
|
||
// Get the maximum number of reviews based on the configuration and the issue labels | ||
export const getRulesForLabels = async (issuesListLabelsOnIssueParams: IssuesListLabelsOnIssueParams, client, rules: Rule[]): Promise<Rule[]> => { | ||
return client.issues.listLabelsOnIssue(issuesListLabelsOnIssueParams) | ||
.then(({data: labels}: Response<IssuesListLabelsOnIssueResponse>) => { | ||
return labels.reduce((acc, label) => acc.concat(label.name), []) | ||
}) | ||
.then((issueLabels: string[]) => rules.filter(rule => issueLabels.includes(rule.label))) | ||
} | ||
|
||
// Get the maximum number of reviews based on the configuration and the issue labels | ||
export const getMaxReviewNumber = (rules: Rule[]): number => rules.reduce((acc, rule) => rule.reviews > acc ? rule.reviews : acc , 0) | ||
|
||
// Returns the repository information using provided gitHubEventPath | ||
export const findRepositoryInformation = ( | ||
gitHubEventPath: string, | ||
log: LoggerFunc & Signale, | ||
exit: Exit | ||
): IssuesListLabelsOnIssueParams => { | ||
const payload: WebhookPayloadWithRepository = require(gitHubEventPath) | ||
if (payload.number === undefined) { | ||
exit.neutral('Action not triggered by a PullRequest action. PR ID is missing') | ||
} | ||
log.info(`Checking files list for PR#${payload.number}`) | ||
return { | ||
issue_number: payload.number, | ||
owner: payload.repository.owner.login, | ||
repo: payload.repository.name | ||
} | ||
} | ||
|
||
// Get the current review count from an issue | ||
export const getCurrentReviewCount = async (pullsListReviewsParams: PullsListReviewsParams, client): Promise<number> => { | ||
return client.pulls.listReviews(pullsListReviewsParams) | ||
.then(({data: reviews}: Response<PullsListReviewsResponse>) => { | ||
return reviews.reduce((acc, review) => review.state === 'APPROVED' ? acc + 1 : acc, 0) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export interface Rule { | ||
label: string | ||
reviews: number | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { | ||
IssuesListLabelsOnIssueParams, | ||
PullsListReviewsParams | ||
} from '@octokit/rest' | ||
import { Rule } from '../src/types' | ||
import { | ||
getRulesForLabels, | ||
getMaxReviewNumber, | ||
getCurrentReviewCount | ||
} from '../src/main' | ||
|
||
const MIGRATION_RULE: Rule = { label: "migration", reviews: 2 } | ||
const TYPESCRIPT_RULE: Rule = { label: "typescript", reviews: 6 } | ||
const LIST_LABELS_PARAMS: IssuesListLabelsOnIssueParams = { | ||
owner: 'travelperk', | ||
issue_number: 1, | ||
repo: 'repository' | ||
} | ||
|
||
const LIST_REVIEWS_PARAMS: PullsListReviewsParams = { | ||
owner: 'travelperk', | ||
pull_number: 1, | ||
repo: 'repository' | ||
} | ||
|
||
const client = { | ||
issues : { | ||
listLabelsOnIssue: jest.fn().mockResolvedValue({ | ||
data: [ | ||
{name: "migration"} | ||
] | ||
}) | ||
}, | ||
pulls : { | ||
listReviews: jest.fn().mockResolvedValue({ | ||
data: [ | ||
{state: "APPROVED"}, | ||
{state: "PENDING"}, | ||
{state: "APPROVED"}, | ||
] | ||
}) | ||
}, | ||
} | ||
|
||
describe('getRulesForLabels', () => { | ||
it('should return empty array if no matching rule', | ||
async () => expect(await getRulesForLabels(LIST_LABELS_PARAMS, client, [TYPESCRIPT_RULE])).toStrictEqual([]) | ||
) | ||
|
||
it('should get the matching rules for the Pull Request labels', | ||
async () => expect( | ||
await getRulesForLabels(LIST_LABELS_PARAMS, client, [TYPESCRIPT_RULE, MIGRATION_RULE]) | ||
).toStrictEqual([MIGRATION_RULE]) | ||
) | ||
}) | ||
|
||
describe('getMaxReviewNumber', () => { | ||
it('should return 0 reviews for an empty array', | ||
() => expect(getMaxReviewNumber([])).toStrictEqual(0) | ||
) | ||
|
||
it('should return the highest review number from a set of rules', | ||
() => expect(getMaxReviewNumber([TYPESCRIPT_RULE, MIGRATION_RULE])).toStrictEqual(6) | ||
) | ||
}) | ||
|
||
describe('findRepositoryInformation', () => { | ||
//TODO: This tests are still missing | ||
}) | ||
|
||
describe('getCurrentReviewCount', () => { | ||
it('should return the number of approved reviews', | ||
async () => expect(await getCurrentReviewCount(LIST_REVIEWS_PARAMS, client)).toStrictEqual(2) | ||
) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "es2017", | ||
"outDir": "build", | ||
"rootDir": "src", | ||
"moduleResolution": "node", | ||
"module": "commonjs", | ||
"sourceMap": true, | ||
"strict": false, | ||
"esModuleInterop": true | ||
}, | ||
"include": [ | ||
"src/**/*.ts" | ||
], | ||
"exclude": [ | ||
"node_modules/**", | ||
"build/" | ||
], | ||
"compileOnSave": false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
{ | ||
"extends": [ | ||
"tslint:latest", | ||
"tslint-config-prettier", | ||
"tslint-immutable" | ||
], | ||
"rules": { | ||
"interface-name": [ | ||
true, | ||
"never-prefix" | ||
], | ||
"no-implicit-dependencies": [ | ||
false | ||
], | ||
"no-var-keyword": true, | ||
"no-parameter-reassignment": true, | ||
"typedef": [ | ||
true, | ||
"call-signature" | ||
], | ||
"no-let": true, | ||
"no-object-mutation": true, | ||
"no-delete": true, | ||
"no-method-signature": true, | ||
"no-this": true, | ||
"no-class": true, | ||
"no-mixed-interface": true, | ||
"adjacent-overload-signatures": true | ||
} | ||
} |