Files
languageservices/workflow-parser/src/model/converter/cron.ts
T

116 lines
3.3 KiB
TypeScript
Raw Normal View History

2023-01-24 12:46:28 -08:00
import cronstrue from "cronstrue";
2023-01-09 17:51:44 -08:00
2023-01-24 12:46:28 -08:00
import {MONTH_RANGE, HOUR_RANGE, MINUTE_RANGE, DOM_RANGE, DOW_RANGE} from "./cron-constants";
2023-01-09 17:51:44 -08:00
type Range = {
2023-01-24 12:46:28 -08:00
min: number;
max: number;
names?: Record<string, number>;
};
2023-01-09 17:51:44 -08:00
export function isValidCron(cron: string): boolean {
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
2023-01-24 12:46:28 -08:00
const parts = cron.split(/ +/);
if (parts.length != 5) {
2023-01-24 12:46:28 -08:00
return false;
}
2023-01-24 12:46:28 -08:00
const [minutes, hours, dom, months, dow] = parts;
return (
validateCronPart(minutes, MINUTE_RANGE) &&
validateCronPart(hours, HOUR_RANGE) &&
validateCronPart(dom, DOM_RANGE) &&
validateCronPart(months, MONTH_RANGE) &&
validateCronPart(dow, DOW_RANGE)
2023-01-24 12:46:28 -08:00
);
}
2023-01-23 17:50:54 -08:00
export function getCronDescription(cronspec: string): string | undefined {
2023-01-24 12:46:28 -08:00
if (!isValidCron(cronspec)) {
return;
}
2023-01-24 12:46:28 -08:00
let desc = "";
try {
desc = cronstrue.toString(cronspec, {
dayOfWeekStartIndexZero: true,
monthStartIndexZero: false,
use24HourTimeFormat: true,
// cronstrue sets the description as the error if throwExceptionOnParseError is false
// so we need to distinguish between an error and a valid description
2023-01-24 12:46:28 -08:00
throwExceptionOnParseError: true
});
} catch (err) {
2023-01-24 12:46:28 -08:00
return;
}
2023-01-23 15:12:16 -08:00
// Make first character lowercase
2023-01-24 12:46:28 -08:00
let result = "Runs " + desc.charAt(0).toLowerCase() + desc.slice(1);
result +=
"\n\nActions schedules run at most every 5 minutes." +
2023-01-23 17:50:54 -08:00
" [Learn more](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#onschedule)";
2023-01-24 12:46:28 -08:00
return result;
}
2023-01-24 12:46:28 -08:00
function validateCronPart(value: string, range: Range, allowSeparators = true): boolean {
2023-01-09 17:51:44 -08:00
if (range.names && range.names[value.toLowerCase()] !== undefined) {
2023-01-24 12:46:28 -08:00
return true;
2023-01-09 16:52:53 -08:00
}
2023-01-09 17:51:44 -08:00
if (value === "*") {
2023-01-24 12:46:28 -08:00
return true;
2023-01-09 17:51:44 -08:00
}
2023-01-10 09:57:07 -08:00
// Operator precedence: , > / > -
2023-01-09 17:51:44 -08:00
if (value.includes(",")) {
if (!allowSeparators) {
2023-01-24 12:46:28 -08:00
return false;
2023-01-09 17:51:44 -08:00
}
2023-01-24 12:46:28 -08:00
return value.split(",").every(v => v && validateCronPart(v, range));
2023-01-09 17:51:44 -08:00
}
if (value.includes("/")) {
if (!allowSeparators) {
2023-01-24 12:46:28 -08:00
return false;
2023-01-09 17:51:44 -08:00
}
2023-01-24 12:46:28 -08:00
const [start, step, ...rest] = value.split("/");
const stepNumber = +step;
2023-01-10 11:54:41 -08:00
if (rest.length > 0 || isNaN(stepNumber) || stepNumber <= 0 || !start || !step) {
2023-01-24 12:46:28 -08:00
return false;
2023-01-09 16:52:53 -08:00
}
2023-01-10 10:22:25 -08:00
// Separators are only allowed in the part before the `/`, e.g. `1-5/2`
2023-01-24 12:46:28 -08:00
return validateCronPart(start, range) && validateCronPart(step, range, false);
2023-01-09 16:52:53 -08:00
}
2023-01-09 17:51:44 -08:00
if (value.includes("-")) {
2023-01-10 09:57:07 -08:00
if (!allowSeparators) {
2023-01-24 12:46:28 -08:00
return false;
2023-01-10 09:57:07 -08:00
}
2023-01-24 12:46:28 -08:00
const [start, end, ...rest] = value.split("-");
2023-01-10 09:57:07 -08:00
if (rest.length > 0 || !start || !end) {
2023-01-24 12:46:28 -08:00
return false;
2023-01-10 09:57:07 -08:00
}
2023-01-09 17:51:44 -08:00
// Convert name to integers so we can make sure end >= start
2023-01-24 12:46:28 -08:00
const startNumber = convertToNumber(start, range.names);
const endNumber = convertToNumber(end, range.names);
return validateCronPart(start, range, false) && validateCronPart(end, range, false) && endNumber >= startNumber;
2023-01-09 16:52:53 -08:00
}
2023-01-09 17:51:44 -08:00
2023-01-24 12:46:28 -08:00
const number = +value;
return !isNaN(number) && number >= range.min && number <= range.max;
}
// Converts a string integer or a short name to a number
function convertToNumber(value: string, names?: Record<string, number>): number {
if (names && names[value.toLowerCase()] !== undefined) {
2023-01-24 12:46:28 -08:00
return +names[value.toLowerCase()];
} else {
return +value;
2023-01-09 17:51:44 -08:00
}
2023-01-24 12:46:28 -08:00
}