Format and remove unused code

This commit is contained in:
Jacob Wallraff
2023-01-24 12:46:28 -08:00
parent f2138b7eed
commit 0790776dea
4 changed files with 79 additions and 245 deletions
@@ -12,8 +12,8 @@ const MONTHS = {
sep: 9, sep: 9,
oct: 10, oct: 10,
nov: 11, nov: 11,
dec: 12, dec: 12
} };
const DAYS = { const DAYS = {
sun: 0, sun: 0,
@@ -22,11 +22,11 @@ const DAYS = {
wed: 3, wed: 3,
thu: 4, thu: 4,
fri: 5, fri: 5,
sat: 6, sat: 6
} };
export const MINUTE_RANGE = { min: 0, max: 59 } export const MINUTE_RANGE = {min: 0, max: 59};
export const HOUR_RANGE = { min: 0, max: 23 } export const HOUR_RANGE = {min: 0, max: 23};
export const DOM_RANGE = { min: 1, max: 31 } export const DOM_RANGE = {min: 1, max: 31};
export const MONTH_RANGE = { min: 1, max: 12, names: MONTHS } export const MONTH_RANGE = {min: 1, max: 12, names: MONTHS};
export const DOW_RANGE = { min: 0, max: 6, names: DAYS } export const DOW_RANGE = {min: 0, max: 6, names: DAYS};
@@ -1,4 +1,4 @@
import { isValidCron, getSchedule, getCronDescription } from "./cron" import {isValidCron, getCronDescription} from "./cron";
describe("cron", () => { describe("cron", () => {
describe("valid cron", () => { describe("valid cron", () => {
@@ -18,15 +18,15 @@ describe("cron", () => {
["0 0 * * SUN-TUE", "accepts day of week short name range"], ["0 0 * * SUN-TUE", "accepts day of week short name range"],
["0 0 * * SUN-2", "accepts day of week range combined with number"], ["0 0 * * SUN-2", "accepts day of week range combined with number"],
["0 2-4/5 * * *", "accepts range with step"], ["0 2-4/5 * * *", "accepts range with step"],
["0 0 * * *", "accepts multiple spaces"], ["0 0 * * *", "accepts multiple spaces"]
] ];
for (const [cron, reason] of valid) { for (const [cron, reason] of valid) {
it(`${cron} should be valid: ${reason}`, () => { it(`${cron} should be valid: ${reason}`, () => {
expect(isValidCron(cron)).toBe(true) expect(isValidCron(cron)).toBe(true);
}) });
} }
}) });
describe("invalid cron", () => { describe("invalid cron", () => {
const invalid = [ const invalid = [
@@ -54,106 +54,26 @@ describe("cron", () => {
["0 2/4-5 * * *", "step size may not be a range"], ["0 2/4-5 * * *", "step size may not be a range"],
["0 2-4-6 * * *", "range may not contain multiple -"], ["0 2-4-6 * * *", "range may not contain multiple -"],
["0 2/4/6 * * *", "step may not contain multiple /"], ["0 2/4/6 * * *", "step may not contain multiple /"],
["0 * * */FEB */TUE", "step size may not be a short name"], ["0 * * */FEB */TUE", "step size may not be a short name"]
] ];
for (const [cron, reason] of invalid) { for (const [cron, reason] of invalid) {
it(`${cron} should be invalid: ${reason}`, () => { it(`${cron} should be invalid: ${reason}`, () => {
expect(isValidCron(cron)).toBe(false) expect(isValidCron(cron)).toBe(false);
}) });
} }
}) });
describe("getSchedule", () => {
const testCases = [
{
cron: "0 1 2 3 4",
expected: {
minutes: [0],
hours: [1],
dom: [2],
months: [3],
dow: [4],
}
},
{
cron: "2,4 * * * *",
expected: {
minutes: [2, 4],
hours: undefined,
dom: undefined,
months: undefined,
dow: undefined,
}
},
{
cron: "2-4 * * * *",
expected: {
minutes: [2, 3, 4],
hours: undefined,
dom: undefined,
months: undefined,
dow: undefined,
}
},
{
cron: "*/15 * * * *",
expected: {
minutes: [0, 15, 30, 45],
hours: undefined,
dom: undefined,
months: undefined,
dow: undefined,
}
},
{
cron: "10/15 * * * *",
expected: {
minutes: [10, 25, 40, 55],
hours: undefined,
dom: undefined,
months: undefined,
dow: undefined,
}
},
{
cron: "5,*/20 * * * *",
expected: {
minutes: [0, 5, 20, 40],
hours: undefined,
dom: undefined,
months: undefined,
dow: undefined,
}
},
{
cron: "* * * JAN SUN",
expected: {
minutes: undefined,
hours: undefined,
dom: undefined,
months: [1],
dow: [0],
}
},
]
for (const testCase of testCases) {
it(`getSchedule '${testCase.cron}'`, () => {
expect(getSchedule(testCase.cron)).toEqual(testCase.expected)
})
}
})
describe("getCronDescription", () => { describe("getCronDescription", () => {
it(`Produces a sentence for valid cron`, () => { it(`Produces a sentence for valid cron`, () => {
expect(getCronDescription("0 * * * *")).toEqual("Runs every hour\n\n" + expect(getCronDescription("0 * * * *")).toEqual(
"Actions schedules run at most every 5 minutes. [Learn more](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#onschedule)" "Runs every hour\n\n" +
) "Actions schedules run at most every 5 minutes. [Learn more](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#onschedule)"
}) );
});
it(`Returns nothing for invalid cron`, () => { it(`Returns nothing for invalid cron`, () => {
expect(getCronDescription("* * * * * *")).toBeUndefined() expect(getCronDescription("* * * * * *")).toBeUndefined();
}) });
}) });
}) });
@@ -1,35 +1,29 @@
import cronstrue from 'cronstrue'; import cronstrue from "cronstrue";
import { import {MONTH_RANGE, HOUR_RANGE, MINUTE_RANGE, DOM_RANGE, DOW_RANGE} from "./cron-constants";
MONTH_RANGE,
HOUR_RANGE,
MINUTE_RANGE,
DOM_RANGE,
DOW_RANGE
} from './cron-constants'
type Range = { type Range = {
min: number min: number;
max: number max: number;
names?: Record<string, number> names?: Record<string, number>;
} };
type Schedule = { type Schedule = {
minutes?: number[] minutes?: number[];
hours?: number[] hours?: number[];
dom?: number[] dom?: number[];
months?: number[] months?: number[];
dow?: number[] dow?: number[];
} };
export function isValidCron(cron: string): boolean { export function isValidCron(cron: string): boolean {
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
const parts = cron.split(/ +/) const parts = cron.split(/ +/);
if (parts.length != 5) { if (parts.length != 5) {
return false return false;
} }
const [minutes, hours, dom, months, dow] = parts const [minutes, hours, dom, months, dow] = parts;
return ( return (
validateCronPart(minutes, MINUTE_RANGE) && validateCronPart(minutes, MINUTE_RANGE) &&
@@ -37,16 +31,15 @@ export function isValidCron(cron: string): boolean {
validateCronPart(dom, DOM_RANGE) && validateCronPart(dom, DOM_RANGE) &&
validateCronPart(months, MONTH_RANGE) && validateCronPart(months, MONTH_RANGE) &&
validateCronPart(dow, DOW_RANGE) validateCronPart(dow, DOW_RANGE)
) );
} }
export function getCronDescription(cronspec: string): string | undefined { export function getCronDescription(cronspec: string): string | undefined {
const schedule = getSchedule(cronspec) if (!isValidCron(cronspec)) {
if (!schedule) { return;
return
} }
let desc = '' let desc = "";
try { try {
desc = cronstrue.toString(cronspec, { desc = cronstrue.toString(cronspec, {
dayOfWeekStartIndexZero: true, dayOfWeekStartIndexZero: true,
@@ -54,156 +47,77 @@ export function getCronDescription(cronspec: string): string | undefined {
use24HourTimeFormat: true, use24HourTimeFormat: true,
// cronstrue sets the description as the error if throwExceptionOnParseError is false // cronstrue sets the description as the error if throwExceptionOnParseError is false
// so we need to distinguish between an error and a valid description // so we need to distinguish between an error and a valid description
throwExceptionOnParseError: true, throwExceptionOnParseError: true
}) });
} catch (err) { } catch (err) {
return return;
} }
// Make first character lowercase // Make first character lowercase
let result = "Runs " + desc.charAt(0).toLowerCase() + desc.slice(1) let result = "Runs " + desc.charAt(0).toLowerCase() + desc.slice(1);
result += "\n\nActions schedules run at most every 5 minutes." + result +=
"\n\nActions schedules run at most every 5 minutes." +
" [Learn more](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#onschedule)"; " [Learn more](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#onschedule)";
return result return result;
} }
function parseCronPart(part: string, range: Range): number[] | undefined { function validateCronPart(value: string, range: Range, allowSeparators = true): boolean {
const values: number[] = []
if (part === "*") {
return undefined
}
if (part.includes(",")) {
part.split(",").forEach((v) => {
const value = parseCronPart(v, range)
if (value) {
values.push(...value)
}
})
}
if (part.includes("/")) {
const [stepRange, step] = part.split("/")
const stepNumber = +step
let startNumber, endNumber
if (stepRange.includes("-")) {
const [start, end] = stepRange.split("-")
startNumber = convertToNumber(start, range.names)
endNumber = convertToNumber(end, range.names)
}
else {
if (stepRange === "*") {
startNumber = range.min
}
else {
startNumber = convertToNumber(stepRange, range.names)
}
endNumber = range.max
}
for (let i = startNumber; i <= endNumber; i += stepNumber) {
values.push(i)
}
}
if (part.includes("-")) {
const [start, end] = part.split("-")
const startNumber = convertToNumber(start, range.names)
const endNumber = convertToNumber(end, range.names)
for (let i = startNumber; i <= endNumber; i++) {
values.push(i)
}
}
const number = convertToNumber(part, range.names)
if (!isNaN(number)) {
values.push(number)
}
return values.sort((a, b) => a - b)
}
function validateCronPart(
value: string,
range: Range,
allowSeparators = true
): boolean {
if (range.names && range.names[value.toLowerCase()] !== undefined) { if (range.names && range.names[value.toLowerCase()] !== undefined) {
return true return true;
} }
if (value === "*") { if (value === "*") {
return true return true;
} }
// Operator precedence: , > / > - // Operator precedence: , > / > -
if (value.includes(",")) { if (value.includes(",")) {
if (!allowSeparators) { if (!allowSeparators) {
return false return false;
} }
return value.split(",").every((v) => v && validateCronPart(v, range)) return value.split(",").every(v => v && validateCronPart(v, range));
} }
if (value.includes("/")) { if (value.includes("/")) {
if (!allowSeparators) { if (!allowSeparators) {
return false return false;
} }
const [start, step, ...rest] = value.split("/") const [start, step, ...rest] = value.split("/");
const stepNumber = +step const stepNumber = +step;
if (rest.length > 0 || isNaN(stepNumber) || stepNumber <= 0 || !start || !step) { if (rest.length > 0 || isNaN(stepNumber) || stepNumber <= 0 || !start || !step) {
return false return false;
} }
// Separators are only allowed in the part before the `/`, e.g. `1-5/2` // Separators are only allowed in the part before the `/`, e.g. `1-5/2`
return ( return validateCronPart(start, range) && validateCronPart(step, range, false);
validateCronPart(start, range) && validateCronPart(step, range, false)
)
} }
if (value.includes("-")) { if (value.includes("-")) {
if (!allowSeparators) { if (!allowSeparators) {
return false return false;
} }
const [start, end, ...rest] = value.split("-") const [start, end, ...rest] = value.split("-");
if (rest.length > 0 || !start || !end) { if (rest.length > 0 || !start || !end) {
return false return false;
} }
// Convert name to integers so we can make sure end >= start // Convert name to integers so we can make sure end >= start
const startNumber = convertToNumber(start, range.names) const startNumber = convertToNumber(start, range.names);
const endNumber = convertToNumber(end, range.names) const endNumber = convertToNumber(end, range.names);
return ( return validateCronPart(start, range, false) && validateCronPart(end, range, false) && endNumber >= startNumber;
validateCronPart(start, range, false) && validateCronPart(end, range, false) && endNumber >= startNumber
)
} }
const number = +value const number = +value;
return !isNaN(number) && number >= range.min && number <= range.max return !isNaN(number) && number >= range.min && number <= range.max;
}
export function getSchedule(cron: string): Schedule | undefined {
if (!isValidCron(cron)) {
return
}
const [minutes, hours, dom, months, dow] = cron.split(/ +/)
return {
minutes: parseCronPart(minutes, MINUTE_RANGE),
hours: parseCronPart(hours, HOUR_RANGE),
dom: parseCronPart(dom, DOM_RANGE),
months: parseCronPart(months, MONTH_RANGE),
dow: parseCronPart(dow, DOW_RANGE)
}
} }
// Converts a string integer or a short name to a number // Converts a string integer or a short name to a number
function convertToNumber(value: string, names?: Record<string, number>): number { function convertToNumber(value: string, names?: Record<string, number>): number {
if (names && names[value.toLowerCase()] !== undefined) { if (names && names[value.toLowerCase()] !== undefined) {
return +names[value.toLowerCase()] return +names[value.toLowerCase()];
} else {
return +value;
} }
else { }
return +value
}
}
@@ -13,7 +13,7 @@ import {
TypesFilterConfig, TypesFilterConfig,
WorkflowFilterConfig WorkflowFilterConfig
} from "../workflow-template"; } from "../workflow-template";
import {isValidCron} from "./cron" import {isValidCron} from "./cron";
import {convertStringList} from "./string-list"; import {convertStringList} from "./string-list";
import {convertEventWorkflowDispatchInputs} from "./workflow-dispatch"; import {convertEventWorkflowDispatchInputs} from "./workflow-dispatch";
@@ -146,7 +146,7 @@ function convertSchedule(context: TemplateContext, token: SequenceToken): Schedu
const cron = schedule.value.assertString(`schedule cron`); const cron = schedule.value.assertString(`schedule cron`);
// Validate the cron string // Validate the cron string
if (!isValidCron(cron.value)) { if (!isValidCron(cron.value)) {
context.error(cron, "Invalid cron string") context.error(cron, "Invalid cron string");
} }
result.push({cron: cron.value}); result.push({cron: cron.value});
} else { } else {