github-projects/map-labels/mapLabelsToAttributes.js (164 lines of code) (raw):
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.mapLabelsToAttributes = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const rest_1 = require("@octokit/rest");
const projectsGraphQL_1 = require("../api/projectsGraphQL");
/**
* Lists issues in a github project, and updates fields on them based on the mapping file given.
*
* The mapping file should be a JSON file with the following structure:
* {
* "<labelName>": {
* "<fieldName>": "<value>"
* }
* ...
* }
*/
async function mapLabelsToAttributes(args) {
const { issueNumber: issueNumbers, projectNumber, owner, repo, mapping, all, dryRun, githubToken } = args;
const hasFilter = issueNumbers.length > 0;
// If we're requesting all issues, we should list ~1000 issues to max it out
// if we have a filter, we will also want to search for those issues, so max it out
// if we're running with either of these args, we should be fine with the 50 most recent
const issueCount = hasFilter || all ? 1000 : 50;
const octokit = new rest_1.Octokit({
auth: githubToken.trim(),
});
if (dryRun) {
console.log('⚠️ Running in dry-run mode. No changes will be made.');
}
console.log(`Loading label mapping file ${mapping}`);
const labelsToFields = loadMapping(mapping);
console.log(`Requesting project ${owner}/${projectNumber} and its issues...`);
const projectAndFields = await (0, projectsGraphQL_1.gqlGetProject)(octokit, { projectNumber, owner });
const issuesInProject = await (0, projectsGraphQL_1.gqlGetIssuesForProject)(octokit, { projectNumber, findIssueNumbers: issueNumbers, owner }, {
issueCount,
});
const targetIssues = repo ? filterIssuesByRepo(issuesInProject, repo) : issuesInProject;
if (!targetIssues.length) {
console.error(`Could not find any update target(s) issues for params:`, {
projectNumber,
issueNumbers,
repo,
owner,
});
throw new Error('No target issues found');
}
else {
console.log(`Found ${targetIssues.length} target issue(s) for update`);
}
const updateResults = {
success: [],
failure: [],
skipped: [],
projectUrl: projectAndFields.url,
};
for (const issueNode of targetIssues) {
console.log(`Updating issue target: ${issueNode.content.url}...`);
try {
const updatedFields = await adjustSingleItemLabels(octokit, {
issueNode,
owner,
projectNumber,
projectId: projectAndFields.id,
mapping: labelsToFields,
dryRun,
});
if (updatedFields.length) {
console.log(`Updated fields: ${updatedFields.join(', ')}`);
updateResults.success.push(issueNode);
}
else {
console.log('No fields updated');
updateResults.skipped.push(issueNode);
}
}
catch (error) {
console.error('Error updating issue', error);
updateResults.failure.push(issueNode);
}
}
return updateResults;
}
exports.mapLabelsToAttributes = mapLabelsToAttributes;
function loadMapping(mappingFileName) {
const pathToMapping = path_1.default.join(__dirname, mappingFileName);
const mapping = fs_1.default.readFileSync(pathToMapping, 'utf8');
return JSON.parse(mapping);
}
function filterIssuesByRepo(issuesInProject, repo) {
console.log('Filtering issues by repository: ', repo);
return issuesInProject.filter((issue) => {
return issue.content.repository.name === repo;
});
}
async function adjustSingleItemLabels(octokit, options) {
var _a;
const { issueNode, projectNumber, projectId, mapping, owner, dryRun } = options;
const { content: issue, id: itemId } = issueNode;
const labels = issue.labels.nodes;
const updatedFields = [];
// Get fields for each mappable label
for (const label of labels) {
const fieldUpdate = mapping[label.name];
if (!fieldUpdate) {
continue;
}
const fieldName = Object.keys(fieldUpdate)[0];
const value = fieldUpdate[fieldName];
console.log('Finding option for value', { fieldName, value });
// Get field id
const optionForValue = await getOptionIdForValue(octokit, { projectNumber, fieldName, value, owner });
if (!optionForValue) {
continue;
}
// Check if the field is already set
const existingField = issueNode.fieldValues.nodes.find((field) => field.__typename === 'ProjectV2ItemFieldSingleSelectValue' && field.field.name === fieldName);
const fieldLookup = await getFieldLookupObj(octokit, { projectNumber, owner });
if (existingField) {
const existingFieldValue = (_a = fieldLookup[fieldName]) === null || _a === void 0 ? void 0 : _a.options.find((e) => e.id === existingField.optionId);
console.log(`Field "${fieldName}" is already set to "${existingFieldValue === null || existingFieldValue === void 0 ? void 0 : existingFieldValue.name}" (${existingField.optionId}), skipping update`);
continue;
}
// update field
console.log(`Updating field "${fieldName}" to "${value}" (${optionForValue.optionId})`);
const updateParams = {
projectId,
itemId,
fieldId: optionForValue.fieldId,
optionId: optionForValue.optionId,
fieldName,
};
if (dryRun) {
console.log('Dry run: skipping update for parameters', updateParams);
}
else {
await (0, projectsGraphQL_1.gqlUpdateFieldValue)(octokit, updateParams);
}
updatedFields.push(fieldName);
}
return updatedFields;
}
const getFieldLookupObj = (() => {
let fieldLookup;
return async (octokit, projectOptions) => {
if (typeof fieldLookup === 'undefined' || Object.keys(fieldLookup).length === 0) {
const fieldOptions = await (0, projectsGraphQL_1.gqlGetFieldOptions)(octokit, projectOptions);
const singleSelectFields = fieldOptions.organization.projectV2.fields.nodes.filter((f) => f.__typename === 'ProjectV2SingleSelectField');
fieldLookup = singleSelectFields.reduce((acc, field) => {
acc[field.name] = field;
return acc;
}, {});
console.log('Field lookup populated', fieldLookup);
}
return fieldLookup;
};
})();
async function getOptionIdForValue(octokit, options) {
var _a;
const { fieldName, value } = options;
const fieldLookup = await getFieldLookupObj(octokit, options);
const field = fieldLookup[fieldName];
if (!field) {
console.error(`Could not find field "${fieldName}" in project fields`);
return null;
}
const optionId = (_a = field.options.find((o) => o.name === value)) === null || _a === void 0 ? void 0 : _a.id;
if (!optionId) {
console.warn(`Could not find option for field "${fieldName}" and value "${value}"`, field.options);
return null;
}
else {
return {
optionId,
fieldId: field.id,
};
}
}
//# sourceMappingURL=mapLabelsToAttributes.js.map