Skip to content

Commit

Permalink
Write the first action version
Browse files Browse the repository at this point in the history
  • Loading branch information
namelivia committed Jun 8, 2020
1 parent 3425686 commit 248ca88
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
.idea
build
package-lock.json
coverage
junit.xml
21 changes: 21 additions & 0 deletions Dockerfile
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"]
6 changes: 6 additions & 0 deletions action.yml
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'
31 changes: 31 additions & 0 deletions jest.config.js
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,
}
29 changes: 29 additions & 0 deletions package.json
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"
}
}
59 changes: 59 additions & 0 deletions src/entrypoint.ts
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
)
49 changes: 49 additions & 0 deletions src/main.ts
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)
})
}
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Rule {
label: string
reviews: number
}
75 changes: 75 additions & 0 deletions tests/main.test.ts
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)
)
})
20 changes: 20 additions & 0 deletions tsconfig.json
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
}
30 changes: 30 additions & 0 deletions tslint.json
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
}
}

0 comments on commit 248ca88

Please sign in to comment.