Skip to content
This repository has been archived by the owner on Jul 10, 2024. It is now read-only.

Commit

Permalink
Added schema validation (#11)
Browse files Browse the repository at this point in the history
Added logic to read all the commands available in a directory and
validate them against a schema.

The schema is simple, but it will be slowly growing.

Resolves #7 and resolves #10
  • Loading branch information
Bullrich committed Apr 17, 2024
1 parent f8e8fd1 commit 88a8e05
Show file tree
Hide file tree
Showing 13 changed files with 580 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .github/scripts/example/example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: Example command
description: This command is just used for examples
commandStart: 'echo "Hello World!"'
15 changes: 15 additions & 0 deletions .github/workflows/cmd-action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Command-Action

on:
pull_request:

jobs:
cmd-check:
runs-on: ubuntu-latest
name: Bot
steps:
- uses: actions/checkout@v4
- uses: paritytech/cmd-action@main
with:
commands-directory: '.github/scripts'
GITHUB_TOKEN: ${{ github.token }}
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
# we may forget to set it back to main
- name: Validate that action points to main branch
run: |
BRANCH=$(yq '.jobs.review-approvals.steps[2].uses' $FILE_NAME | cut -d "@" -f2)
BRANCH=$(yq '.jobs.cmd-check.steps[1].uses' $FILE_NAME | cut -d "@" -f2)
# If the branch is not the main branch
if [ "$BRANCH" != "$GITHUB_BASE_REF" ]; then
echo "Action points to $BRANCH. It has to point to $GITHUB_BASE_REF instead!"
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ inputs:
GITHUB_TOKEN:
required: true
description: The token to access the repo
commands-directory:
required: false
description: The directory where the command scripts are available
default: "./github/commands"
outputs:
repo:
description: 'The name of the repo in owner/repo pattern'
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^5.1.1",
"@octokit/webhooks-types": "^7.3.1"
"@eng-automation/integrations": "^4.3.0",
"@octokit/webhooks-types": "^7.3.1",
"joi": "^17.12.3",
"yaml": "^2.4.1"
},
"devDependencies": {
"@eng-automation/js-style": "^2.3.0",
Expand Down
35 changes: 35 additions & 0 deletions src/commander.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { readFile } from "fs/promises";
import { parse } from "yaml";

import { ActionLogger } from "./github/types";
import { Command } from "./schema/command";
import { validateConfig } from "./schema/validator";
import { findFilesWithExtension } from "./util";

/** The 'commander' of the command actions */
export class Commander {
private commands?: Command[];
constructor(
private readonly scriptsDiretory: string,
private readonly logger: ActionLogger,
) {}

/** Get all the commands from a specific directory and validates them */
async getCommands(): Promise<Command[]> {
if (this.commands) {
return this.commands;
}
const files = await findFilesWithExtension(this.scriptsDiretory, "yml");
const commands: Command[] = [];
for (const file of files) {
const content = await readFile(file, "utf-8");
const command = parse(content) as Command;
this.logger.info(`Parsing ${file}`);
validateConfig(command);
commands.push(command);
}

this.commands = commands;
return commands;
}
}
24 changes: 22 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { getInput, info, setOutput } from "@actions/core";
import { getInput, info, setFailed, setOutput } from "@actions/core";
import { context, getOctokit } from "@actions/github";
import { Context } from "@actions/github/lib/context";
import { PullRequest } from "@octokit/webhooks-types";

import { Commander } from "./commander";
import { PullRequestApi } from "./github/pullRequest";
import { generateCoreLogger } from "./util";

Expand All @@ -23,9 +24,28 @@ const getRepo = (ctx: Context) => {
const repo = getRepo(context);

setOutput("repo", `${repo.owner}/${repo.repo}`);
const scripts =
getInput("commands-directory", { required: false }) ?? "./github/commands";

const logger = generateCoreLogger();
const commander = new Commander(scripts, logger);

// Handle both pull_request and pull_request_target
if (context.eventName.includes("pull_request")) {
commander
.getCommands()
.then((commands) =>
logger.info(
`Found ${commands.length} valid commands: ${commands
.map(({ name }) => name)
.join(", ")}`,
),
)
.catch(setFailed);
}

const token = getInput("GITHUB_TOKEN", { required: true });
if (context.payload.pull_request) {
const token = getInput("GITHUB_TOKEN", { required: true });
const api = new PullRequestApi(getOctokit(token), generateCoreLogger());
const author = api.getPrAuthor(context.payload.pull_request as PullRequest);
info("Author of the PR is " + author);
Expand Down
7 changes: 7 additions & 0 deletions src/schema/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Command {
name: string;
description?: string;
machine?: string[];
timeout?: number;
commandStart: string;
}
17 changes: 17 additions & 0 deletions src/schema/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { validate } from "@eng-automation/js";
import Joi from "joi";

import { Command } from "./command";

const commandSchema = Joi.object<Command>().keys({
name: Joi.string().required(),
description: Joi.string().optional(),
machine: Joi.array().items(Joi.string()).optional(),
timeout: Joi.number().min(1).optional(),
commandStart: Joi.string().required(),
});

export const validateConfig = (config: Command): Command | never =>
validate<Command>(config, commandSchema, {
message: "Command file is invalid",
});
7 changes: 7 additions & 0 deletions src/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { findFilesWithExtension } from "../util";

test("adds 1 + 2 to equal 3", () => {
expect(1 + 2).toBe(3);
});

test("find files", async () => {
const files = await findFilesWithExtension(".github/scripts", "yml");
expect(files.length).toBeGreaterThan(0);
});
21 changes: 21 additions & 0 deletions src/test/validator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Command } from "../schema/command";
import { validateConfig } from "../schema/validator";

test("test good command", () => {
const goodCommand: Command = {
name: "Hi",
timeout: 10,
commandStart: "./bin",
};
validateConfig(goodCommand);
});

test("test bad command", () => {
const badCommand = {
timeout: -10,
commandStart: "./bin",
};
expect(() => validateConfig(badCommand as Command)).toThrow(
"Command file is invalid",
);
});
20 changes: 20 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
import { debug, error, info, warning } from "@actions/core";
import fs from "fs/promises";
import { resolve } from "path";

import { ActionLogger } from "./github/types";

export function generateCoreLogger(): ActionLogger {
return { info, debug, warn: warning, error };
}

export async function findFilesWithExtension(
dir: string,
ext: string,
): Promise<string[]> {
const files: string[] = [];
const dirents = await fs.readdir(dir, { withFileTypes: true });
for (const dirent of dirents) {
const res = resolve(dir, dirent.name);
if (dirent.isDirectory()) {
files.push(...(await findFilesWithExtension(res, ext)));
} else if (res.endsWith(ext)) {
files.push(res);
}
}

return files;
}
Loading

0 comments on commit 88a8e05

Please sign in to comment.