2019-08-13 23:36:21 +03:00
|
|
|
const fs = require('fs');
|
|
|
|
const ical = require('ical-toolkit');
|
2019-08-25 01:05:08 +03:00
|
|
|
const ghpages = require('gh-pages');
|
2019-08-13 23:36:21 +03:00
|
|
|
|
2019-08-24 01:26:52 +03:00
|
|
|
const DIST_DIR = 'dist';
|
2019-08-13 23:36:21 +03:00
|
|
|
const CONFIG_FILE = 'config.json';
|
|
|
|
const HISTORY_FILE = 'history.json';
|
2019-08-24 01:26:52 +03:00
|
|
|
const TRIAGERS_KEY = 'triagers';
|
2019-08-24 02:00:38 +03:00
|
|
|
const ICAL_FILE = 'layout-triage.ics';
|
2019-08-24 01:26:52 +03:00
|
|
|
const INDENT = ' ';
|
|
|
|
const DUTY_START_DATES_KEY = 'duty-start-dates';
|
2019-08-13 23:36:21 +03:00
|
|
|
const CYCLE_LENGTH_DAYS = 7;
|
|
|
|
const DAY_TO_MS = 24 * 60 * 60 * 1000;
|
|
|
|
const CYCLE_LENGTH_MS = CYCLE_LENGTH_DAYS * DAY_TO_MS;
|
|
|
|
|
2019-08-24 01:26:52 +03:00
|
|
|
/**
|
|
|
|
* Return the parsed results from the config file. Reads file synchronously.
|
|
|
|
*/
|
2019-08-13 23:36:21 +03:00
|
|
|
function readConfig() {
|
2019-08-24 01:26:52 +03:00
|
|
|
return JSON.parse(fs.readFileSync(CONFIG_FILE));
|
2019-08-13 23:36:21 +03:00
|
|
|
}
|
|
|
|
|
2019-08-24 01:26:52 +03:00
|
|
|
/**
|
|
|
|
* Write the given JSON object to the history file.
|
|
|
|
* @param {*} json
|
|
|
|
*/
|
|
|
|
function writeToHistory(json) {
|
|
|
|
const data = JSON.stringify(json, undefined, INDENT);
|
|
|
|
fs.writeFileSync(HISTORY_FILE, data);
|
2019-08-13 23:36:21 +03:00
|
|
|
}
|
|
|
|
|
2019-08-24 01:26:52 +03:00
|
|
|
/**
|
|
|
|
* Given a date, return the date of the Monday preceding it.
|
|
|
|
* @param {Date} date
|
|
|
|
*/
|
|
|
|
function getLastMonday(date) {
|
|
|
|
const day = date.getDay();
|
|
|
|
const diff = date.getDate() - day + (day === 0 ? -6 : 1);
|
|
|
|
const monday = new Date();
|
|
|
|
monday.setDate(diff);
|
|
|
|
|
|
|
|
return monday;
|
2019-08-13 23:36:21 +03:00
|
|
|
}
|
|
|
|
|
2019-08-24 01:26:52 +03:00
|
|
|
function appendDutyCycle({ component, date, triagerName, triagerData }) {
|
|
|
|
const filePath = `${DIST_DIR}/${component}.json`;
|
|
|
|
let data = fs.readFileSync(filePath);
|
|
|
|
const calendar = JSON.parse(data);
|
|
|
|
|
|
|
|
const triagers = calendar[TRIAGERS_KEY];
|
|
|
|
const dutyStartDates = calendar[DUTY_START_DATES_KEY];
|
|
|
|
if (!dutyStartDates || !triagers) {
|
|
|
|
throw `\nFATAL ERROR: Invalid data in calendar ${component}.json`;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!triagers[triagerName]) {
|
|
|
|
triagers[triagerName] = triagerData;
|
|
|
|
}
|
|
|
|
|
|
|
|
dutyStartDates[date] = triagerName;
|
|
|
|
|
|
|
|
data = JSON.stringify(calendar, undefined, ' ');
|
|
|
|
fs.writeFileSync(filePath, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given an array, select n random items from the array,
|
|
|
|
*
|
|
|
|
* @param {} arr The array from which to select items.
|
|
|
|
* @param {*} n The number of items to select.
|
|
|
|
*/
|
2019-08-13 23:36:21 +03:00
|
|
|
function selectRandom(arr, n) {
|
|
|
|
const result = [];
|
|
|
|
const taken = [];
|
|
|
|
let len = arr.length;
|
|
|
|
|
|
|
|
if (n > len) {
|
|
|
|
throw `FATAL ERROR: Cannot select ${n} items from array with length ${arr.length}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
while (n--) {
|
|
|
|
let idx = Math.floor(Math.random() * len);
|
|
|
|
result[n] = arr[idx in taken ? taken[idx] : idx];
|
|
|
|
taken[idx] = --len in taken ? taken[len] : len;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2019-08-24 01:26:52 +03:00
|
|
|
/**
|
|
|
|
* Given a duty cycle history object, return the most recent cycle.
|
|
|
|
*
|
|
|
|
* @param {*} params
|
|
|
|
* @param {*} params.dutyCycleHistory The duty cycle history as formatted in the history file.
|
|
|
|
*/
|
|
|
|
function getLastDutyCycle({ dutyCycleHistory }) {
|
2019-08-13 23:36:21 +03:00
|
|
|
const dutyDates = Object.keys(dutyCycleHistory).sort();
|
|
|
|
if (dutyDates.length < 1) {
|
2019-08-24 01:26:52 +03:00
|
|
|
return {};
|
2019-08-13 23:36:21 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
const lastDutyDate = dutyDates.slice(-1)[0];
|
2019-08-24 01:26:52 +03:00
|
|
|
if (!dutyCycleHistory[lastDutyDate]) {
|
|
|
|
throw `\nFATAL ERROR: Invalid data in history file!`;
|
|
|
|
}
|
2019-08-13 23:36:21 +03:00
|
|
|
|
2019-08-24 01:26:52 +03:00
|
|
|
const lastTriagePair = Object.keys(dutyCycleHistory[lastDutyDate]);
|
|
|
|
return {
|
|
|
|
lastDutyDate,
|
|
|
|
lastTriagePair
|
2019-08-13 23:36:21 +03:00
|
|
|
}
|
2019-08-24 01:26:52 +03:00
|
|
|
}
|
2019-08-13 23:36:21 +03:00
|
|
|
|
2019-08-24 02:00:38 +03:00
|
|
|
function generateICALFile({ dutyCycleHistory }) {
|
|
|
|
const builder = ical.createIcsFileBuilder();
|
|
|
|
|
|
|
|
builder.calname = 'Layout Triage';
|
|
|
|
builder.timezone = 'America/Los_Angeles';
|
|
|
|
builder.tzid = 'America/Los_Angeles';
|
|
|
|
builder.additionalTags = {
|
|
|
|
'REFRESH-INTERVAL': 'VALUE=DURATION:P1H',
|
|
|
|
'X-WR-CALDESC': 'Layout Triage'
|
|
|
|
};
|
|
|
|
|
|
|
|
for (let dutyCycleDate in dutyCycleHistory) {
|
|
|
|
const triagers = Object.keys(dutyCycleHistory[dutyCycleDate]);
|
|
|
|
const dutyCycleDateMs = new Date(dutyCycleDate).getTime();
|
|
|
|
|
|
|
|
builder.events.push({
|
|
|
|
start: new Date(dutyCycleDateMs),
|
|
|
|
end: new Date(dutyCycleDateMs + CYCLE_LENGTH_MS),
|
|
|
|
summary: `Triage Duty: ${triagers.join(', ')}`,
|
|
|
|
description: ``,
|
|
|
|
allDay: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = builder.toString();
|
|
|
|
fs.writeFileSync(`${DIST_DIR}/${ICAL_FILE}`, data);
|
|
|
|
}
|
|
|
|
|
2019-08-24 01:26:52 +03:00
|
|
|
function generateDutyCycle({ dutyCycleHistory, triagersData, components }) {
|
|
|
|
let { lastDutyDate, lastTriagePair } = getLastDutyCycle({ dutyCycleHistory })
|
|
|
|
let lastTriagerIdx = -1;
|
2019-08-13 23:36:21 +03:00
|
|
|
const triagers = Object.keys(triagersData);
|
2019-08-24 01:26:52 +03:00
|
|
|
const createDateString = date => {
|
|
|
|
return date.toISOString().replace(/T.*$/, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!lastDutyDate || !Array.isArray(lastTriagePair) || lastTriagePair.length !== 2) {
|
|
|
|
console.warn('No existing duty cycle history. Generating first cycle.');
|
|
|
|
lastDutyDate = createDateString(getLastMonday(new Date()));
|
|
|
|
} else {
|
|
|
|
lastTriagerIdx = triagers.indexOf(lastTriagePair[1]);
|
|
|
|
if (lastTriagerIdx === -1) {
|
|
|
|
console.warn(`Unable to find triager named ${lastTriagePair[1]} in config. Starting over from first triager.`);
|
|
|
|
}
|
2019-08-13 23:36:21 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
const nextTriagerIdx = (lastTriagerIdx + 1) % triagers.length;
|
|
|
|
const nextDutyDateMS = new Date(lastDutyDate).getTime() + CYCLE_LENGTH_MS;
|
|
|
|
const nextTriagePair = [triagers[nextTriagerIdx], triagers[(nextTriagerIdx +1 ) % triagers.length]];
|
2019-08-24 01:26:52 +03:00
|
|
|
const nextDutyDate = createDateString(new Date(nextDutyDateMS));
|
2019-08-13 23:36:21 +03:00
|
|
|
const firstComponentSet = selectRandom(components, Math.floor(components.length / 2));
|
|
|
|
const secondComponentSet = components.filter(c => firstComponentSet.indexOf(c) === -1);
|
|
|
|
const dutyCycle = {};
|
|
|
|
dutyCycle[nextTriagePair[0]] = firstComponentSet;
|
|
|
|
dutyCycle[nextTriagePair[1]] = secondComponentSet;
|
|
|
|
|
|
|
|
return {
|
|
|
|
date: nextDutyDate,
|
|
|
|
dutyCycle
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-08-24 01:26:52 +03:00
|
|
|
function runUpdate() {
|
|
|
|
const { triagers: triagersData, components } = readConfig();
|
|
|
|
const { dutyCycleHistory } = JSON.parse(fs.readFileSync(HISTORY_FILE));
|
|
|
|
const { date, dutyCycle } = generateDutyCycle({ dutyCycleHistory, triagersData, components });
|
2019-08-13 23:36:21 +03:00
|
|
|
|
2019-08-24 02:00:38 +03:00
|
|
|
function updateJSONCalendars() {
|
|
|
|
const newDutyCycleTriagers = Object.keys(dutyCycle);
|
|
|
|
newDutyCycleTriagers.forEach(triager => {
|
|
|
|
const components = dutyCycle[triager];
|
|
|
|
components.forEach(component => {
|
|
|
|
appendDutyCycle({ component, date, triagerName: triager, triagerData: triagersData[triager] });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-08-13 23:36:21 +03:00
|
|
|
dutyCycleHistory[date] = dutyCycle;
|
2019-08-24 01:26:52 +03:00
|
|
|
|
2019-08-24 02:00:38 +03:00
|
|
|
updateJSONCalendars();
|
|
|
|
writeToHistory({ dutyCycleHistory });
|
|
|
|
generateICALFile({ dutyCycleHistory });
|
2019-08-24 01:26:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reset all existing data, leaving one duty cycle in the history to serve as a
|
|
|
|
* seed for the next cycle.
|
|
|
|
*/
|
|
|
|
function runReset() {
|
|
|
|
const { components } = readConfig();
|
|
|
|
const resetData = {};
|
|
|
|
resetData[TRIAGERS_KEY] = {};
|
|
|
|
resetData[DUTY_START_DATES_KEY] = {};
|
|
|
|
const resetDataString = JSON.stringify(resetData, undefined, INDENT);
|
|
|
|
|
|
|
|
components.forEach(component => {
|
|
|
|
const filePath = `${DIST_DIR}/${component}.json`;
|
|
|
|
fs.writeFileSync(filePath, resetDataString);
|
|
|
|
});
|
|
|
|
|
|
|
|
writeToHistory({ dutyCycleHistory: {} });
|
2019-08-24 02:00:38 +03:00
|
|
|
generateICALFile({ dutyCycleHistory: {} });
|
2019-08-13 23:36:21 +03:00
|
|
|
}
|
|
|
|
|
2019-08-25 01:05:08 +03:00
|
|
|
function runPublish() {
|
|
|
|
ghpages.publish(DIST_DIR, function (err) {
|
|
|
|
if (err) {
|
|
|
|
console.error('There was an error during publishing.');
|
|
|
|
} else {
|
|
|
|
console.log('Publish to GitHub was successful.');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-08-13 23:36:21 +03:00
|
|
|
let args = process.argv.slice(2);
|
|
|
|
let command = args.shift();
|
|
|
|
|
|
|
|
switch (command) {
|
|
|
|
case 'update': {
|
2019-08-24 01:26:52 +03:00
|
|
|
runUpdate();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case 'reset': {
|
|
|
|
runReset();
|
2019-08-13 23:36:21 +03:00
|
|
|
break;
|
|
|
|
}
|
2019-08-25 01:05:08 +03:00
|
|
|
|
|
|
|
case 'publish': {
|
|
|
|
runPublish();
|
|
|
|
break;
|
|
|
|
}
|
2019-08-13 23:36:21 +03:00
|
|
|
}
|