Convert to typescript

This commit is contained in:
Peter Evans
2020-07-20 15:28:38 +09:00
parent 4a930f9887
commit ad9cf8de57
18 changed files with 7291 additions and 22483 deletions
+3
View File
@@ -0,0 +1,3 @@
dist/
lib/
node_modules/
+18 -16
View File
@@ -1,17 +1,19 @@
{
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
}
}
"env": { "node": true, "jest": true },
"parser": "@typescript-eslint/parser",
"parserOptions": { "ecmaVersion": 9, "sourceType": "module" },
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:prettier/recommended",
"prettier/@typescript-eslint"
],
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/camelcase": "off"
}
}
+3 -1
View File
@@ -19,8 +19,10 @@ jobs:
with:
node-version: 12.x
- run: npm ci
- run: npm run build
- run: npm run format-check
- run: npm run lint
- run: npm run test
- run: npm run package
- uses: actions/upload-artifact@v2
with:
name: dist
+2 -1
View File
@@ -1 +1,2 @@
node_modules
lib/
node_modules/
+3
View File
@@ -0,0 +1,3 @@
dist/
lib/
node_modules/
+11
View File
@@ -0,0 +1,11 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid",
"parser": "typescript"
}
+231
View File
@@ -0,0 +1,231 @@
import {
Inputs,
Command,
SlashCommandPayload,
commandDefaults,
getCommandsConfigFromInputs,
getCommandsConfigFromJson,
actorHasPermission,
configIsValid,
getSlashCommandPayload
} from '../lib/command-helper'
describe('command-helper tests', () => {
test('building config with required inputs only', async () => {
const inputs: Inputs = {
token: '',
reactionToken: '',
reactions: true,
commands: 'list, of, slash, commands',
permission: 'write',
issueType: 'both',
allowEdits: false,
// Should be process.env.GITHUB_REPOSITORY, but '' for tests
repository: '',
eventTypeSuffix: '-command',
namedArgs: false,
config: '',
configFromFile: ''
}
const commands = inputs.commands.replace(/\s+/g, '').split(',')
const config = getCommandsConfigFromInputs(inputs)
expect(config.length).toEqual(4)
for (var i = 0; i < config.length; i++) {
expect(config[i].command).toEqual(commands[i])
expect(config[i].permission).toEqual(commandDefaults.permission)
expect(config[i].issue_type).toEqual(commandDefaults.issue_type)
expect(config[i].allow_edits).toEqual(commandDefaults.allow_edits)
expect(config[i].repository).toEqual(commandDefaults.repository)
expect(config[i].event_type_suffix).toEqual(
commandDefaults.event_type_suffix
)
expect(config[i].named_args).toEqual(commandDefaults.named_args)
}
})
test('building config with optional inputs', async () => {
const inputs: Inputs = {
token: '',
reactionToken: '',
reactions: true,
commands: 'list, of, slash, commands',
permission: 'admin',
issueType: 'pull-request',
allowEdits: true,
repository: 'owner/repo',
eventTypeSuffix: '-cmd',
namedArgs: true,
config: '',
configFromFile: ''
}
const commands = inputs.commands.replace(/\s+/g, '').split(',')
const config = getCommandsConfigFromInputs(inputs)
expect(config.length).toEqual(4)
for (var i = 0; i < config.length; i++) {
expect(config[i].command).toEqual(commands[i])
expect(config[i].permission).toEqual(inputs.permission)
expect(config[i].issue_type).toEqual(inputs.issueType)
expect(config[i].allow_edits).toEqual(inputs.allowEdits)
expect(config[i].repository).toEqual(inputs.repository)
expect(config[i].event_type_suffix).toEqual(inputs.eventTypeSuffix)
expect(config[i].named_args).toEqual(inputs.namedArgs)
}
})
test('building config with required JSON only', async () => {
const json = `[
{
"command": "do-stuff"
},
{
"command": "test-all-the-things"
}
]`
const commands = ['do-stuff', 'test-all-the-things']
const config = getCommandsConfigFromJson(json)
expect(config.length).toEqual(2)
for (var i = 0; i < config.length; i++) {
expect(config[i].command).toEqual(commands[i])
expect(config[i].permission).toEqual(commandDefaults.permission)
expect(config[i].issue_type).toEqual(commandDefaults.issue_type)
expect(config[i].allow_edits).toEqual(commandDefaults.allow_edits)
expect(config[i].repository).toEqual(commandDefaults.repository)
expect(config[i].event_type_suffix).toEqual(
commandDefaults.event_type_suffix
)
expect(config[i].named_args).toEqual(commandDefaults.named_args)
}
})
test('building config with optional JSON properties', async () => {
const json = `[
{
"command": "do-stuff",
"permission": "admin",
"issue_type": "pull-request",
"allow_edits": true,
"repository": "owner/repo",
"event_type_suffix": "-cmd",
"named_args": true
},
{
"command": "test-all-the-things",
"permission": "read"
}
]`
const commands = ['do-stuff', 'test-all-the-things']
const config = getCommandsConfigFromJson(json)
expect(config.length).toEqual(2)
expect(config[0].command).toEqual(commands[0])
expect(config[0].permission).toEqual('admin')
expect(config[0].issue_type).toEqual('pull-request')
expect(config[0].allow_edits).toBeTruthy()
expect(config[0].repository).toEqual('owner/repo')
expect(config[0].event_type_suffix).toEqual('-cmd')
expect(config[0].named_args).toBeTruthy()
expect(config[1].command).toEqual(commands[1])
expect(config[1].permission).toEqual('read')
expect(config[1].issue_type).toEqual(commandDefaults.issue_type)
})
test('valid config', async () => {
const config: Command[] = [
{
command: 'test',
permission: 'write',
issue_type: 'both',
allow_edits: false,
repository: 'peter-evans/slash-command-dispatch',
event_type_suffix: '-command',
named_args: false
}
]
expect(configIsValid(config)).toBeTruthy()
})
test('invalid permission level in config', async () => {
const config: Command[] = [
{
command: 'test',
permission: 'test-case-invalid-permission',
issue_type: 'both',
allow_edits: false,
repository: 'peter-evans/slash-command-dispatch',
event_type_suffix: '-command',
named_args: false
}
]
expect(configIsValid(config)).toBeFalsy()
})
test('invalid issue type in config', async () => {
const config: Command[] = [
{
command: 'test',
permission: 'write',
issue_type: 'test-case-invalid-issue-type',
allow_edits: false,
repository: 'peter-evans/slash-command-dispatch',
event_type_suffix: '-command',
named_args: false
}
]
expect(configIsValid(config)).toBeFalsy()
})
test('actor does not have permission', async () => {
expect(actorHasPermission('none', 'read')).toBeFalsy()
expect(actorHasPermission('read', 'write')).toBeFalsy()
expect(actorHasPermission('write', 'admin')).toBeFalsy()
})
test('actor has permission', async () => {
expect(actorHasPermission('read', 'none')).toBeTruthy()
expect(actorHasPermission('write', 'read')).toBeTruthy()
expect(actorHasPermission('admin', 'write')).toBeTruthy()
expect(actorHasPermission('write', 'write')).toBeTruthy()
})
test('slash command payload', async () => {
const commandWords = ['test', 'arg1', 'arg2', 'arg3']
const namedArgs = false
const payload: SlashCommandPayload = {
command: 'test',
args: 'arg1 arg2 arg3',
arg1: 'arg1',
arg2: 'arg2',
arg3: 'arg3'
}
expect(getSlashCommandPayload(commandWords, namedArgs)).toEqual(payload)
})
test('slash command payload with named args', async () => {
const commandWords = ['test', 'branch=master', 'arg1', 'env=prod', 'arg2']
const namedArgs = true
const payload: SlashCommandPayload = {
command: 'test',
args: 'branch=master arg1 env=prod arg2',
unnamed_args: 'arg1 arg2',
branch: 'master',
env: 'prod',
arg1: 'arg1',
arg2: 'arg2'
}
expect(getSlashCommandPayload(commandWords, namedArgs)).toEqual(payload)
})
test('slash command payload with malformed named args', async () => {
const commandWords = ['test', 'branch=', 'arg1', 'e-nv=prod', 'arg2']
const namedArgs = true
const payload: SlashCommandPayload = {
command: 'test',
args: 'branch= arg1 e-nv=prod arg2',
unnamed_args: 'branch= arg1 e-nv=prod arg2',
arg1: 'branch=',
arg2: 'arg1',
arg3: 'e-nv=prod',
arg4: 'arg2'
}
expect(getSlashCommandPayload(commandWords, namedArgs)).toEqual(payload)
})
})
+7
View File
@@ -9,21 +9,28 @@ inputs:
default: ${{ github.token }}
reactions:
description: 'Add reactions to comments containing commands.'
default: true
commands:
description: 'A comma separated list of commands.'
required: true
permission:
description: 'The repository permission level required by the user to dispatch commands.'
default: write
issue-type:
description: 'The issue type required for commands.'
default: both
allow-edits:
description: 'Allow edited comments to trigger command dispatches.'
default: false
repository:
description: 'The full name of the repository to send the dispatch events.'
default: ${{ github.repository }}
event-type-suffix:
description: 'The repository dispatch event type suffix for the commands.'
default: -command
named-args:
description: 'Parse named arguments and add them to the command payload.'
default: false
config:
description: 'JSON configuration for commands.'
config-from-file:
+3985 -19814
View File
File diff suppressed because it is too large Load Diff
+11 -3
View File
@@ -1,3 +1,11 @@
process.env = Object.assign(process.env, {
GITHUB_REPOSITORY: "peter-evans/slash-command-dispatch"
});
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}
+2630 -2150
View File
File diff suppressed because it is too large Load Diff
+29 -9
View File
@@ -1,18 +1,25 @@
{
"name": "slash-command-dispatch",
"version": "1.0.0",
"private": true,
"description": "Create repository dispatch events for slash commands",
"main": "src/index.js",
"main": "lib/main.js",
"scripts": {
"lint": "eslint src/index.js",
"package": "ncc build src/index.js -o dist",
"test": "eslint src/index.js && jest"
"build": "tsc && ncc build",
"format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'",
"lint": "eslint src/**/*.ts",
"test": "jest"
},
"repository": {
"type": "git",
"url": "git+https://github.com/peter-evans/slash-command-dispatch.git"
},
"keywords": [],
"keywords": [
"slash",
"command",
"dispatch"
],
"author": "Peter Evans",
"license": "MIT",
"bugs": {
@@ -21,11 +28,24 @@
"homepage": "https://github.com/peter-evans/slash-command-dispatch#readme",
"dependencies": {
"@actions/core": "1.2.4",
"@actions/github": "2.1.1"
"@actions/github": "4.0.0",
"@octokit/core": "3.1.0",
"@octokit/plugin-paginate-rest": "2.2.3",
"@octokit/plugin-rest-endpoint-methods": "4.0.0"
},
"devDependencies": {
"@zeit/ncc": "0.22.1",
"eslint": "6.8.0",
"jest": "26.0.1"
"@types/jest": "26.0.4",
"@types/node": "14.0.23",
"@typescript-eslint/parser": "3.6.1",
"@zeit/ncc": "0.22.3",
"eslint": "7.4.0",
"eslint-plugin-github": "4.0.1",
"eslint-plugin-jest": "23.18.0",
"jest": "26.1.0",
"jest-circus": "26.1.0",
"js-yaml": "3.14.0",
"prettier": "2.0.5",
"ts-jest": "26.1.2",
"typescript": "3.9.6"
}
}
+246
View File
@@ -0,0 +1,246 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import {inspect} from 'util'
import {Octokit} from './octokit-client'
const NAMED_ARG_PATTERN = /^(?<name>[a-zA-Z0-9_]+)=(?<value>[^\s]+)$/
export const MAX_ARGS = 50
export interface Inputs {
token: string
reactionToken: string
reactions: boolean
commands: string
permission: string
issueType: string
allowEdits: boolean
repository: string
eventTypeSuffix: string
namedArgs: boolean
config: string
configFromFile: string
}
export interface Command {
command: string
permission: string
issue_type: string
allow_edits: boolean
repository: string
event_type_suffix: string
named_args: boolean
}
interface Repo {
owner: string
repo: string
}
export interface SlashCommandPayload {
command: string
args: string
[k: string]: string
}
export const commandDefaults = Object.freeze({
permission: 'write',
issue_type: 'both',
allow_edits: false,
repository: process.env.GITHUB_REPOSITORY || '',
event_type_suffix: '-command',
named_args: false
})
export function toBool(input: string, defaultVal: boolean): boolean {
if (typeof input === 'boolean') {
return input
} else if (typeof input === 'undefined' || input.length == 0) {
return defaultVal
} else {
return input === 'true'
}
}
export function getInputs(): Inputs {
// Defaults set in action.yml
return {
token: core.getInput('token'),
reactionToken: core.getInput('reaction-token'),
reactions: core.getInput('reactions') === 'true',
commands: core.getInput('commands'),
permission: core.getInput('permission'),
issueType: core.getInput('issue-type'),
allowEdits: core.getInput('allow-edits') === 'true',
repository: core.getInput('repository'),
eventTypeSuffix: core.getInput('event-type-suffix'),
namedArgs: core.getInput('named-args') === 'true',
config: core.getInput('config'),
configFromFile: core.getInput('config-from-file')
}
}
export function getCommandsConfig(inputs: Inputs): Command[] {
if (inputs.configFromFile) {
core.info(`Using JSON configuration from file '${inputs.configFromFile}'.`)
const json = fs.readFileSync(inputs.configFromFile, {
encoding: 'utf8'
})
return getCommandsConfigFromJson(json)
} else if (inputs.config) {
core.info(`Using JSON configuration from 'config' input.`)
return getCommandsConfigFromJson(inputs.config)
} else {
core.info(`Using configuration from yaml inputs.`)
return getCommandsConfigFromInputs(inputs)
}
}
export function getCommandsConfigFromInputs(inputs: Inputs): Command[] {
// Get commands
const commands = inputs.commands.replace(/\s+/g, '').split(',')
core.debug(`Commands: ${inspect(commands)}`)
// Build config
const config: Command[] = []
for (const c of commands) {
const cmd: Command = {
command: c,
permission: inputs.permission,
issue_type: inputs.issueType,
allow_edits: inputs.allowEdits,
repository: inputs.repository,
event_type_suffix: inputs.eventTypeSuffix,
named_args: inputs.namedArgs
}
config.push(cmd)
}
return config
}
export function getCommandsConfigFromJson(json: string): Command[] {
const jsonConfig = JSON.parse(json)
core.debug(`JSON config: ${inspect(jsonConfig)}`)
const config: Command[] = []
for (const jc of jsonConfig) {
const cmd: Command = {
command: jc.command,
permission: jc.permission ? jc.permission : commandDefaults.permission,
issue_type: jc.issue_type ? jc.issue_type : commandDefaults.issue_type,
allow_edits: toBool(jc.allow_edits, commandDefaults.allow_edits),
repository: jc.repository ? jc.repository : commandDefaults.repository,
event_type_suffix: jc.event_type_suffix
? jc.event_type_suffix
: commandDefaults.event_type_suffix,
named_args: toBool(jc.named_args, commandDefaults.named_args)
}
config.push(cmd)
}
return config
}
export function configIsValid(config: Command[]): boolean {
for (const command of config) {
if (!['none', 'read', 'write', 'admin'].includes(command.permission)) {
core.setFailed(`'${command.permission}' is not a valid 'permission'.`)
return false
}
if (!['issue', 'pull-request', 'both'].includes(command.issue_type)) {
core.setFailed(`'${command.issue_type}' is not a valid 'issue-type'.`)
return false
}
}
return true
}
export function actorHasPermission(
actorPermission: string,
commandPermission: string
): boolean {
const permissionLevels = Object.freeze({
none: 1,
read: 2,
write: 3,
admin: 4
})
core.debug(`Actor permission level: ${permissionLevels[actorPermission]}`)
core.debug(`Command permission level: ${permissionLevels[commandPermission]}`)
return (
permissionLevels[actorPermission] >= permissionLevels[commandPermission]
)
}
export async function getActorPermission(
octokit: InstanceType<typeof Octokit>,
repo: Repo,
actor: string
): Promise<string> {
const {
data: {permission}
} = await octokit.repos.getCollaboratorPermissionLevel({
...repo,
username: actor
})
return permission
}
export async function addReaction(
octokit: InstanceType<typeof Octokit>,
repo: Repo,
commentId: number,
reaction:
| '+1'
| '-1'
| 'laugh'
| 'confused'
| 'heart'
| 'hooray'
| 'rocket'
| 'eyes'
): Promise<void> {
try {
await octokit.reactions.createForIssueComment({
...repo,
comment_id: commentId,
content: reaction
})
} catch (error) {
core.debug(inspect(error))
core.warning(`Failed to set reaction on comment ID ${commentId}.`)
}
}
export function getSlashCommandPayload(
commandWords: string[],
namedArgs: boolean
): SlashCommandPayload {
const payload: SlashCommandPayload = {
command: commandWords[0],
args: ''
}
if (commandWords.length > 1) {
const argWords = commandWords.slice(1, MAX_ARGS + 1)
payload.args = argWords.join(' ')
// Parse named and unnamed args
let unnamedCount = 1
const unnamedArgs: string[] = []
for (const argWord of argWords) {
if (namedArgs && NAMED_ARG_PATTERN.test(argWord)) {
const result = NAMED_ARG_PATTERN.exec(argWord)
if (result && result.groups) {
payload[`${result.groups['name']}`] = result.groups['value']
}
} else {
unnamedArgs.push(argWord)
payload[`arg${unnamedCount}`] = argWord
unnamedCount += 1
}
}
// Add a string of only the unnamed args
if (namedArgs && unnamedArgs.length > 0) {
payload['unnamed_args'] = unnamedArgs.join(' ')
}
}
return payload
}
-204
View File
@@ -1,204 +0,0 @@
const { inspect } = require("util");
var fs = require("fs");
const core = require("@actions/core");
const MAX_ARGS = 50;
const namedArgPattern = /^(?<name>[a-zA-Z0-9_]+)=(?<value>[^\s]+)$/;
const commandDefaults = Object.freeze({
permission: "write",
issue_type: "both",
allow_edits: false,
repository: process.env.GITHUB_REPOSITORY,
event_type_suffix: "-command",
named_args: false
});
function toBool(input, defaultVal) {
if (typeof input === 'boolean') {
return input;
} else if (typeof input === 'undefined' || input.length == 0) {
return defaultVal;
} else {
return input === "true";
}
}
function getInputs() {
return {
token: core.getInput("token"),
reactionToken: core.getInput("reaction-token"),
reactions: toBool(core.getInput("reactions"), true),
commands: core.getInput("commands"),
permission: core.getInput("permission"),
issueType: core.getInput("issue-type"),
allowEdits: core.getInput("allow-edits"),
repository: core.getInput("repository"),
eventTypeSuffix: core.getInput("event-type-suffix"),
namedArgs: core.getInput("named-args"),
config: core.getInput("config"),
configFromFile: core.getInput("config-from-file")
};
}
function getCommandsConfig(inputs) {
if (inputs.configFromFile) {
core.info(`Using JSON configuration from file '${inputs.configFromFile}'.`);
return getCommandsConfigFromJson(fs.readFileSync(inputs.configFromFile));
} else if (inputs.config) {
core.info(`Using JSON configuration from 'config' input.`);
return getCommandsConfigFromJson(inputs.config);
} else {
core.info(`Using configuration from yaml inputs.`);
return getCommandsConfigFromInputs(inputs);
}
}
function extend(obj1, obj2) {
var result = {};
for (val in obj1) result[val] = obj1[val];
for (val in obj2) result[val] = obj2[val];
return result;
}
function getCommandsConfigFromInputs(inputs) {
// Get commands
const commands = inputs.commands.replace(/\s+/g, "").split(",");
core.debug(`Commands: ${inspect(commands)}`);
// Build config
var config = [];
for (const c of commands) {
var cmd = {};
cmd.command = c;
cmd = extend(commandDefaults, cmd);
cmd.permission = inputs.permission ? inputs.permission : cmd.permission;
cmd.issue_type = inputs.issueType ? inputs.issueType : cmd.issue_type;
cmd.allow_edits = toBool(inputs.allowEdits, cmd.allow_edits);
cmd.repository = inputs.repository ? inputs.repository : cmd.repository;
cmd.event_type_suffix = inputs.eventTypeSuffix
? inputs.eventTypeSuffix
: cmd.event_type_suffix;
cmd.named_args = toBool(inputs.namedArgs, cmd.named_args);
config.push(cmd);
}
return config;
}
function getCommandsConfigFromJson(json) {
const jsonConfig = JSON.parse(json);
core.debug(`JSON config: ${inspect(jsonConfig)}`);
var config = [];
for (const jc of jsonConfig) {
var cmd = {};
cmd.command = jc.command;
cmd = extend(commandDefaults, cmd);
cmd.permission = jc.permission ? jc.permission : cmd.permission;
cmd.issue_type = jc.issue_type ? jc.issue_type : cmd.issue_type;
cmd.allow_edits = toBool(jc.allow_edits, cmd.allow_edits);
cmd.repository = jc.repository ? jc.repository : cmd.repository;
cmd.event_type_suffix = jc.event_type_suffix
? jc.event_type_suffix
: cmd.event_type_suffix;
cmd.named_args = toBool(jc.named_args, cmd.named_args);
config.push(cmd);
}
return config;
}
function configIsValid(config) {
for (const command of config) {
if (!["none", "read", "write", "admin"].includes(command.permission)) {
core.setFailed(`'${command.permission}' is not a valid 'permission'.`);
return false;
}
if (!["issue", "pull-request", "both"].includes(command.issue_type)) {
core.setFailed(`'${command.issue_type}' is not a valid 'issue-type'.`);
return false;
}
}
return true;
}
function actorHasPermission(actorPermission, commandPermission) {
const permissionLevels = Object.freeze({
none: 1,
read: 2,
write: 3,
admin: 4
});
core.debug(`Actor permission level: ${permissionLevels[actorPermission]}`);
core.debug(
`Command permission level: ${permissionLevels[commandPermission]}`
);
return (
permissionLevels[actorPermission] >= permissionLevels[commandPermission]
);
}
async function getActorPermission(octokit, repo, actor) {
const {
data: { permission }
} = await octokit.repos.getCollaboratorPermissionLevel({
...repo,
username: actor
});
return permission;
}
async function addReaction(octokit, repo, commentId, reaction) {
try {
await octokit.reactions.createForIssueComment({
...repo,
comment_id: commentId,
content: reaction
});
} catch (error) {
core.debug(inspect(error));
core.warning(`Failed to set reaction on comment ID ${commentId}.`);
}
}
function getSlashCommandPayload(commandWords, namedArgs) {
var payload = {
command: commandWords[0],
args: ""
};
if (commandWords.length > 1) {
const argWords = commandWords.slice(1, MAX_ARGS + 1);
payload.args = argWords.join(" ");
// Parse named and unnamed args
var unnamedCount = 1;
var unnamedArgs = [];
for (var argWord of argWords) {
if (namedArgs && namedArgPattern.test(argWord)) {
const { groups: { name, value } } = namedArgPattern.exec(argWord);
payload[`${name}`] = value;
} else {
unnamedArgs.push(argWord)
payload[`arg${unnamedCount}`] = argWord;
unnamedCount += 1;
}
}
// Add a string of only the unnamed args
if (namedArgs && unnamedArgs.length > 0) {
payload["unnamed_args"] = unnamedArgs.join(" ");
}
}
return payload;
}
module.exports = {
MAX_ARGS,
commandDefaults,
getInputs,
getCommandsConfig,
getCommandsConfigFromInputs,
getCommandsConfigFromJson,
configIsValid,
actorHasPermission,
getActorPermission,
addReaction,
getSlashCommandPayload
};
-202
View File
@@ -1,202 +0,0 @@
const {
commandDefaults,
getCommandsConfigFromInputs,
getCommandsConfigFromJson,
actorHasPermission,
configIsValid,
getSlashCommandPayload
} = require("./func");
test("building config with required inputs only", async () => {
const inputs = {
commands: "list, of, slash, commands"
};
const commands = inputs.commands.replace(/\s+/g, "").split(",");
const config = getCommandsConfigFromInputs(inputs);
expect(config.length).toEqual(4);
for (var i = 0; i < config.length; i++) {
expect(config[i].command).toEqual(commands[i]);
expect(config[i].permission).toEqual(commandDefaults.permission);
expect(config[i].issue_type).toEqual(commandDefaults.issue_type);
expect(config[i].allow_edits).toEqual(commandDefaults.allow_edits);
expect(config[i].repository).toEqual(commandDefaults.repository);
expect(config[i].event_type_suffix).toEqual(
commandDefaults.event_type_suffix
);
expect(config[i].named_args).toEqual(commandDefaults.named_args);
}
});
test("building config with optional inputs", async () => {
const inputs = {
commands: "list, of, slash, commands",
permission: "admin",
issueType: "pull-request",
allowEdits: true,
repository: "owner/repo",
eventTypeSuffix: "-cmd",
namedArgs: true
};
const commands = inputs.commands.replace(/\s+/g, "").split(",");
const config = getCommandsConfigFromInputs(inputs);
expect(config.length).toEqual(4);
for (var i = 0; i < config.length; i++) {
expect(config[i].command).toEqual(commands[i]);
expect(config[i].permission).toEqual(inputs.permission);
expect(config[i].issue_type).toEqual(inputs.issueType);
expect(config[i].allow_edits).toEqual(inputs.allowEdits);
expect(config[i].repository).toEqual(inputs.repository);
expect(config[i].event_type_suffix).toEqual(inputs.eventTypeSuffix);
expect(config[i].named_args).toEqual(inputs.namedArgs);
}
});
test("building config with required JSON only", async () => {
const json = `[
{
"command": "do-stuff"
},
{
"command": "test-all-the-things"
}
]`;
const commands = ["do-stuff", "test-all-the-things"];
const config = getCommandsConfigFromJson(json);
expect(config.length).toEqual(2);
for (var i = 0; i < config.length; i++) {
expect(config[i].command).toEqual(commands[i]);
expect(config[i].permission).toEqual(commandDefaults.permission);
expect(config[i].issue_type).toEqual(commandDefaults.issue_type);
expect(config[i].allow_edits).toEqual(commandDefaults.allow_edits);
expect(config[i].repository).toEqual(commandDefaults.repository);
expect(config[i].event_type_suffix).toEqual(
commandDefaults.event_type_suffix
);
expect(config[i].named_args).toEqual(commandDefaults.named_args);
}
});
test("building config with optional JSON properties", async () => {
const json = `[
{
"command": "do-stuff",
"permission": "admin",
"issue_type": "pull-request",
"allow_edits": true,
"repository": "owner/repo",
"event_type_suffix": "-cmd",
"named_args": true
},
{
"command": "test-all-the-things",
"permission": "read"
}
]`;
const commands = ["do-stuff", "test-all-the-things"];
const config = getCommandsConfigFromJson(json);
expect(config.length).toEqual(2);
expect(config[0].command).toEqual(commands[0]);
expect(config[0].permission).toEqual("admin");
expect(config[0].issue_type).toEqual("pull-request");
expect(config[0].allow_edits).toBeTruthy();
expect(config[0].repository).toEqual("owner/repo");
expect(config[0].event_type_suffix).toEqual("-cmd");
expect(config[0].named_args).toBeTruthy();
expect(config[1].command).toEqual(commands[1]);
expect(config[1].permission).toEqual("read");
expect(config[1].issue_type).toEqual(commandDefaults.issue_type);
});
test("valid config", async () => {
const config = [
{
command: "test",
permission: "write",
issue_type: "both",
repository: "peter-evans/slash-command-dispatch",
event_type_suffix: "-command"
}
];
expect(configIsValid(config)).toBeTruthy();
});
test("invalid permission level in config", async () => {
const config = [
{
command: "test",
permission: "test-case-invalid-permission",
issue_type: "both",
repository: "peter-evans/slash-command-dispatch",
event_type_suffix: "-command"
}
];
expect(configIsValid(config)).toBeFalsy();
});
test("invalid issue type in config", async () => {
const config = [
{
command: "test",
permission: "write",
issue_type: "test-case-invalid-issue-type",
repository: "peter-evans/slash-command-dispatch",
event_type_suffix: "-command"
}
];
expect(configIsValid(config)).toBeFalsy();
});
test("actor does not have permission", async () => {
expect(actorHasPermission("none", "read")).toBeFalsy();
expect(actorHasPermission("read", "write")).toBeFalsy();
expect(actorHasPermission("write", "admin")).toBeFalsy();
});
test("actor has permission", async () => {
expect(actorHasPermission("read", "none")).toBeTruthy();
expect(actorHasPermission("write", "read")).toBeTruthy();
expect(actorHasPermission("admin", "write")).toBeTruthy();
expect(actorHasPermission("write", "write")).toBeTruthy();
});
test("slash command payload", async () => {
commandWords = ["test", "arg1", "arg2", "arg3"];
payload = {
command: "test",
args: "arg1 arg2 arg3",
arg1: "arg1",
arg2: "arg2",
arg3: "arg3"
};
expect(getSlashCommandPayload(commandWords)).toEqual(payload);
});
test("slash command payload with named args", async () => {
commandWords = ["test", "branch=master", "arg1", "env=prod", "arg2"];
namedArgs = true;
payload = {
command: "test",
args: "branch=master arg1 env=prod arg2",
unnamed_args: "arg1 arg2",
branch: "master",
env: "prod",
arg1: "arg1",
arg2: "arg2"
};
expect(getSlashCommandPayload(commandWords, namedArgs)).toEqual(payload);
});
test("slash command payload with malformed named args", async () => {
commandWords = ["test", "branch=", "arg1", "e-nv=prod", "arg2"];
namedArgs = true;
payload = {
command: "test",
args: "branch= arg1 e-nv=prod arg2",
unnamed_args: "branch= arg1 e-nv=prod arg2",
arg1: "branch=",
arg2: "arg1",
arg3: "e-nv=prod",
arg4: "arg2"
};
expect(getSlashCommandPayload(commandWords, namedArgs)).toEqual(payload);
});
+89 -83
View File
@@ -1,7 +1,7 @@
const { inspect } = require("util");
const core = require("@actions/core");
const github = require("@actions/github");
const {
import * as core from '@actions/core'
import * as github from '@actions/github'
import {inspect} from 'util'
import {
getInputs,
getCommandsConfig,
configIsValid,
@@ -9,144 +9,150 @@ const {
getActorPermission,
addReaction,
getSlashCommandPayload
} = require("./func");
} from './command-helper'
import {Octokit} from './octokit-client'
async function run() {
async function run(): Promise<void> {
try {
// Check required context properties exist (satisfy type checking)
if (
!github.context.payload.action ||
!github.context.payload.issue ||
!github.context.payload.comment
) {
throw new Error('Required context properties are missing.')
}
// Only handle 'created' and 'edited' event types
if (!["created", "edited"].includes(github.context.payload.action)) {
if (!['created', 'edited'].includes(github.context.payload.action)) {
core.warning(
`Event type '${github.context.payload.action}' not supported.`
);
return;
)
return
}
// Get action inputs
const inputs = getInputs();
core.debug(`Inputs: ${inspect(inputs)}`);
const inputs = getInputs()
core.debug(`Inputs: ${inspect(inputs)}`)
// Check required inputs
if (!inputs.token) {
core.setFailed("Missing required input 'token'.");
return false;
throw new Error(`Missing required input 'token'.`)
}
// Get configuration for registered commands
const config = getCommandsConfig(inputs);
core.debug(`Commands config: ${inspect(config)}`);
const config = getCommandsConfig(inputs)
core.debug(`Commands config: ${inspect(config)}`)
// Check the config is valid
if (!configIsValid(config)) return;
if (!configIsValid(config)) return
// Get the comment body and id
const commentBody = github.context.payload["comment"]["body"];
const commentId = github.context.payload["comment"]["id"];
core.debug(`Comment body: ${commentBody}`);
core.debug(`Comment id: ${commentId}`);
const commentBody: string = github.context.payload.comment.body
const commentId: number = github.context.payload.comment.id
core.debug(`Comment body: ${commentBody}`)
core.debug(`Comment id: ${commentId}`)
// Check if the first line of the comment is a slash command
const firstLine = commentBody.split(/\r?\n/)[0].trim();
if (firstLine.length < 2 || firstLine.charAt(0) != "/") {
console.debug("The first line of the comment is not a valid slash command.");
return;
const firstLine = commentBody.split(/\r?\n/)[0].trim()
if (firstLine.length < 2 || firstLine.charAt(0) != '/') {
console.debug(
'The first line of the comment is not a valid slash command.'
)
return
}
// Split the first line into "words"
const commandWords = firstLine.slice(1).split(" ");
core.debug(`Command words: ${inspect(commandWords)}`);
const commandWords = firstLine.slice(1).split(' ')
core.debug(`Command words: ${inspect(commandWords)}`)
// Check if the command is registered for dispatch
var configMatches = config.filter(function(cmd) {
return cmd.command == commandWords[0];
});
core.debug(`Config matches on 'command': ${inspect(configMatches)}`);
let configMatches = config.filter(function (cmd) {
return cmd.command == commandWords[0]
})
core.debug(`Config matches on 'command': ${inspect(configMatches)}`)
if (configMatches.length == 0) {
core.info(`Command '${commandWords[0]}' is not registered for dispatch.`);
return;
core.info(`Command '${commandWords[0]}' is not registered for dispatch.`)
return
}
// Filter matching commands by issue type
const isPullRequest = "pull_request" in github.context.payload.issue;
configMatches = configMatches.filter(function(cmd) {
const isPullRequest = 'pull_request' in github.context.payload.issue
configMatches = configMatches.filter(function (cmd) {
return (
cmd.issue_type == "both" ||
(cmd.issue_type == "issue" && !isPullRequest) ||
(cmd.issue_type == "pull-request" && isPullRequest)
);
});
core.debug(`Config matches on 'issue_type': ${inspect(configMatches)}`);
cmd.issue_type == 'both' ||
(cmd.issue_type == 'issue' && !isPullRequest) ||
(cmd.issue_type == 'pull-request' && isPullRequest)
)
})
core.debug(`Config matches on 'issue_type': ${inspect(configMatches)}`)
if (configMatches.length == 0) {
const issueType = isPullRequest ? "pull request" : "issue";
const issueType = isPullRequest ? 'pull request' : 'issue'
core.info(
`Command '${commandWords[0]}' is not configured for the issue type '${issueType}'.`
);
return;
)
return
}
// Filter matching commands by whether or not to allow edits
if (github.context.payload.action == "edited") {
configMatches = configMatches.filter(function(cmd) {
return cmd.allow_edits;
});
core.debug(`Config matches on 'allow_edits': ${inspect(configMatches)}`);
if (github.context.payload.action == 'edited') {
configMatches = configMatches.filter(function (cmd) {
return cmd.allow_edits
})
core.debug(`Config matches on 'allow_edits': ${inspect(configMatches)}`)
if (configMatches.length == 0) {
core.info(
`Command '${commandWords[0]}' is not configured to allow edits.`
);
return;
)
return
}
}
// Set octokit clients
const octokit = new github.GitHub(inputs.token);
const reactionOctokit = new github.GitHub(inputs.reactionToken)
const octokit = new Octokit({auth: `${inputs.token}`})
const reactionOctokit = new Octokit({auth: `${inputs.reactionToken}`})
// At this point we know the command is registered
// Add the "eyes" reaction to the comment
if (inputs.reactions)
await addReaction(
reactionOctokit,
github.context.repo,
commentId,
"eyes"
);
await addReaction(reactionOctokit, github.context.repo, commentId, 'eyes')
// Get the actor permission
const actorPermission = await getActorPermission(
octokit,
github.context.repo,
github.context.actor
);
core.debug(`Actor permission: ${actorPermission}`);
)
core.debug(`Actor permission: ${actorPermission}`)
// Filter matching commands by the user's permission level
configMatches = configMatches.filter(function(cmd) {
return actorHasPermission(actorPermission, cmd.permission);
});
core.debug(`Config matches on 'permission': ${inspect(configMatches)}`);
configMatches = configMatches.filter(function (cmd) {
return actorHasPermission(actorPermission, cmd.permission)
})
core.debug(`Config matches on 'permission': ${inspect(configMatches)}`)
if (configMatches.length == 0) {
core.info(
`Command '${commandWords[0]}' is not configured for the user's permission level '${actorPermission}'.`
);
return;
)
return
}
// Determined that the command should be dispatched
core.info(`Command '${commandWords[0]}' to be dispatched.`);
core.info(`Command '${commandWords[0]}' to be dispatched.`)
// Define payload
var clientPayload = {
const clientPayload = {
slash_command: {},
github: github.context
};
}
// Get the pull request context for the dispatch payload
if (isPullRequest) {
const { data: pullRequest } = await octokit.pulls.get({
const {data: pullRequest} = await octokit.pulls.get({
...github.context.repo,
pull_number: github.context.payload.issue.number
});
clientPayload["pull_request"] = pullRequest;
})
clientPayload['pull_request'] = pullRequest
}
// Dispatch for each matching configuration
@@ -155,23 +161,23 @@ async function run() {
clientPayload.slash_command = getSlashCommandPayload(
commandWords,
cmd.named_args
);
)
core.debug(
`Slash command payload: ${inspect(clientPayload.slash_command)}`
);
)
// Dispatch the command
const dispatchRepo = cmd.repository.split("/");
const eventType = cmd.command + cmd.event_type_suffix;
const dispatchRepo = cmd.repository.split('/')
const eventType = cmd.command + cmd.event_type_suffix
await octokit.repos.createDispatchEvent({
owner: dispatchRepo[0],
repo: dispatchRepo[1],
event_type: eventType,
client_payload: clientPayload
});
})
core.info(
`Command '${cmd.command}' dispatched to '${cmd.repository}' ` +
`with event type '${eventType}'.`
);
)
}
// Add the "rocket" reaction to the comment
@@ -180,12 +186,12 @@ async function run() {
reactionOctokit,
github.context.repo,
commentId,
"rocket"
);
'rocket'
)
} catch (error) {
core.debug(inspect(error));
core.setFailed(error.message);
core.debug(inspect(error))
core.setFailed(error.message)
}
}
run();
run()
+7
View File
@@ -0,0 +1,7 @@
import {Octokit as Core} from '@octokit/core'
import {paginateRest} from '@octokit/plugin-paginate-rest'
import {restEndpointMethods} from '@octokit/plugin-rest-endpoint-methods'
export {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods'
export {OctokitOptions} from '@octokit/core/dist-types/types'
export const Octokit = Core.plugin(paginateRest, restEndpointMethods)
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": [
"es6"
],
"outDir": "./lib",
"rootDir": "./src",
"declaration": true,
"strict": true,
"noImplicitAny": false,
"esModuleInterop": true
},
"exclude": ["__test__", "lib", "node_modules"]
}