scripts/aggregateGitHubDataFromUpwork.ts
Rama main · réplica de lectura del repositorio original.
import CONST from '@github/libs/CONST';
/**
* This script is used for categorizing upwork costs into cost buckets for accounting purposes.
*
* To run this script from the root of E/App:
*
* bun ./scripts/aggregateGitHubDataFromUpwork.ts <path_to_csv> <github_pat> <output_path>
*
* The input file must be a CSV with a single column containing just the GitHub issue number. The CSV must have a single header row.
*/
import {getOctokitOptions, GitHub} from '@actions/github/lib/utils';
import {paginateRest} from '@octokit/plugin-paginate-rest';
import {throttling} from '@octokit/plugin-throttling';
import {createObjectCsvWriter} from 'csv-writer';
import fs from 'fs';
type IssueType = 'bug' | 'feature' | 'other';
if (process.argv.length < 3) {
throw new Error('Error: must provide filepath for CSV data');
}
if (process.argv.length < 4) {
throw new Error('Error: must provide GitHub token');
}
if (process.argv.length < 5) {
throw new Error('Error: must provide output file path');
}
// Get filepath for csv
const inputFilepath = process.argv.at(2);
if (!inputFilepath) {
throw new Error('Error: must provide filepath for CSV data');
}
// Get GitHub token
const token = (process.argv.at(3) ?? '').trim();
if (!token) {
throw new Error('Error: must provide GitHub token');
}
const Octokit = GitHub.plugin(throttling, paginateRest);
const octokit = new Octokit(
getOctokitOptions(token, {
throttle: {
onRateLimit: (retryAfter, options, throttledOctokit, retryCount) => {
console.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
// Retry once after hitting a rate limit error, then give up
if (retryCount <= 1) {
console.log(`Retrying after ${retryAfter} seconds!`);
return true;
}
},
onSecondaryRateLimit: (retryAfter, options) => {
// does not retry, only logs a warning
console.warn(`Abuse detected for request ${options.method} ${options.url}`);
},
},
}),
);
// Get output filepath
const outputFilepath = process.argv.at(4);
if (!outputFilepath) {
throw new Error('Error: must provide output file path');
}
// Get data from csv
const issues = fs
.readFileSync(inputFilepath)
.toString()
.split('\n')
.reduce((acc, issue) => {
if (!issue) {
return acc;
}
const issueNum = Number(issue.trim());
if (!issueNum) {
return acc;
}
acc.push(issueNum);
return acc;
}, [] as number[]);
const csvWriter = createObjectCsvWriter({
path: outputFilepath,
header: [
{id: 'number', title: 'number'},
{id: 'title', title: 'title'},
{id: 'labels', title: 'labels'},
{id: 'type', title: 'type'},
{id: 'capSWProjects', title: 'capSWProjects'},
],
});
function getIssueTypeFromLabels(labels: string[]): IssueType {
if (labels.includes('NewFeature')) {
return 'feature';
}
if (labels.includes('Bug')) {
return 'bug';
}
return 'other';
}
/**
* Returns a comma-delimited string with all projects associated with the given issue.
*/
async function getProjectsForIssue(issueNumber: number): Promise<string> {
const response = await octokit.graphql(
`
{
repository(owner: "${CONST.GITHUB_OWNER}", name: "${CONST.APP_REPO}") {
issue(number: ${issueNumber}) {
projectsV2(last: 30) {
nodes {
title
}
}
}
}
}
`,
);
return (response as {repository: {issue: {projectsV2: {nodes: Array<{title: string}>}}}}).repository.issue.projectsV2.nodes.map((node) => node.title).join(',');
}
async function getGitHubData() {
const gitHubData = [];
// Note: we fetch issues in a loop rather than in parallel to help address rate limiting issues with a PAT
for (const issueNumber of issues) {
console.info(`Fetching ${issueNumber}`);
const result = await octokit.rest.issues
.get({
owner: CONST.GITHUB_OWNER,
repo: CONST.APP_REPO,
// eslint-disable-next-line @typescript-eslint/naming-convention
issue_number: issueNumber,
})
.catch(() => {
console.warn(`Error getting issue ${issueNumber}`);
});
if (result) {
const issue = result.data;
const labels = issue.labels.reduce((acc, label) => {
if (typeof label === 'string') {
acc.push(label);
} else if (label.name) {
acc.push(label.name);
}
return acc;
}, [] as string[]);
const type = getIssueTypeFromLabels(labels);
let capSWProjects = '';
if (type === 'feature') {
capSWProjects = await getProjectsForIssue(issueNumber);
}
gitHubData.push({
number: issue.number,
title: issue.title,
labels,
type,
capSWProjects,
});
}
}
return gitHubData;
}
getGitHubData()
.then((gitHubData) => csvWriter.writeRecords(gitHubData))
.then(() => console.info(`Done ✅ Wrote file to ${outputFilepath}`));