diff --git a/actions-workflow-parser/src/model/converter/cron.test.ts b/actions-workflow-parser/src/model/converter/cron.test.ts new file mode 100644 index 0000000..6f5839f --- /dev/null +++ b/actions-workflow-parser/src/model/converter/cron.test.ts @@ -0,0 +1,43 @@ +import { isValidCron } from "./cron" + +describe("isValidCron", () => { + const valid = [ + "0 0 * * *", + "15 * * * *", + "2,10 4,5 * * *", + "30 4-6 * * *", + "20/15 * * * *", + "30 5,17 * * *", + "30 5 * * 1,3", + "30 5 * * 2,4", + "28 */4 * * *", + "28 5,*/4 * * *", + "28 5,*/4,6 * * *", + "28 5,*/4,6-8 * * *", + "0 0 * * SUN", + "0 0 * * SUN-TUE", + "0 0 * * SUN-2", + "0 * * */FEB */TUE", + ] + + for (const cron of valid) { + it(`${cron} should be valid`, () => { + expect(isValidCron(cron)).toBe(true) + }) + } + + const invalid = [ + "0 0 * *", + "0 0 * * BUN", + "0 0 * SUN JAN", + "0 0 * * FRI-TUE", + "0 12-4 * * *", + "0 */0 * * *" + ] + + for (const cron of invalid) { + it(`${cron} should be invalid`, () => { + expect(isValidCron(cron)).toBe(false) + }) + } +}) \ No newline at end of file diff --git a/actions-workflow-parser/src/model/converter/cron.ts b/actions-workflow-parser/src/model/converter/cron.ts index 5af4803..52b1cab 100644 --- a/actions-workflow-parser/src/model/converter/cron.ts +++ b/actions-workflow-parser/src/model/converter/cron.ts @@ -1,95 +1,111 @@ const MONTHS = { - jan: 1, - feb: 2, - mar: 3, - apr: 4, - may: 5, - jun: 6, - jul: 7, - aug: 8, - sep: 9, - oct: 10, - nov: 11, - dec: 12, + jan: 1, + feb: 2, + mar: 3, + apr: 4, + may: 5, + jun: 6, + jul: 7, + aug: 8, + sep: 9, + oct: 10, + nov: 11, + dec: 12, +} + +const DAYS = { + sun: 0, + mon: 1, + tue: 2, + wed: 3, + thu: 4, + fri: 5, + sat: 6, +} + +// TODO: make this parseCron +export function isValidCron(cron: string): boolean { + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule + + const parts = cron.split(" ") + if (parts.length != 5) { + return false } - - const DAYS = { - sun: 0, - mon: 1, - tue: 2, - wed: 3, - thu: 4, - fri: 5, - sat: 6, + + const [minutes, hours, dom, months, dow] = parts + + return ( + validateRange(minutes, { min: 0, max: 59 }) && + validateRange(hours, { min: 0, max: 23 }) && + validateRange(dom, { min: 1, max: 31 }) && + validateRange(months, { min: 1, max: 12, names: MONTHS }) && + validateRange(dow, { min: 0, max: 6, names: DAYS }) + ) +} + +type Range = { + min: number + max: number + names?: Record +} + +function validateRange( + value: string, + range: Range, + allowSeparators = true +): boolean { + if (range.names && range.names[value.toLowerCase()] !== undefined) { + return true } - - // TODO: make this parseCron - export function isValidCron(cron: string): boolean { - // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule - - const parts = cron.split(" ") - if (parts.length != 5) { + + if (value === "*") { + return true + } + + if (value.includes(",")) { + if (!allowSeparators) { + return false + } + // Allow separators + return value.split(",").every((v) => validateRange(v, range)) + } + + if (value.includes("/")) { + if (!allowSeparators) { + return false + } + + const [start, step, ...rest] = value.split("/") + // Supports */TUE and similar, which nees to be verified with the go cron library + const stepNumber = convertToNumber(range, step) + if (rest.length > 0 || stepNumber <= 0) { return false } - - const [minutes, hours, dom, months, dow] = parts - return ( - validateRange(minutes, { min: 0, max: 59 }) && - validateRange(hours, { min: 0, max: 23 }) && - validateRange(dom, { min: 1, max: 31 }) && - validateRange(months, { min: 1, max: 12, names: MONTHS }) && - validateRange(dow, { min: 0, max: 6, names: DAYS }) + validateRange(start, range, false) && validateRange(step, range, false) ) } - - type Range = { - min: number - max: number - names?: Record + + if (value.includes("-")) { + const [start, end] = value.split("-") + + // Convert name to integers so we can make sure end >= start + const startNumber = convertToNumber(range, start) + const endNumber = convertToNumber(range, end) + return ( + validateRange(start, range, false) && validateRange(end, range, false) && endNumber >= startNumber + ) } - - function validateRange( - value: string, - range: Range, - allowSeparators = true - ): boolean { - if (range.names && range.names[value.toLowerCase()] !== undefined) { - return true - } - - if (value === "*") { - return true - } - - if (value.includes(",")) { - if (!allowSeparators) { - return false - } - return value.split(",").every((v) => validateRange(v, range, false)) - } - - if (value.includes("/")) { - if (!allowSeparators) { - return false - } - - const [start, step, ...rest] = value.split("/") - if (rest.length > 0) { - return false - } - return ( - validateRange(start, range, false) && validateRange(step, range, false) - ) - } - - if (value.includes("-")) { - const [start, end] = value.split("-") - return ( - validateRange(start, range, false) && validateRange(end, range, false) - ) - } - - const number = parseInt(value, 10) - return !isNaN(number) && number >= range.min && number <= range.max - } \ No newline at end of file + + const number = +value + return !isNaN(number) && number >= range.min && number <= range.max +} + +function convertToNumber(range: Range, value: string): number { + if (range.names && range.names[value.toLowerCase()] !== undefined) { + return +range.names[value.toLowerCase()] + } + else { + return +value + } +} \ No newline at end of file