|
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(); |
|
} |