Skip to content

feat(programmatic-interface): export a function to enable running an octoherd script against a repository list #221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import enquirer from "enquirer";
import { cache as octokitCachePlugin } from "./lib/octokit-plugin-cache.js";
import { requestLog } from "./lib/octokit-plugin-request-log.js";
import { requestConfirm } from "./lib/octokit-plugin-request-confirm.js";
import { runScriptAgainstRepositories } from "./lib/run-script-against-repositories.js";
import { loadRepositoriesAndRunScript } from "./lib/load-repositories-and-run-script.js";
import { VERSION } from "./version.js";

export { Octokit } from "@octoherd/octokit";
export { runScriptAgainstRepositories } from "./lib/run-script-against-repositories.js";

const levelColor = {
debug: chalk.bgGray.black,
Expand Down Expand Up @@ -113,7 +114,7 @@ export async function octoherd(options) {
octoherdReposPassedAsFlag: !!octoherdRepos,
};

await runScriptAgainstRepositories(state, octoherdRepos);
await loadRepositoriesAndRunScript(state, octoherdRepos);

console.log("");
console.log(chalk.gray("-".repeat(80)));
Expand Down
62 changes: 62 additions & 0 deletions lib/load-repositories-and-run-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import enquirer from "enquirer";
import chalk from "chalk";

import { resolveRepositories } from "./resolve-repositories.js";
import { runScriptAgainstRepositories } from "./run-script-against-repositories.js";

export async function loadRepositoriesAndRunScript(state, octoherdRepos = []) {
if (!state.octoherdReposPassedAsFlag) {
console.log("");
const prompt = new enquirer.List({
message: "Enter repositories",
separator: / +/,
hint:
"e.g. octoherd/cli. Use a * to load all repositories for an owner, e.g. octoherd/*. Enter nothing to exit",
validate(input) {
const values = typeof input === "string" ? [input] : input;

const invalid = values.find((value) => {
if (value.trim() === "*") return;

if (/^!?[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.*-]+$/.test(value.trim())) {
return;
}

return true;
});

if (!invalid) return true;

return (
chalk.red(`"${invalid}" is not a valid repository name.`) +
chalk.gray(" The format is <owner>/<repo>")
);
},
});

octoherdRepos = await prompt.run();

if (!state.reposNoticeShown) {
console.log(
`${chalk.gray(
"To avoid this prompt in future, pass repositories with --octoherd-repos or -R"
)}\n`
);
}
state.reposNoticeShown = true;
}

if (octoherdRepos.length === 0) return;

try {
state.octokit.log.info("Loading repositories ...");
const repositories = await resolveRepositories(state, octoherdRepos);

await runScriptAgainstRepositories(state.octokit, repositories, state.script, state.userOptions);
} catch (error) {
state.octokit.log.error(error);
process.exitCode = 1;
}

await loadRepositoriesAndRunScript(state);
}
89 changes: 17 additions & 72 deletions lib/run-script-against-repositories.js
Original file line number Diff line number Diff line change
@@ -1,76 +1,21 @@
import enquirer from "enquirer";
import chalk from "chalk";

import { resolveRepositories } from "./resolve-repositories.js";

export async function runScriptAgainstRepositories(state, octoherdRepos = []) {
if (!state.octoherdReposPassedAsFlag) {
console.log("");
const prompt = new enquirer.List({
message: "Enter repositories",
separator: / +/,
hint:
"e.g. octoherd/cli. Use a * to load all repositories for an owner, e.g. octoherd/*. Enter nothing to exit",
validate(input) {
const values = typeof input === "string" ? [input] : input;

const invalid = values.find((value) => {
if (value.trim() === "*") return;

if (/^!?[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.*-]+$/.test(value.trim())) {
return;
}

return true;
});

if (!invalid) return true;

return (
chalk.red(`"${invalid}" is not a valid repository name.`) +
chalk.gray(" The format is <owner>/<repo>")
);
},
});

octoherdRepos = await prompt.run();

if (!state.reposNoticeShown) {
console.log(
`${chalk.gray(
"To avoid this prompt in future, pass repositories with --octoherd-repos or -R"
)}\n`
);
}
state.reposNoticeShown = true;
}

if (octoherdRepos.length === 0) return;

try {
state.octokit.log.info("Loading repositories ...");
const repositories = await resolveRepositories(state, octoherdRepos);

for (const repository of repositories) {
state.octokit.log.info(
{ octoherd: true },
"Running on %s ...",
repository.full_name
);

try {
const { id, owner, name } = repository;
state.octokit.log.setContext({ repository: { id, owner, name } });
await state.script(state.octokit, repository, state.userOptions);
} catch (error) {
if (!error.cancel) throw error;
state.octokit.log.debug(error.message);
export async function runScriptAgainstRepositories(octokit, repositories, script, options) {
for (const repository of repositories) {
octokit.log.info(
{ octoherd: true },
"Running on %s ...",
repository.full_name
);

try {
if (octokit.log.setContext) {
const {id, owner, name} = repository;
octokit.log.setContext({repository: {id, owner, name}});
}

await script(octokit, repository, options);
} catch (error) {
if (!error.cancel) throw error;
octokit.log.debug(error.message);
}
} catch (error) {
state.octokit.log.error(error);
process.exitCode = 1;
}

await runScriptAgainstRepositories(state);
}
69 changes: 69 additions & 0 deletions tests/run-script-against-resolved-repositories.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { suite } from "uvu";
import { equal } from "uvu/assert";
import simple from "simple-mock";

import { runScriptAgainstRepositories } from "../lib/run-script-against-repositories.js";

const runAgainstRepos = suite('run script against repositories');

const userOptions = {};
const octoherdCliRepository = {id: 123, owner: {login: 'octoherd'}, name: 'cli', full_name: 'octoherd/cli'};

runAgainstRepos('run the script against the provided repositories', async () => {
const octokitInfoLogger = simple.spy(() => undefined);
const octokit = {log: {info: octokitInfoLogger}};
const octokitCoreRepository = {id: 456, owner: {login: 'octokit'}, name: 'core.js', full_name: 'octokit/core.js'};
const repositories = [octoherdCliRepository, octokitCoreRepository];
const script = simple.spy(() => undefined);

await runScriptAgainstRepositories(octokit, repositories, script, userOptions);

repositories.forEach((repository, index) => {
equal(script.calls[index].args, [octokit, repository, userOptions]);
equal(octokitInfoLogger.calls[index].args, [{octoherd: true}, "Running on %s ...", repository.full_name]);
});
});

runAgainstRepos('set logger context when available', async () => {
const octokitLoggerContextSetter = simple.spy(() => undefined);
const octokit = {log: {info: () => undefined, setContext: octokitLoggerContextSetter}};
const octokitCoreRepository = {id: 456, owner: {login: 'octokit'}, name: 'core.js', full_name: 'octokit/core.js'};
const repositories = [octoherdCliRepository, octokitCoreRepository];
const script = simple.spy(() => undefined);

await runScriptAgainstRepositories(octokit, repositories, script, userOptions);

repositories.forEach((repository, index) => equal(
octokitLoggerContextSetter.calls[index].args,
[{repository: {id: repository.id, owner: repository.owner, name: repository.name}}]
));
});

runAgainstRepos('log cancelled routes at debug level', async () => {
const repositories = [octoherdCliRepository];
const errorMessage = 'error message';
const cancelledRouteError = new Error(errorMessage);
cancelledRouteError.cancel = true;
const octokitDebugLogger = simple.spy(() => undefined);
const octokit = {log: {info: () => undefined, debug: octokitDebugLogger}};
const script = simple.spy(() => undefined).throwWith(cancelledRouteError);

await runScriptAgainstRepositories(octokit, repositories, script, userOptions);

equal(octokitDebugLogger.calls[0].args, [errorMessage]);
});

runAgainstRepos('throw other errors', async () => {
const repositories = [octoherdCliRepository];
const error = new Error();
const octokit = {log: {info: () => undefined}};
const script = simple.spy(() => undefined).throwWith(error);

try {
await runScriptAgainstRepositories(octokit, repositories, script, userOptions);
} catch (err) {
equal(err, error);
}
});

runAgainstRepos.run();