Skip to content

Commit b117a45

Browse files
authored
feat: initial version (#1)
1 parent 5a38633 commit b117a45

10 files changed

+6396
-0
lines changed

.github/dependabot.yml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "npm"
4+
directory: "/"
5+
schedule:
6+
interval: "daily"
7+
# https://docs.github.com/en/github/administering-a-repository/keeping-your-actions-up-to-date-with-github-dependabot
8+
- package-ecosystem: "npm"
9+
directory: "/"
10+
schedule:
11+
interval: "daily"

.github/workflows/release.yml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Release
2+
on:
3+
push:
4+
branches:
5+
- main
6+
- next
7+
- beta
8+
- "*.x" # maintenance releases
9+
10+
jobs:
11+
release:
12+
name: release
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v2
16+
- uses: actions/setup-node@v1
17+
- run: npm ci
18+
- run: npx semantic-release
19+
env:
20+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

bin/mutate-github-repositories.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env node
2+
3+
const yargs = require("yargs");
4+
5+
const findGitHubReleases = require("..");
6+
7+
var argv = yargs
8+
.usage("Usage: $0 [options] [script] [repos...]")
9+
.example(
10+
"$0 --token 0123456789012345678901234567890123456789 octokit/rest.js"
11+
)
12+
.command("$0 [script] [repos...]", "", (yargs) => {
13+
yargs.positional("script", {
14+
demandOption: true,
15+
describe: "Path to your *.js script",
16+
});
17+
yargs.positional("repos", {
18+
demandOption: true,
19+
describe: "One or multiple arrays in the form of 'repo-owner/repo-name'",
20+
default: [],
21+
});
22+
})
23+
.option("token", {
24+
description:
25+
'Requires the "public_repo" scope for public repositories, "repo" scope for private repositories.',
26+
demandOption: true,
27+
type: "string",
28+
})
29+
.option("cache", {
30+
description: "Cache responses for debugging",
31+
type: "boolean",
32+
default: false,
33+
})
34+
.epilog("copyright 2020").argv;
35+
36+
const { _, $0, ...options } = argv;
37+
findGitHubReleases(options).catch(console.error);

example.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module.exports = exampleScript;
2+
3+
/**
4+
* Example script that stars or unstars the passed repository based on the `--unstar` CLI option
5+
*
6+
* @param {import('@octokit/core').Octokit} octokit
7+
* @param {object} repository Repository object as returned by the GitHub API, see https://docs.github.com/en/rest/reference/repos#get-a-repository
8+
* @param {object} options Custom user options passed to the CLI
9+
*/
10+
async function exampleScript(octokit, repository, options) {
11+
const method = options.unstar ? "DELETE" : "PUT";
12+
13+
// https://docs.github.com/en/rest/reference/activity#star-a-repository-for-the-authenticated-user
14+
// https://docs.github.com/en/rest/reference/activity#unstar-a-repository-for-the-authenticated-user
15+
await octokit.request("/user/starred/{owner}/{repo}", {
16+
method,
17+
owner: repository.owner.login,
18+
repo: repository.name,
19+
});
20+
}

index.js

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
module.exports = mutateGithubRepositories;
2+
3+
const { resolve } = require("path");
4+
5+
const { Octokit: OctokitCore } = require("@octokit/core");
6+
const { paginateRest } = require("@octokit/plugin-paginate-rest");
7+
const { throttling } = require("@octokit/plugin-throttling");
8+
const { retry } = require("@octokit/plugin-retry");
9+
10+
const { cache: octokitCachePlugin } = require("./lib/octokit-plugin-cache");
11+
const { resolveRepositories } = require("./lib/resolve-repositories");
12+
const { name, version } = require("./package.json");
13+
14+
const Octokit = OctokitCore.plugin(paginateRest, throttling, retry).defaults({
15+
log: console,
16+
userAgent: [name, version].join("/"),
17+
throttle: {
18+
onAbuseLimit: (error, options) => {
19+
octokit.log.error("onAbuseLimit", error, options);
20+
},
21+
onRateLimit: (error, options) => {
22+
octokit.log.error("onRateLimit", error, options);
23+
},
24+
},
25+
});
26+
27+
/**
28+
* Find all releases in a GitHub repository or organization after a specified date
29+
*
30+
* @param {object} options
31+
* @param {string} options.token Personal Access Token: Requires the "public_repo" scope for public repositories, "repo" scope for private repositories.
32+
* @param {string} options.script Path to script to run against a repository
33+
* @param {string} options.repos Array of repository names in the form of "repo-owner/repo-name". To match all repositories for an owner, pass "repo-owner/*"
34+
* @param {boolean} options.cache Cache responses for debugging
35+
*/
36+
async function mutateGithubRepositories(
37+
options = {
38+
cache: false,
39+
repos: [],
40+
}
41+
) {
42+
const { token, script, repos, cache, ...userOptions } = options;
43+
const MyOctokit = cache ? Octokit.plugin(octokitCachePlugin) : Octokit;
44+
const octokit = new MyOctokit({
45+
auth: token,
46+
});
47+
48+
let userScript;
49+
try {
50+
userScript = require(resolve(process.cwd(), script));
51+
} catch (error) {
52+
throw new Error(
53+
`[mutate-github-repositories] ${script} script could not be found`
54+
);
55+
}
56+
57+
if (repos.length === 0) {
58+
throw new Error("[mutate-github-repositories] No repositories provided");
59+
}
60+
61+
state = {
62+
log: console,
63+
octokit,
64+
};
65+
66+
const repositories = await resolveRepositories(state, repos);
67+
68+
for (const repository of repositories) {
69+
console.log("Running %s on %s...", script, repository.full_name);
70+
await userScript(octokit, repository, userOptions);
71+
}
72+
73+
console.log("\ndone.");
74+
}

lib/octokit-plugin-cache.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module.exports = { cache };
2+
3+
const { URL } = require("url");
4+
const { dirname } = require("path");
5+
6+
const mkdirp = require("mkdirp");
7+
const { readFileSync, writeFileSync } = require("jsonfile");
8+
9+
function cache(octokit) {
10+
octokit.hook.wrap("request", async (request, options) => {
11+
if (options.method !== "GET") {
12+
return request(options);
13+
}
14+
15+
const { url } = octokit.request.endpoint.parse(options);
16+
17+
const { pathname, searchParams } = new URL(url);
18+
const page = searchParams.get("page");
19+
const cachePath = `./cache${pathname}${page ? `-page-${page}` : ""}.json`;
20+
21+
try {
22+
return readFileSync(cachePath);
23+
} catch (error) {
24+
const response = await request(options);
25+
26+
mkdirp.sync(dirname(cachePath));
27+
writeFileSync(cachePath, response);
28+
return response;
29+
}
30+
});
31+
}

lib/resolve-repositories.js

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
module.exports = { resolveRepositories };
2+
3+
async function resolveRepositories(state, repositories) {
4+
const invalidRepositories = repositories.filter((fullName) => {
5+
/^[a-z0-9_.-]+\/([a-z0-9_.-]+|\*)$/i.test(fullName);
6+
});
7+
8+
if (invalidRepositories.length) {
9+
throw new Error(
10+
"[mutate-github-repositories] Invalid repositories: %o",
11+
invalidRepositories
12+
);
13+
}
14+
15+
const repositoriesWithStars = repositories.filter((fullName) => {
16+
return /^[a-z0-9_.-]+\/\*$/i.test(fullName);
17+
});
18+
19+
const repositoriesWithoutStars = repositories.filter((fullName) => {
20+
return !/^[a-z0-9_.-]+\/\*$/i.test(fullName);
21+
});
22+
const resolvedRepositories = [];
23+
24+
for (const name of repositoriesWithoutStars) {
25+
const [owner, repo] = name.split("/");
26+
const { data } = await state.octokit.request("/repos/{owner}/{repo}", {
27+
owner,
28+
repo,
29+
});
30+
resolvedRepositories.push(data);
31+
}
32+
33+
for (const name of repositoriesWithStars) {
34+
const owner = name.split("/")[0];
35+
const isOrg = await state.octokit
36+
.request("HEAD /orgs/:org", {
37+
org: owner,
38+
})
39+
.then(
40+
() => true,
41+
() => false
42+
);
43+
const paginateReposRoute = isOrg
44+
? // https://docs.github.com/en/rest/reference/repos#list-organization-repositories
45+
"GET /orgs/{owner}/repos"
46+
: // https://docs.github.com/en/rest/reference/repos#list-repositories-for-a-user
47+
"GET /users/{owner}/repos";
48+
49+
for await (const response of state.octokit.paginate.iterator(
50+
paginateReposRoute,
51+
{
52+
owner,
53+
per_page: 100,
54+
}
55+
)) {
56+
resolvedRepositories.push(...response.data);
57+
}
58+
}
59+
60+
// return unique array (https://www.samanthaming.com/tidbits/43-3-ways-to-remove-array-duplicates/)
61+
return [...new Set(resolvedRepositories)];
62+
}

0 commit comments

Comments
 (0)