mirror of https://github.com/rancher/dashboard.git
Zube migration: Update script, update workflow and remove Zube workflow (#10855)
This commit is contained in:
parent
c298d64d95
commit
c116777dc3
|
|
@ -1,18 +0,0 @@
|
||||||
name: zube-integration
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [ opened, reopened, edited, closed ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
rancher_zube:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: '16.x'
|
|
||||||
- name: script
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: node .github/workflows/scripts/pr.js
|
|
||||||
|
|
@ -6,19 +6,11 @@
|
||||||
|
|
||||||
const request = require('./request');
|
const request = require('./request');
|
||||||
|
|
||||||
const TRIAGE_LABEL = '[zube]: To Triage';
|
|
||||||
const IN_REVIEW_LABEL = '[zube]: Review';
|
|
||||||
const IN_TEST_LABEL = '[zube]: To Test';
|
|
||||||
const DONE_LABEL = '[zube]: Done';
|
|
||||||
const BACKEND_BLOCKED_LABEL = '[zube]: Backend Blocked';
|
|
||||||
const QA_REVIEW_LABEL = '[zube]: QA Review';
|
|
||||||
const TECH_DEBT_LABEL = 'kind/tech-debt';
|
const TECH_DEBT_LABEL = 'kind/tech-debt';
|
||||||
const DEV_VALIDATE_LABEL = 'status/dev-validate';
|
const DEV_VALIDATE_LABEL = 'status/dev-validate';
|
||||||
const QA_NONE_LABEL = 'QA/None';
|
const QA_NONE_LABEL = 'QA/None';
|
||||||
const QA_DEV_AUTOMATION_LABEL = 'QA/dev-automation'
|
const QA_DEV_AUTOMATION_LABEL = 'QA/dev-automation'
|
||||||
|
|
||||||
const GH_PRJ_TRIAGE = 'Triage';
|
|
||||||
|
|
||||||
const GH_PRJ_TO_TEST = 'To Test';
|
const GH_PRJ_TO_TEST = 'To Test';
|
||||||
const GH_PRJ_QA_REVIEW = 'QA Review';
|
const GH_PRJ_QA_REVIEW = 'QA Review';
|
||||||
const GH_PRJ_IN_REVIEW = 'Review';
|
const GH_PRJ_IN_REVIEW = 'Review';
|
||||||
|
|
@ -78,34 +70,28 @@ function hasLabel(issue, label) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function moveIssueToProjectState(project, prjIssueID, issue, state) {
|
async function moveIssueToProjectState(project, prjIssueID, issue, state) {
|
||||||
console.log(`moveIssueToProjectState ${ state }`);
|
// console.log(`moveIssueToProjectState ${ state }`);
|
||||||
console.log(JSON.stringify(project, null, 2));
|
// console.log(JSON.stringify(project, null, 2));
|
||||||
console.log(prjIssueID);
|
// console.log(prjIssueID);
|
||||||
// console.log(JSON.stringify(issue, null, 2));
|
// console.log(JSON.stringify(issue, null, 2));
|
||||||
|
|
||||||
const res = await request.ghUpdateProjectIssueStatus(project, prjIssueID, state);
|
const res = await request.ghUpdateProjectIssueStatus(project, prjIssueID, state);
|
||||||
|
|
||||||
console.log(JSON.stringify(res, null, 2));
|
// console.log(JSON.stringify(res, null, 2));
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove Zube labels
|
* Remove all Zube labels from an issue
|
||||||
*/
|
*/
|
||||||
async function removeZubeLabels(issue, label) {
|
async function removeZubeLabels(issue) {
|
||||||
// Remove all Zube labels
|
const currentLabels = issue.labels.map((v) => v.name);
|
||||||
let cleanLabels = labels.filter(l => l.name.indexOf('[zube]') === -1);
|
let cleanLabels = issue.labels.filter(l => l.name.indexOf('[zube]') === -1);
|
||||||
|
|
||||||
cleanLabels = cleanLabels.map((v) => {
|
cleanLabels = cleanLabels.map((v) => v.name());
|
||||||
return v.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Turn the array of labels into just their names
|
// Turn the array of labels into just their names
|
||||||
console.log(` Current Labels: ${cleanLabels}`);
|
console.log(' Removing Zube labels:');
|
||||||
|
console.log(` Current Labels: ${currentLabels}`);
|
||||||
// Add the 'to test' label
|
|
||||||
cleanLabels.push(label);
|
|
||||||
console.log(` New Labels : ${cleanLabels}`);
|
console.log(` New Labels : ${cleanLabels}`);
|
||||||
|
|
||||||
// Update the labels
|
// Update the labels
|
||||||
|
|
@ -113,27 +99,6 @@ async function removeZubeLabels(issue, label) {
|
||||||
return request.put(labelsAPI, { labels: cleanLabels });
|
return request.put(labelsAPI, { labels: cleanLabels });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForLabel(issue, label) {
|
|
||||||
let tries = 0;
|
|
||||||
while (!hasLabel(issue, label) && tries < 10) {
|
|
||||||
console.log(` Waiting for issue to have the label ${label} (${tries})`);
|
|
||||||
|
|
||||||
// Wait 10 seconds
|
|
||||||
await new Promise(r => setTimeout(r, 10000));
|
|
||||||
|
|
||||||
// Refetch the issue
|
|
||||||
issue = await request.fetch(issue.url);
|
|
||||||
|
|
||||||
tries++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tries > 10) {
|
|
||||||
console.log('WARNING: Timed out waiting for issue to have the Done label');
|
|
||||||
} else {
|
|
||||||
console.log(' Issue has the done label');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processClosedAction() {
|
async function processClosedAction() {
|
||||||
const pr = event.pull_request;
|
const pr = event.pull_request;
|
||||||
const body = pr.body;
|
const body = pr.body;
|
||||||
|
|
@ -147,8 +112,6 @@ async function processClosedAction() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(JSON.stringify(ghProject, null, 2));
|
|
||||||
|
|
||||||
console.log('======');
|
console.log('======');
|
||||||
console.log('Processing Closed PR #' + pr.number + ' : ' + pr.title);
|
console.log('Processing Closed PR #' + pr.number + ' : ' + pr.title);
|
||||||
console.log('======');
|
console.log('======');
|
||||||
|
|
@ -202,55 +165,52 @@ async function processClosedAction() {
|
||||||
if (hasLabel(iss, TECH_DEBT_LABEL) || hasLabel(iss, DEV_VALIDATE_LABEL) || hasLabel(iss, QA_NONE_LABEL)) {
|
if (hasLabel(iss, TECH_DEBT_LABEL) || hasLabel(iss, DEV_VALIDATE_LABEL) || hasLabel(iss, QA_NONE_LABEL)) {
|
||||||
console.log(' Issue is tech debt/dev validate/qa none - ignoring');
|
console.log(' Issue is tech debt/dev validate/qa none - ignoring');
|
||||||
} else {
|
} else {
|
||||||
// Put this in when we remove the Zube workflow
|
// Re-open the issue after GH closes it
|
||||||
// A single workflow needs to re-open the issue after GH closes it
|
await new Promise(r => setTimeout(r, 2500));
|
||||||
// console.log(' Waiting for Zube to mark the issue as done ...');
|
|
||||||
|
|
||||||
// // Output labels
|
// Re-open the issue if it is closed (it should be)
|
||||||
// const labels = iss.labels || [];
|
if (iss.state === 'closed') {
|
||||||
|
console.log(' Re-opening issue');
|
||||||
// console.log(labels.join(', '));
|
await request.patch(detail, { state: 'open' });
|
||||||
|
|
||||||
// // console.log(JSON.stringify(iss, null, 2));
|
|
||||||
|
|
||||||
// // The Zube Integration will label the issue with the Done label
|
|
||||||
// // Since it runs via a webhook, it should have done that well before our GitHub action
|
|
||||||
// // is scheduled and has run, but we will check it has the label and wait if not
|
|
||||||
// await waitForLabel(iss, DONE_LABEL);
|
|
||||||
|
|
||||||
// // Wait
|
|
||||||
// await new Promise(r => setTimeout(r, 10000));
|
|
||||||
// // Re-open the issue if it is closed
|
|
||||||
// if (iss.state === 'closed') {
|
|
||||||
// console.log(' Re-opening issue');
|
|
||||||
// await request.patch(detail, { state: 'open' });
|
|
||||||
// } else {
|
|
||||||
// console.log(' Expecting issue to be closed, but it is not');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// console.log(' Waiting for Zube to mark the issue as in triage ...');
|
|
||||||
|
|
||||||
// // The Zube Integration will label the issue as To Triage now that is has been re-opened
|
|
||||||
// // Wait for that and then we can move it to test
|
|
||||||
// await waitForLabel(iss, TRIAGE_LABEL);
|
|
||||||
|
|
||||||
// Move to QA Review if the issue has the label that dev wrote automated tests
|
|
||||||
if (hasLabel(iss, QA_DEV_AUTOMATION_LABEL)) {
|
|
||||||
console.log(' Updating GitHub Project to move issue to QA Review');
|
|
||||||
|
|
||||||
console.log(JSON.stringify(iss, null, 2));
|
|
||||||
|
|
||||||
// await moveIssueToProjectState(iss, GH_PRJ_QA_REVIEW);
|
|
||||||
// Uncomment when we switch off Zube
|
|
||||||
// await removeZubeLabels(iss);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(' Updating GitHub Project to move issue to Test');
|
console.log(' Expecting issue to be closed, but it is not');
|
||||||
|
}
|
||||||
|
|
||||||
console.log(JSON.stringify(iss, null, 2));
|
// Need to fetch the issue project status
|
||||||
|
const info = parseOrgAndRepo(iss.repository_url);
|
||||||
|
let prjIssue = await request.ghProjectIssue(info.org, info.repo, i);
|
||||||
|
|
||||||
// await moveIssueToProjectState(iss, GH_PRJ_TO_TEST);
|
// Is the issue on the board?
|
||||||
// Uncomment when we switch off Zube
|
if (!prjIssue?.[ghProject.id]) {
|
||||||
// await removeZubeLabels(iss);
|
// Issue is not on the board
|
||||||
|
console.log(`Issue ${ i } is NOT on the project board - adding it ...`);
|
||||||
|
|
||||||
|
await request.ghAddIssueToProject(ghProject, iss);
|
||||||
|
|
||||||
|
prjIssue = await request.ghProjectIssue(info.org, info.repo, i);
|
||||||
|
|
||||||
|
if (!prjIssue?.[ghProject.id]) {
|
||||||
|
console.log("Error: Could not add issue to Project Board");
|
||||||
|
console.log(prjIssue);
|
||||||
|
} else {
|
||||||
|
console.log('Added issue to the project board');
|
||||||
|
console.log(JSON.stringify(prjIssue, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prjIssue?.[ghProject.id]) {
|
||||||
|
// Move to QA Review if the issue has the label that dev wrote automated tests
|
||||||
|
if (hasLabel(iss, QA_DEV_AUTOMATION_LABEL)) {
|
||||||
|
console.log(' Updating GitHub Project to move issue to QA Review');
|
||||||
|
|
||||||
|
await moveIssueToProjectState(ghProject, prjIssue[ghProject.id], iss, GH_PRJ_QA_REVIEW);
|
||||||
|
await removeZubeLabels(iss);
|
||||||
|
} else {
|
||||||
|
console.log(' Updating GitHub Project to move issue to Test');
|
||||||
|
|
||||||
|
await moveIssueToProjectState(ghProject, prjIssue[ghProject.id], iss, GH_PRJ_TO_TEST);
|
||||||
|
await removeZubeLabels(iss);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,8 +249,6 @@ async function processOpenOrEditAction() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(JSON.stringify(ghProject, null, 2));
|
|
||||||
|
|
||||||
const pr = event.pull_request;
|
const pr = event.pull_request;
|
||||||
const body = pr.body;
|
const body = pr.body;
|
||||||
const issues = getReferencedIssues(body);
|
const issues = getReferencedIssues(body);
|
||||||
|
|
@ -309,19 +267,15 @@ async function processOpenOrEditAction() {
|
||||||
console.log('Processing Issue #' + i + ' - ' + iss.title);
|
console.log('Processing Issue #' + i + ' - ' + iss.title);
|
||||||
|
|
||||||
if (pr.draft) {
|
if (pr.draft) {
|
||||||
console.log(' Issue will not be moved to In Review (Draft PR)');
|
console.log(' Issue will not be moved to Review (Draft PR)');
|
||||||
} else if (hasLabel(iss, BACKEND_BLOCKED_LABEL)) {
|
// TODO:
|
||||||
console.log(' Issue will not be moved to In Review (Backend Blocked)');
|
// } else if (hasLabel(iss, BACKEND_BLOCKED_LABEL)) {
|
||||||
|
// console.log(' Issue will not be moved to Review (Backend Blocked)');
|
||||||
} else {
|
} else {
|
||||||
// Need to fetch the issue project status
|
// Need to fetch the issue project status
|
||||||
const info = parseOrgAndRepo(iss.repository_url);
|
const info = parseOrgAndRepo(iss.repository_url);
|
||||||
let prjIssue = await request.ghProjectIssue(info.org, info.repo, i);
|
let prjIssue = await request.ghProjectIssue(info.org, info.repo, i);
|
||||||
|
|
||||||
// console.log(info);
|
|
||||||
// console.log('-------- GH ISSUE -----');
|
|
||||||
// console.log(JSON.stringify(prjIssue, null, 2));
|
|
||||||
// console.log('---------');
|
|
||||||
|
|
||||||
// Is the issue on the board?
|
// Is the issue on the board?
|
||||||
if (!prjIssue?.[ghProject.id]) {
|
if (!prjIssue?.[ghProject.id]) {
|
||||||
// Issue is not on the board
|
// Issue is not on the board
|
||||||
|
|
@ -342,6 +296,7 @@ async function processOpenOrEditAction() {
|
||||||
|
|
||||||
if (prjIssue?.[ghProject.id]) {
|
if (prjIssue?.[ghProject.id]) {
|
||||||
await moveIssueToProjectState(ghProject, prjIssue[ghProject.id], iss, GH_PRJ_IN_REVIEW);
|
await moveIssueToProjectState(ghProject, prjIssue[ghProject.id], iss, GH_PRJ_IN_REVIEW);
|
||||||
|
await removeZubeLabels(iss);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Can not move issue to state ${ GH_PRJ_IN_REVIEW } - issue is not on the board`);
|
console.log(`Can not move issue to state ${ GH_PRJ_IN_REVIEW } - issue is not on the board`);
|
||||||
}
|
}
|
||||||
|
|
@ -371,7 +326,7 @@ async function processOpenOrEditAction() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debugging
|
// Debugging
|
||||||
console.log(JSON.stringify(event, null, 2));
|
// console.log(JSON.stringify(event, null, 2));
|
||||||
|
|
||||||
// Look at the action
|
// Look at the action
|
||||||
if (event.action === 'opened' || event.action === 'reopened') {
|
if (event.action === 'opened' || event.action === 'reopened') {
|
||||||
|
|
|
||||||
|
|
@ -1,273 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const request = require('./request');
|
|
||||||
|
|
||||||
const TRIAGE_LABEL = '[zube]: To Triage';
|
|
||||||
const IN_REVIEW_LABEL = '[zube]: Review';
|
|
||||||
const IN_TEST_LABEL = '[zube]: To Test';
|
|
||||||
const DONE_LABEL = '[zube]: Done';
|
|
||||||
const BACKEND_BLOCKED_LABEL = '[zube]: Backend Blocked';
|
|
||||||
const QA_REVIEW_LABEL = '[zube]: QA Review';
|
|
||||||
const TECH_DEBT_LABEL = 'kind/tech-debt';
|
|
||||||
const DEV_VALIDATE_LABEL = 'status/dev-validate';
|
|
||||||
const QA_NONE_LABEL = 'QA/None';
|
|
||||||
const QA_DEV_AUTOMATION_LABEL = 'QA/dev-automation'
|
|
||||||
|
|
||||||
// The event object
|
|
||||||
const event = require(process.env.GITHUB_EVENT_PATH);
|
|
||||||
|
|
||||||
function getReferencedIssues(body) {
|
|
||||||
// https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword
|
|
||||||
const regexp = /[Ff]ix(es|ed)?\s*#([0-9]*)|[Cc]lose(s|d)?\s*#([0-9]*)|[Rr]esolve(s|d)?\s*#([0-9]*)/g;
|
|
||||||
var v;
|
|
||||||
const issues = [];
|
|
||||||
do {
|
|
||||||
v = regexp.exec(body);
|
|
||||||
if (v) {
|
|
||||||
issues.push(parseInt(v[2], 10));
|
|
||||||
}
|
|
||||||
} while (v);
|
|
||||||
return issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasLabel(issue, label) {
|
|
||||||
const labels = issue.labels || [];
|
|
||||||
|
|
||||||
return !!(labels.find(l =>l.name.toLowerCase() === label.toLowerCase()));
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeZubeLabels(labels) {
|
|
||||||
return labels.filter(l => l.name.indexOf('[zube]') === -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resetZubeLabels(issue, label) {
|
|
||||||
// Remove all Zube labels
|
|
||||||
let cleanLabels = removeZubeLabels(issue.labels);
|
|
||||||
|
|
||||||
cleanLabels = cleanLabels.map((v) => {
|
|
||||||
return v.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Turn the array of labels into just their names
|
|
||||||
console.log(` Current Labels: ${cleanLabels}`);
|
|
||||||
|
|
||||||
// Add the 'to test' label
|
|
||||||
cleanLabels.push(label);
|
|
||||||
console.log(` New Labels : ${cleanLabels}`);
|
|
||||||
|
|
||||||
// Update the labels
|
|
||||||
const labelsAPI = `${issue.url}/labels`;
|
|
||||||
return request.put(labelsAPI, {labels: cleanLabels});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForLabel(issue, label) {
|
|
||||||
let tries = 0;
|
|
||||||
while (!hasLabel(issue, label) && tries < 10) {
|
|
||||||
console.log(` Waiting for issue to have the label ${label} (${tries})`);
|
|
||||||
|
|
||||||
// Wait 10 seconds
|
|
||||||
await new Promise(r => setTimeout(r, 10000));
|
|
||||||
|
|
||||||
// Refetch the issue
|
|
||||||
issue = await request.fetch(issue.url);
|
|
||||||
|
|
||||||
tries++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tries > 10) {
|
|
||||||
console.log('WARNING: Timed out waiting for issue to have the Done label');
|
|
||||||
} else {
|
|
||||||
console.log(' Issue has the done label');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processClosedAction() {
|
|
||||||
const pr = event.pull_request;
|
|
||||||
const body = pr.body;
|
|
||||||
|
|
||||||
console.log('======');
|
|
||||||
console.log('Processing Closed PR #' + pr.number + ' : ' + pr.title);
|
|
||||||
console.log('======');
|
|
||||||
|
|
||||||
// Check that the issue was merged and not just closed
|
|
||||||
if (!pr.merged) {
|
|
||||||
console.log( ' PR was closed without merging - ignoring');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const issues = getReferencedIssues(body);
|
|
||||||
if (issues.length > 0) {
|
|
||||||
console.log(' This PR fixes issues: ' + issues.join(', '));
|
|
||||||
} else {
|
|
||||||
console.log(" This PR does not fix any issues");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Need to get all open PRs to see if any other references the same issues that this PR says it fixes
|
|
||||||
const openPRs = event.repository.url + '/pulls?state=open&per_page=100';
|
|
||||||
const r = await request.fetch(openPRs);
|
|
||||||
const issueMap = issues.reduce((prev, issue) => { prev[issue] = true; return prev; }, {})
|
|
||||||
|
|
||||||
// Go through all of the Open PRs and see if they fix any of the same issues that this PR does
|
|
||||||
// If not, then the issue has been completed, so we can process it
|
|
||||||
r.forEach(openPR => {
|
|
||||||
const fixed = getReferencedIssues(openPR.body);
|
|
||||||
fixed.forEach(issue => issueMap[issue] = false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter down the list of issues that should be closed because this PR was merged
|
|
||||||
const fixed = Object.keys(issueMap).filter(key => !!issueMap[key]);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
Object.keys(issueMap).forEach(k => {
|
|
||||||
if (k && !issueMap[k]) {
|
|
||||||
console.log(` Issue #${k} will be ignored as another open PR also states that it fixes this issue`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GitHub will do the closing, so we expect each issue to already be closed
|
|
||||||
// We will fetch each issue in turn, expecting it to be closed
|
|
||||||
// We will re-open the issue and label it as ready to test
|
|
||||||
fixed.forEach(async(i) => {
|
|
||||||
const detail = event.repository.url + '/issues/' + i;
|
|
||||||
const iss = await request.fetch(detail);
|
|
||||||
console.log('')
|
|
||||||
console.log('Processing Issue #' + i + ' - ' + iss.title);
|
|
||||||
|
|
||||||
// If the issue is a tech debt issue or says dev will validate then don't move it to 'To Test'
|
|
||||||
if(hasLabel(iss, TECH_DEBT_LABEL) || hasLabel(iss, DEV_VALIDATE_LABEL) || hasLabel(iss, QA_NONE_LABEL)) {
|
|
||||||
console.log(' Issue is tech debt/dev validate/qa none - ignoring');
|
|
||||||
} else {
|
|
||||||
console.log(' Waiting for Zube to mark the issue as done ...');
|
|
||||||
|
|
||||||
// Output labels
|
|
||||||
const labels = iss.labels || [];
|
|
||||||
|
|
||||||
console.log(labels.join(', '));
|
|
||||||
|
|
||||||
// console.log(JSON.stringify(iss, null, 2));
|
|
||||||
|
|
||||||
// The Zube Integration will label the issue with the Done label
|
|
||||||
// Since it runs via a webhook, it should have done that well before our GitHub action
|
|
||||||
// is scheduled and has run, but we will check it has the label and wait if not
|
|
||||||
await waitForLabel(iss, DONE_LABEL);
|
|
||||||
|
|
||||||
// Wait
|
|
||||||
await new Promise(r => setTimeout(r, 10000));
|
|
||||||
// Re-open the issue if it is closed
|
|
||||||
if (iss.state === 'closed') {
|
|
||||||
console.log(' Re-opening issue');
|
|
||||||
await request.patch(detail, { state: 'open' });
|
|
||||||
} else {
|
|
||||||
console.log(' Expecting issue to be closed, but it is not');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Waiting for Zube to mark the issue as in triage ...');
|
|
||||||
|
|
||||||
// The Zube Integration will label the issue as To Triage now that is has been re-opened
|
|
||||||
// Wait for that and then we can move it to test
|
|
||||||
await waitForLabel(iss, TRIAGE_LABEL);
|
|
||||||
|
|
||||||
// Move to QA Review if the issue has the label that dev wrote automated tests
|
|
||||||
if (hasLabel(iss, QA_DEV_AUTOMATION_LABEL)) {
|
|
||||||
console.log(' Updating labels to move issue to QA Review');
|
|
||||||
|
|
||||||
await resetZubeLabels(iss, QA_REVIEW_LABEL);
|
|
||||||
} else {
|
|
||||||
console.log(' Updating labels to move issue to Test');
|
|
||||||
|
|
||||||
await resetZubeLabels(iss, IN_TEST_LABEL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processOpenAction() {
|
|
||||||
const pr = event.pull_request;
|
|
||||||
|
|
||||||
// Check that an assignee has been set
|
|
||||||
if (pr.assignees.length === 0) {
|
|
||||||
console.log('======');
|
|
||||||
console.log('Processing Opened PR #' + pr.number + ' : ' + pr.title);
|
|
||||||
console.log('======');
|
|
||||||
|
|
||||||
console.log(` Adding assignee to the PR: ${pr.user.login}`);
|
|
||||||
|
|
||||||
// Update the assignees
|
|
||||||
const assigneesAPI = `${event.repository.url}/issues/${pr.number}/assignees`;
|
|
||||||
await request.post(assigneesAPI, {assignees: [pr.user.login]});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processOpenOrEditAction() {
|
|
||||||
console.log('======');
|
|
||||||
console.log('Processing Opened/Edited PR #' + event.pull_request.number + ' : ' + event.pull_request.title);
|
|
||||||
console.log('======');
|
|
||||||
|
|
||||||
const pr = event.pull_request;
|
|
||||||
const body = pr.body;
|
|
||||||
const issues = getReferencedIssues(body);
|
|
||||||
if (issues.length > 0) {
|
|
||||||
console.log('+ This PR fixes issues: #' + issues.join(', '));
|
|
||||||
} else {
|
|
||||||
console.log("+ This PR does not fix any issues");
|
|
||||||
}
|
|
||||||
|
|
||||||
const milestones = {};
|
|
||||||
|
|
||||||
for (i of issues) {
|
|
||||||
const detail = `${event.repository.url}/issues/${i}`;
|
|
||||||
const iss = await request.fetch(detail);
|
|
||||||
console.log('')
|
|
||||||
console.log('Processing Issue #' + i + ' - ' + iss.title);
|
|
||||||
|
|
||||||
if (pr.draft) {
|
|
||||||
console.log(' Issue will not be moved to In Review (Draft PR)');
|
|
||||||
} else if (hasLabel(iss, BACKEND_BLOCKED_LABEL)) {
|
|
||||||
console.log(' Issue will not be moved to In Review (Backend Blocked)');
|
|
||||||
} else {
|
|
||||||
if (!hasLabel(iss, IN_REVIEW_LABEL)) {
|
|
||||||
// Add the In Review label to the issue as it does not have it
|
|
||||||
await resetZubeLabels(iss, IN_REVIEW_LABEL);
|
|
||||||
} else {
|
|
||||||
console.log(' Issue already has the In Review label');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (iss.milestone) {
|
|
||||||
milestones[iss.milestone.title] = iss.milestone.number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = Object.keys(milestones);
|
|
||||||
if (keys.length === 0) {
|
|
||||||
console.log('No milestones on issue(s) for this PR');
|
|
||||||
} else if (keys.length > 1) {
|
|
||||||
console.log('More than one milestone on issues for this PR');
|
|
||||||
} else {
|
|
||||||
// There is exactly 1 milestone, so use that for the PR
|
|
||||||
const milestoneNumber = milestones[keys[0]];
|
|
||||||
|
|
||||||
if (event.pull_request.milestone?.number !== milestoneNumber) {
|
|
||||||
console.log('Updating PR with milestone: ' + keys[0]);
|
|
||||||
await request.patch(event.pull_request.issue_url, {milestone: milestoneNumber});
|
|
||||||
} else {
|
|
||||||
console.log('PR is already assigned to milestone ' + keys[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debugging
|
|
||||||
// console.log(JSON.stringify(event, null, 2));
|
|
||||||
|
|
||||||
// Look at the action
|
|
||||||
if (event.action === 'opened' || event.action === 'reopened') {
|
|
||||||
processOpenAction();
|
|
||||||
processOpenOrEditAction();
|
|
||||||
} else if (event.action === 'edited') {
|
|
||||||
processOpenOrEditAction();
|
|
||||||
} else if (event.action === 'closed') {
|
|
||||||
processClosedAction();
|
|
||||||
}
|
|
||||||
|
|
@ -33,6 +33,10 @@ async function ghProject(org, num) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
... on ProjectV2FieldCommon {
|
||||||
|
id
|
||||||
|
name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -40,6 +44,8 @@ async function ghProject(org, num) {
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
// console.log(gQL);
|
||||||
|
|
||||||
const res = await graphql(gQL);
|
const res = await graphql(gQL);
|
||||||
|
|
||||||
const prj = {};
|
const prj = {};
|
||||||
|
|
@ -62,13 +68,21 @@ async function ghProject(org, num) {
|
||||||
options: optionMap
|
options: optionMap
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const storyPointsField = v2Project.fields?.nodes.find((node) => node.name === 'Story Points');
|
||||||
|
|
||||||
|
if (storyPointsField) {
|
||||||
|
prj.storyPointsField = {
|
||||||
|
id: storyPointsField.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log(res);
|
console.log(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
return prj;
|
return prj;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the issue and get the project info for the issue (map of project ID to project issue ID)
|
* Fetch the issue and get the project info for the issue (map of project ID to project issue ID)
|
||||||
*
|
*
|
||||||
|
|
@ -187,6 +201,24 @@ async function ghFetchOpenIssues(org, repo, milestone, label, previous) {
|
||||||
extra += ` label:\\"${label}\\"`;
|
extra += ` label:\\"${label}\\"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const query = `repo:${org}/${repo} ${extra}`;
|
||||||
|
|
||||||
|
return ghQueryIssues(query, previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ghFetchOpenIssuesInProject(org, projectId, milestone, label, previous) {
|
||||||
|
let extra = milestone ? `milestone:${ milestone }` : '';
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
extra += ` label:\\"${label}\\"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `project:${org}/${projectId} ${extra}`;
|
||||||
|
|
||||||
|
return ghQueryIssues(query, previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ghQueryIssues(query, previous) {
|
||||||
let after = '';
|
let after = '';
|
||||||
|
|
||||||
if (previous && previous.pageInfo?.endCursor) {
|
if (previous && previous.pageInfo?.endCursor) {
|
||||||
|
|
@ -194,7 +226,7 @@ async function ghFetchOpenIssues(org, repo, milestone, label, previous) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const gQL = `query {
|
const gQL = `query {
|
||||||
search(first:100, ${after} type:ISSUE, query:"is:open is:issue repo:${org}/${repo} ${extra}") {
|
search(first:100, ${after} type:ISSUE, query:"is:open is:issue ${query}") {
|
||||||
issueCount,
|
issueCount,
|
||||||
pageInfo {
|
pageInfo {
|
||||||
startCursor
|
startCursor
|
||||||
|
|
@ -225,6 +257,11 @@ async function ghFetchOpenIssues(org, repo, milestone, label, previous) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
storyPoints: fieldValueByName(name: "Story Points") {
|
||||||
|
...on ProjectV2ItemFieldNumberValue {
|
||||||
|
number
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,7 +281,7 @@ async function ghFetchOpenIssues(org, repo, milestone, label, previous) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.data.search.pageInfo.hasNextPage) {
|
if (res.data.search.pageInfo.hasNextPage) {
|
||||||
return await ghFetchOpenIssues(org, repo, milestone, label, res.data.search)
|
return await ghQueryIssues(query, res.data.search)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.data.search.nodes;
|
return res.data.search.nodes;
|
||||||
|
|
@ -319,7 +356,11 @@ function write(url, data, method) {
|
||||||
|
|
||||||
response.on('end', () => {
|
response.on('end', () => {
|
||||||
let response_body = Buffer.concat(chunks_of_data);
|
let response_body = Buffer.concat(chunks_of_data);
|
||||||
resolve(JSON.parse(response_body.toString()));
|
try {
|
||||||
|
resolve(JSON.parse(response_body.toString()));
|
||||||
|
} catch (e) {
|
||||||
|
reject(response);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
response.on('error', (error) => {
|
response.on('error', (error) => {
|
||||||
|
|
@ -342,4 +383,5 @@ module.exports = {
|
||||||
ghUpdateProjectIssueStatus,
|
ghUpdateProjectIssueStatus,
|
||||||
ghFetchOpenIssues,
|
ghFetchOpenIssues,
|
||||||
ghAddIssueToProject,
|
ghAddIssueToProject,
|
||||||
|
ghFetchOpenIssuesInProject,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,22 @@ console.log('=========================================');
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
const STATUS_MAP = {
|
const STATUS_MAP = {
|
||||||
'Backlog': 'Backlog',
|
|
||||||
'To Triage': 'To Triage',
|
'To Triage': 'To Triage',
|
||||||
|
'Backlog': 'Backlog',
|
||||||
|
'Icebox': 'Ice Box',
|
||||||
'Groomed': 'Groomed',
|
'Groomed': 'Groomed',
|
||||||
|
'Design Triage': 'To Triage',
|
||||||
|
'Backend Blocked': 'Backend Blocked',
|
||||||
|
'Next Up': 'Working',
|
||||||
|
'Reopened': 'Reopened',
|
||||||
|
'Working': 'Working',
|
||||||
|
'Review': 'Review',
|
||||||
|
'To Test': 'To Test',
|
||||||
|
'QA Review': 'QA Review',
|
||||||
|
'QA Next up': 'QA Working',
|
||||||
|
'QA Blocked': 'QA Blocked',
|
||||||
|
'QA Working': 'QA Working',
|
||||||
|
'Done': 'Done',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Options
|
// Options
|
||||||
|
|
@ -19,6 +32,7 @@ const STATUS_MAP = {
|
||||||
// -z = zube_status - only migrate issues with the given zube label
|
// -z = zube_status - only migrate issues with the given zube label
|
||||||
// -p = project - GH project in the format 'org#number'
|
// -p = project - GH project in the format 'org#number'
|
||||||
// -a = apply - make the changes (default is dry-run that shows which changes will be made)
|
// -a = apply - make the changes (default is dry-run that shows which changes will be made)
|
||||||
|
// -s = size = sync estimates from size labels and not status
|
||||||
|
|
||||||
// Parse the options
|
// Parse the options
|
||||||
|
|
||||||
|
|
@ -30,6 +44,7 @@ const options = {
|
||||||
project: undefined,
|
project: undefined,
|
||||||
repo: undefined,
|
repo: undefined,
|
||||||
apply: false,
|
apply: false,
|
||||||
|
migrateSize: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleOption(options, optName, flag, hasValue) {
|
function handleOption(options, optName, flag, hasValue) {
|
||||||
|
|
@ -58,8 +73,9 @@ handleOption(options, 'repo', '-r', true);
|
||||||
handleOption(options, 'milestone', '-m', true);
|
handleOption(options, 'milestone', '-m', true);
|
||||||
handleOption(options, 'zubeStatus', '-z', true);
|
handleOption(options, 'zubeStatus', '-z', true);
|
||||||
handleOption(options, 'apply', '-a', false);
|
handleOption(options, 'apply', '-a', false);
|
||||||
|
handleOption(options, 'migrateSize', '-s', false);
|
||||||
|
|
||||||
if (!process.env.TOKEN || !process.env.GH_TOKEN) {
|
if (!process.env.TOKEN && !process.env.GH_TOKEN) {
|
||||||
console.log('You must set a GitHub token in either the TOKEN or GH_TOKEN environment variables');
|
console.log('You must set a GitHub token in either the TOKEN or GH_TOKEN environment variables');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
@ -85,6 +101,16 @@ if (!options.repo) {
|
||||||
console.log('You must provide a GitHub repository with the -r flag in the form org/name');
|
console.log('You must provide a GitHub repository with the -r flag in the form org/name');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
const rParts = options.repo.split('/');
|
||||||
|
|
||||||
|
if (rParts.length !== 2) {
|
||||||
|
console.log('GitHub repository must be in the form org/repo');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.repo = rParts;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.milestone) {
|
if (!options.milestone) {
|
||||||
|
|
@ -99,7 +125,7 @@ if (options.zubeStatus && !options.zubeStatus.startsWith('[zube')) {
|
||||||
|
|
||||||
// console.log(options);
|
// console.log(options);
|
||||||
|
|
||||||
async function run() {
|
async function syncStatus() {
|
||||||
// Fetch the GitHub Project board
|
// Fetch the GitHub Project board
|
||||||
const project = await request.ghProject(options.project[0], options.project[1]);
|
const project = await request.ghProject(options.project[0], options.project[1]);
|
||||||
|
|
||||||
|
|
@ -111,8 +137,10 @@ async function run() {
|
||||||
|
|
||||||
// console.log(project);
|
// console.log(project);
|
||||||
|
|
||||||
|
console.log('Fetching issues...');
|
||||||
|
|
||||||
// Fetch all of the matching issues
|
// Fetch all of the matching issues
|
||||||
const issues = await request.ghFetchOpenIssues('rancher', 'dashboard', options.milestone, options.zubeStatus);
|
const issues = await request.ghFetchOpenIssues(options.repo[0], options.repo[1], options.milestone, options.zubeStatus);
|
||||||
|
|
||||||
if (!issues) {
|
if (!issues) {
|
||||||
console.log('Unable to fetch issues');
|
console.log('Unable to fetch issues');
|
||||||
|
|
@ -272,4 +300,156 @@ async function run() {
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
async function syncEstimate() {
|
||||||
|
// Fetch the GitHub Project board
|
||||||
|
const project = await request.ghProject(options.project[0], options.project[1]);
|
||||||
|
|
||||||
|
if (!project || Object.keys(project).length === 0) {
|
||||||
|
console.log('Unable to fetch ID for specified project');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('Fetched project');
|
||||||
|
// console.log(project);
|
||||||
|
|
||||||
|
if (!project.storyPointsField) {
|
||||||
|
console.log('Error: Project does not have a Story Points field');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Fetching issues...');
|
||||||
|
|
||||||
|
// Fetch all of the matching issues in the project
|
||||||
|
const issues = await request.ghFetchOpenIssuesInProject(options.project[0], options.project[1], options.milestone, options.zubeStatus);
|
||||||
|
|
||||||
|
if (!issues) {
|
||||||
|
console.log('Unable to fetch issues');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Fetched ${ issues.length } issue(s)`);
|
||||||
|
|
||||||
|
// Filter down to those with a size label
|
||||||
|
|
||||||
|
const estimated = [];
|
||||||
|
|
||||||
|
issues.forEach((issue) => {
|
||||||
|
const sizeLabels = (issue.labels?.nodes || []).filter((label) => label.name.startsWith('size/')).map((label) => label.name);
|
||||||
|
|
||||||
|
if (sizeLabels.length === 1) {
|
||||||
|
estimated.push({
|
||||||
|
...issue,
|
||||||
|
estimate: parseInt(sizeLabels[0].split('/')[1])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`${ estimated.length } issues have a size label`);
|
||||||
|
|
||||||
|
const updates = [];
|
||||||
|
|
||||||
|
estimated.forEach((issue) => {
|
||||||
|
const projectInfo = issue.projectItems.nodes.find((pItem) => pItem.project.id === project.id);
|
||||||
|
const prjEstimate = projectInfo.storyPoints?.number;
|
||||||
|
|
||||||
|
let updating = false;
|
||||||
|
let action = false;
|
||||||
|
|
||||||
|
if (prjEstimate === undefined) {
|
||||||
|
action = true;
|
||||||
|
} else if (prjEstimate !== issue.estimate) {
|
||||||
|
updating = true;
|
||||||
|
action = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
updates.push({
|
||||||
|
...issue,
|
||||||
|
idInProject: projectInfo?.id,
|
||||||
|
currentEstimate: projectInfo?.storyPoints?.number || 'No Estimate',
|
||||||
|
estimate: issue.estimate,
|
||||||
|
updating,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (updates.length) {
|
||||||
|
console.log('');
|
||||||
|
console.log('Updates required:');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
console.log(`#ISSUE ${ 'TITLE'.padEnd(80) } CHANGE ESTIMATE`);
|
||||||
|
console.log(`------ ${ '-'.padEnd(80, '-') } -------- --------`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let updateQL = `mutation {\n`;
|
||||||
|
|
||||||
|
updates.forEach((update) => {
|
||||||
|
let change = '';
|
||||||
|
let note = '';
|
||||||
|
if (update.updating) {
|
||||||
|
change = 'UPDATE ';
|
||||||
|
note = `${update.currentEstimate} -> ${update.estimate}`;
|
||||||
|
} else {
|
||||||
|
change = `SET `;
|
||||||
|
note = `${update.estimate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateQL += `issue${update.number}: updateProjectV2ItemFieldValue(input: {
|
||||||
|
projectId: "${ project.id }"
|
||||||
|
itemId: "${ update.idInProject }"
|
||||||
|
fieldId: "${ project.storyPointsField.id }"
|
||||||
|
value: {
|
||||||
|
number: ${ update.estimate }
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
projectV2Item {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}\n`;
|
||||||
|
|
||||||
|
const number = `${ update.number }`.padStart(6);
|
||||||
|
|
||||||
|
console.log(`${ number } ${ update.title.substr(0,80).padEnd(80)} ${ change } ${ note }`);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateQL += '}';
|
||||||
|
|
||||||
|
if (!updates.length) {
|
||||||
|
console.log('');
|
||||||
|
console.log('All issues are up to date - nothing to do');
|
||||||
|
} else {
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
if (options.apply) {
|
||||||
|
const updateRes = await request.graphql(updateQL);
|
||||||
|
|
||||||
|
if (!updateRes.data) {
|
||||||
|
console.log('Error updating estimates of issues');
|
||||||
|
console.log(updateRes);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${ updates.length} updates applied`);
|
||||||
|
} else {
|
||||||
|
console.log(`${ updates.length} issue(s) require updating out of ${ estimated.length }`);
|
||||||
|
console.log('');
|
||||||
|
console.log('To apply updates, run again with the -a flag');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two modes
|
||||||
|
|
||||||
|
if (!options.migrateSize) {
|
||||||
|
syncStatus();
|
||||||
|
} else {
|
||||||
|
syncEstimate();
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue