Skip to content

Instantly share code, notes, and snippets.

@sergeyklay
Last active November 6, 2024 12:43
Show Gist options
  • Save sergeyklay/fdcb742117339d622ccd928a570abc3b to your computer and use it in GitHub Desktop.
Save sergeyklay/fdcb742117339d622ccd928a570abc3b to your computer and use it in GitHub Desktop.

Find Repositories Without CODEOWNERS

This script fetches all repositories within a specified GitHub organization and identifies those without a CODEOWNERS file in repository root. It excludes archived/disabled repositories and forks.

Prerequisites

  1. Ensure you have Node.js installed.

  2. Install the required packages using npm:

    npm install axios dotenv

Environment Variables

Create a .env file in the root directory and define the following variables:

GITHUB_API_URL=github_api_base_url # GitHub API Base URL (optional, defaults to 'https://api.github.com')
GITHUB_TOKEN=your_github_token  # Your GitHub personal access token
GITHUB_ORG=your_organization_name  # The name of your GitHub organization

Usage

  1. Clone or copy this script to your project directory.

  2. Create .env file as described above

  3. Install dependencies as described above

  4. Run the script with Node.js:

    node codeowners.js
'use strict';
// Built-in modules
const fs = require('node:fs');
const path = require('node:path');
// External dependencies
const axios = require('axios');
// Load environment variables
require('dotenv').config();
// Constants
const GITHUB_API_URL = process.env.GITHUB_API_URL || 'https://api.github.com';
const GITHUB_ORG = process.env.GITHUB_ORG;
if (!GITHUB_ORG || GITHUB_ORG === '') {
console.error('Error: GITHUB_ORG is not defined or empty');
process.exit(1);
}
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
if (!GITHUB_TOKEN || GITHUB_TOKEN === '') {
console.error('Error: GITHUB_TOKEN is not defined or empty');
process.exit(1);
}
const headers = {
Authorization: `Bearer ${GITHUB_TOKEN}`,
Accept: 'application/vnd.github+json'
};
/**
* Delays execution for a specified amount of milliseconds.
*
* @param {number} ms - The number of milliseconds to delay.
* @returns {Promise<void>} A promise that resolves after the specified delay.
*/
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
/**
* Handles GitHub API rate limiting.
*
* @param {Object} headers - The response headers from the GitHub API.
* @returns {Promise<void>} A promise that resolves after the necessary delay.
*/
const handleRateLimit = async (headers) => {
const remaining = parseInt(headers['x-ratelimit-remaining'], 10);
const resetTime = parseInt(headers['x-ratelimit-reset'], 10) * 1000;
const currentTime = Date.now();
if (remaining <= 1) {
const delayTime = resetTime - currentTime;
console.log(`Rate limit reached. Waiting for ${Math.max(0, delayTime) / 1000} seconds`);
await delay(Math.max(0, delayTime));
} else {
await delay(2000); // Slight delay to stay under rate limit
}
};
/**
* Fetches all repositories in the specified organization.
*
* @param {string} orgName - The name of the GitHub organization.
* @param {number} [page=1] - The page number for pagination.
* @returns {Promise<Array<Object>>} List of repository objects.
*/
const fetchAllRepos = async (orgName, page = 1) => {
try {
const response = await axios.get(`${GITHUB_API_URL}/orgs/${orgName}/repos`, {
headers,
params: { per_page: 100, page }
});
// Handle rate limit
await handleRateLimit(response.headers);
// Filter out archived repositories
const repos = response.data.filter(repo => !repo.archived && !repo.fork && !repo.disabled);
// If there are more pages, recursively fetch
if (repos.length === 100) {
const nextPageRepos = await fetchAllRepos(orgName, page + 1);
return repos.concat(nextPageRepos);
}
return repos;
} catch (error) {
console.error('Error fetching repositories:', error.message);
if (error.response && error.response.status === 403) {
await handleRateLimit(error.response.headers);
return fetchAllRepos(orgName, page);
}
return [];
}
};
/**
* Checks if a CODEOWNERS file exists in the specified repository.
*
* @param {string} owner - The owner of the repository.
* @param {string} repo - The name of the repository.
* @returns {Promise<boolean>} True if the CODEOWNERS file exists, false otherwise.
*/
const checkCodeownersFile = async (owner, repo) => {
const codeownersPath = 'CODEOWNERS';
try {
await axios.get(`${GITHUB_API_URL}/repos/${owner}/${repo}/contents/${codeownersPath}`, {
headers
});
return true; // File exists
} catch (error) {
if (error.response && error.response.status === 404) {
return false; // File does not exist
}
console.error(`Error checking CODEOWNERS for ${repo}:`, error.message);
return false; // Assume file doesn't exist on error
}
};
/**
* Main function to find repositories without CODEOWNERS.
*/
const findReposWithoutCodeowners = async () => {
const repos = await fetchAllRepos(GITHUB_ORG);
const reposWithoutCodeowners = [];
for (const repo of repos) {
const hasCodeowners = await checkCodeownersFile(repo.owner.login, repo.name);
if (!hasCodeowners) {
reposWithoutCodeowners.push(repo.name);
}
}
console.log(`Repositories without CODEOWNERS: ${reposWithoutCodeowners}`);
};
// Execute the script
findReposWithoutCodeowners()
.catch(error => {
console.error('An unexpected error occurred:', error.message);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment