Skip to content

Instantly share code, notes, and snippets.

@wojtekmaj
Last active October 23, 2023 06:25
Show Gist options
  • Save wojtekmaj/ebfafc017304d95fc3a7adccf89d43d6 to your computer and use it in GitHub Desktop.
Save wojtekmaj/ebfafc017304d95fc3a7adccf89d43d6 to your computer and use it in GitHub Desktop.
GitHub monthly issues stats
import fs from 'node:fs';
import { asyncForEach, asyncForEachStrict } from '@wojtekmaj/async-array-utils';
import chalk from 'chalk';
import { endOfMonth, formatISO, startOfMonth } from 'date-fns';
const CACHE_DIR = '.cache';
const GITHUB_API_URL = 'https://api.github.com';
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const DEBUG = process.env.DEBUG === 'true';
if (!GITHUB_TOKEN) {
throw new Error('GITHUB_TOKEN environment variable must be set');
}
function log(...args: unknown[]) {
if (DEBUG) {
console.log(...args);
}
}
type SearchResultsPage = {
total_count: number;
};
class NetworkError extends Error {
status: number;
constructor(status: number, statusText: string) {
super(statusText);
this.name = 'NetworkError';
this.status = status;
}
}
/**
* Fetches a URL and caches it in the .cache directory. On subsequent calls, it
* will return the cached version if it exists.
*/
async function fetchWithCache(input: string | URL, init?: RequestInit): Promise<string> {
// log(chalk.gray`Getting %s…`, input);
const cacheKey = input
.toString()
.replace(/[^a-z0-9]/gi, '_')
.toLowerCase();
const cachePath = `${CACHE_DIR}/fetch/${cacheKey}`;
const cacheExists = fs.existsSync(cachePath);
// If the cache exists, return it
if (cacheExists) {
// log(chalk.gray` Retrieving from cache`);
const cachedFile = fs.readFileSync(cachePath, 'utf-8');
return cachedFile;
}
// Otherwise, fetch the URL
// log(chalk.gray` Fetching from network`, input);
const response = await fetch(input, init);
if (!response.ok) {
throw new NetworkError(response.status, response.statusText);
}
const requestText = await response.text();
// Ensure the cache directory exists
fs.mkdirSync(CACHE_DIR, { recursive: true });
// Write the response to the cache
fs.writeFileSync(cachePath, requestText);
return requestText;
}
const startDate = new Date(2020, 0, 1);
const endDate = new Date();
// List of monthly date ranges
const ranges: [Date, Date][] = [];
// Generate the list of monthly date ranges
for (let date = startDate; date <= endDate; date.setMonth(date.getMonth() + 1)) {
const start = startOfMonth(date);
const end = endOfMonth(date);
ranges.push([start, end]);
}
const results: number[] = [];
await (DEBUG ? asyncForEachStrict : asyncForEach)(ranges, async ([date1, date2], i) => {
const date1String = formatISO(date1, { representation: 'date' });
const date2String = formatISO(date2, { representation: 'date' });
log(chalk.gray`Fetching issues from %s to %s`, date1String, date2String);
const url = new URL(`${GITHUB_API_URL}/search/issues`);
url.searchParams.set(
'q',
`"this makes the require call ambiguous and unsound" created:${date1String}..${date2String}`,
);
const rawResponse = await fetchWithCache(url, {
headers: {
Authorization: `token ${process.env.GITHUB_TOKEN}`,
},
});
const response = JSON.parse(rawResponse) as SearchResultsPage;
results[i] = response.total_count;
});
log(results);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment