Skip to content

Instantly share code, notes, and snippets.

@eai04191
Last active November 27, 2024 03:45
Show Gist options
  • Save eai04191/c6760b3a3c827f1cad57bb8a94048e94 to your computer and use it in GitHub Desktop.
Save eai04191/c6760b3a3c827f1cad57bb8a94048e94 to your computer and use it in GitHub Desktop.

GitHub Organization Outside Collaborators Report Generator

This script generates a Markdown report of all outside collaborators with access to private repositories in a GitHub organization.

Features

  • Lists all outside collaborators in your organization
  • Shows which private repositories each collaborator has access to
  • Displays permission levels (read/write/admin)
  • Creates clickable links to repositories and user profiles
  • Outputs report as a clean Markdown file

Prerequisites

  • Deno installed
  • GitHub CLI installed and authenticated
  • Appropriate GitHub permissions to view organization members and repository collaborators

Installation

  1. Save the script as github-outside-collaborators.ts
  2. Ensure you're logged in to GitHub CLI:
gh auth login

Usage

deno run --allow-net --allow-run --allow-write github-outside-collaborators.ts <organization-name>

The script will generate a Markdown file named outside-collaborators-{org}-{date}.md containing a formatted report.

Output Format

The generated report includes:

  • Generation timestamp
  • List of outside collaborators
  • Links to user profiles and repositories
  • Table of repositories and permissions for each user

Example output:

# Outside Collaborators Report for my-organization

Generated at: 2024-11-27T10:00:00.000Z

## Collaborators

### [<img src="https://avatars.githubusercontent.com/u/0000000?v=4" width="32" height="32" alt="username's avatar"> username](https://github.com/orgs/my-organization/people/username)

| Repository | Permission |
|------------|------------|
| [repo-name](https://github.com/my-organization/repo-name) | write |

Permissions Required

The GitHub token used (via GitHub CLI) needs the following permissions:

  • read:org
  • repo

Note

This script only checks private repositories as public repositories are accessible to everyone.

License

MIT


Created with 💖 by Claude
import { encode } from "https://deno.land/[email protected]/encoding/base64.ts";
const GITHUB_API_URL = "https://api.github.com";
// GitHubのTokenを環境変数から取得
const getGitHubToken = async (): Promise<string> => {
const process = new Deno.Command("gh", {
args: ["auth", "token"],
});
const { code, stdout, stderr } = await process.output();
if (code !== 0) {
console.error(new TextDecoder().decode(stderr));
throw new Error("Failed to get GitHub token");
}
return new TextDecoder().decode(stdout).trim();
};
// 組織のprivateリポジトリを取得
async function getOrgPrivateRepos(org: string, token: string): Promise<any[]> {
const repos = [];
let page = 1;
while (true) {
const response = await fetch(
`${GITHUB_API_URL}/orgs/${org}/repos?type=private&per_page=100&page=${page}`,
{
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github.v3+json",
},
}
);
if (!response.ok) {
throw new Error(
`Failed to fetch repositories: ${response.statusText}`
);
}
const data = await response.json();
if (data.length === 0) break;
repos.push(...data);
page++;
}
return repos;
}
interface Collaborator {
login: string;
avatar_url: string;
permissions: {
admin: boolean;
maintain: boolean;
push: boolean;
triage: boolean;
pull: boolean;
};
}
// リポジトリのoutside collaboratorを取得
async function getRepoOutsideCollaborators(
org: string,
repo: string,
token: string
): Promise<Collaborator[]> {
const response = await fetch(
`${GITHUB_API_URL}/repos/${org}/${repo}/collaborators?affiliation=outside&per_page=100`,
{
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github.v3+json",
},
}
);
if (!response.ok) {
throw new Error(
`Failed to fetch collaborators for ${repo}: ${response.statusText}`
);
}
return await response.json();
}
interface UserPermission {
repo: string;
permission: string;
}
interface UserInfo {
avatar_url: string;
permissions: UserPermission[];
}
function generateMarkdown(
org: string,
userPermissions: Record<string, UserInfo>
) {
const lines = [
`# Outside Collaborators Report for ${org}`,
"",
`Generated at: ${new Date().toISOString()}`,
"",
"## Collaborators",
"",
];
const users = Object.keys(userPermissions).sort();
for (const user of users) {
const userInfo = userPermissions[user];
const icon = `<img src="${userInfo.avatar_url}" width="32" height="32" alt="${user}'s avatar">`;
lines.push(
`### [${icon} ${user}](https://github.com/orgs/${org}/people/${user})`
);
lines.push("");
const repos = userInfo.permissions.sort((a, b) =>
a.repo.localeCompare(b.repo)
);
if (repos.length === 0) {
lines.push("No private repository access found");
lines.push("");
continue;
}
lines.push("| Repository | Permission |");
lines.push("|------------|------------|");
for (const repo of repos) {
lines.push(
`| [${repo.repo}](https://github.com/${org}/${repo.repo}) | ${repo.permission} |`
);
}
lines.push("");
}
return lines.join("\n");
}
async function main() {
try {
const org = Deno.args[0];
if (!org) {
console.error("Please provide an organization name as an argument");
Deno.exit(1);
}
const token = await getGitHubToken();
console.log("Fetching private repositories...");
const privateRepos = await getOrgPrivateRepos(org, token);
console.log(`Found ${privateRepos.length} private repositories`);
// ユーザーごとのリポジトリアクセス権限を格納
const userPermissions: Record<string, UserInfo> = {};
// 各リポジトリのoutside collaboratorを取得
for (const repo of privateRepos) {
console.log(`Checking collaborators for ${repo.name}...`);
const collaborators = await getRepoOutsideCollaborators(
org,
repo.name,
token
);
for (const collab of collaborators) {
if (!userPermissions[collab.login]) {
userPermissions[collab.login] = {
avatar_url: collab.avatar_url,
permissions: [],
};
}
userPermissions[collab.login].permissions.push({
repo: repo.name,
permission: collab.permissions.admin
? "admin"
: collab.permissions.maintain
? "maintain"
: collab.permissions.push
? "write"
: "read",
});
}
}
// Markdownファイルを生成
const markdown = generateMarkdown(org, userPermissions);
const filename = `outside-collaborators-${org}-${
new Date().toISOString().split("T")[0]
}.md`;
await Deno.writeTextFile(filename, markdown);
console.log(`\nReport generated: ${filename}`);
} catch (error) {
console.error("Error:", error.message);
Deno.exit(1);
}
}
if (import.meta.main) {
main();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment