diff --git a/actions-workflow-parser/src/model/converter/cron-constants.ts b/actions-workflow-parser/src/model/converter/cron-constants.ts index 02df399..f24a52f 100644 --- a/actions-workflow-parser/src/model/converter/cron-constants.ts +++ b/actions-workflow-parser/src/model/converter/cron-constants.ts @@ -12,8 +12,8 @@ const MONTHS = { sep: 9, oct: 10, nov: 11, - dec: 12, -} + dec: 12 +}; const DAYS = { sun: 0, @@ -22,11 +22,11 @@ const DAYS = { wed: 3, thu: 4, fri: 5, - sat: 6, -} + sat: 6 +}; -export const MINUTE_RANGE = { min: 0, max: 59 } -export const HOUR_RANGE = { min: 0, max: 23 } -export const DOM_RANGE = { min: 1, max: 31 } -export const MONTH_RANGE = { min: 1, max: 12, names: MONTHS } -export const DOW_RANGE = { min: 0, max: 6, names: DAYS } \ No newline at end of file +export const MINUTE_RANGE = {min: 0, max: 59}; +export const HOUR_RANGE = {min: 0, max: 23}; +export const DOM_RANGE = {min: 1, max: 31}; +export const MONTH_RANGE = {min: 1, max: 12, names: MONTHS}; +export const DOW_RANGE = {min: 0, max: 6, names: DAYS}; diff --git a/actions-workflow-parser/src/model/converter/cron.test.ts b/actions-workflow-parser/src/model/converter/cron.test.ts index 56a2a3e..18075a9 100644 --- a/actions-workflow-parser/src/model/converter/cron.test.ts +++ b/actions-workflow-parser/src/model/converter/cron.test.ts @@ -1,4 +1,4 @@ -import { isValidCron, getSchedule, getCronDescription } from "./cron" +import {isValidCron, getCronDescription} from "./cron"; describe("cron", () => { describe("valid cron", () => { @@ -18,15 +18,15 @@ describe("cron", () => { ["0 0 * * SUN-TUE", "accepts day of week short name range"], ["0 0 * * SUN-2", "accepts day of week range combined with number"], ["0 2-4/5 * * *", "accepts range with step"], - ["0 0 * * *", "accepts multiple spaces"], - ] + ["0 0 * * *", "accepts multiple spaces"] + ]; for (const [cron, reason] of valid) { it(`${cron} should be valid: ${reason}`, () => { - expect(isValidCron(cron)).toBe(true) - }) + expect(isValidCron(cron)).toBe(true); + }); } - }) + }); describe("invalid cron", () => { const invalid = [ @@ -54,106 +54,26 @@ describe("cron", () => { ["0 2/4-5 * * *", "step size may not be a range"], ["0 2-4-6 * * *", "range 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) { 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", () => { it(`Produces a sentence for valid cron`, () => { - expect(getCronDescription("0 * * * *")).toEqual("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)" - ) - }) + expect(getCronDescription("0 * * * *")).toEqual( + "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`, () => { - expect(getCronDescription("* * * * * *")).toBeUndefined() - }) - }) -}) \ No newline at end of file + expect(getCronDescription("* * * * * *")).toBeUndefined(); + }); + }); +}); diff --git a/actions-workflow-parser/src/model/converter/cron.ts b/actions-workflow-parser/src/model/converter/cron.ts index 1bc2572..5ff75bc 100644 --- a/actions-workflow-parser/src/model/converter/cron.ts +++ b/actions-workflow-parser/src/model/converter/cron.ts @@ -1,35 +1,29 @@ -import cronstrue from 'cronstrue'; +import cronstrue from "cronstrue"; -import { - MONTH_RANGE, - HOUR_RANGE, - MINUTE_RANGE, - DOM_RANGE, - DOW_RANGE -} from './cron-constants' +import {MONTH_RANGE, HOUR_RANGE, MINUTE_RANGE, DOM_RANGE, DOW_RANGE} from "./cron-constants"; type Range = { - min: number - max: number - names?: Record -} + min: number; + max: number; + names?: Record; +}; type Schedule = { - minutes?: number[] - hours?: number[] - dom?: number[] - months?: number[] - dow?: number[] -} + minutes?: number[]; + hours?: number[]; + dom?: number[]; + months?: number[]; + dow?: number[]; +}; export function isValidCron(cron: string): boolean { // 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) { - return false + return false; } - const [minutes, hours, dom, months, dow] = parts + const [minutes, hours, dom, months, dow] = parts; return ( validateCronPart(minutes, MINUTE_RANGE) && @@ -37,16 +31,15 @@ export function isValidCron(cron: string): boolean { validateCronPart(dom, DOM_RANGE) && validateCronPart(months, MONTH_RANGE) && validateCronPart(dow, DOW_RANGE) - ) + ); } export function getCronDescription(cronspec: string): string | undefined { - const schedule = getSchedule(cronspec) - if (!schedule) { - return + if (!isValidCron(cronspec)) { + return; } - let desc = '' + let desc = ""; try { desc = cronstrue.toString(cronspec, { dayOfWeekStartIndexZero: true, @@ -54,156 +47,77 @@ export function getCronDescription(cronspec: string): string | undefined { 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 - throwExceptionOnParseError: true, - }) + throwExceptionOnParseError: true + }); } catch (err) { - return + return; } // Make first character lowercase - let result = "Runs " + desc.charAt(0).toLowerCase() + desc.slice(1) - result += "\n\nActions schedules run at most every 5 minutes." + + let result = "Runs " + desc.charAt(0).toLowerCase() + desc.slice(1); + 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)"; - return result + return result; } -function parseCronPart(part: string, range: Range): number[] | undefined { - 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 { +function validateCronPart(value: string, range: Range, allowSeparators = true): boolean { if (range.names && range.names[value.toLowerCase()] !== undefined) { - return true + return true; } if (value === "*") { - return true + return true; } // Operator precedence: , > / > - if (value.includes(",")) { 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 (!allowSeparators) { - return false + return false; } - const [start, step, ...rest] = value.split("/") - const stepNumber = +step + const [start, step, ...rest] = value.split("/"); + const stepNumber = +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` - return ( - validateCronPart(start, range) && validateCronPart(step, range, false) - ) + return validateCronPart(start, range) && validateCronPart(step, range, false); } if (value.includes("-")) { if (!allowSeparators) { - return false + return false; } - const [start, end, ...rest] = value.split("-") + const [start, end, ...rest] = value.split("-"); if (rest.length > 0 || !start || !end) { - return false + return false; } // Convert name to integers so we can make sure end >= start - const startNumber = convertToNumber(start, range.names) - const endNumber = convertToNumber(end, range.names) - return ( - validateCronPart(start, range, false) && validateCronPart(end, range, false) && endNumber >= startNumber - ) + const startNumber = convertToNumber(start, range.names); + const endNumber = convertToNumber(end, range.names); + return validateCronPart(start, range, false) && validateCronPart(end, range, false) && endNumber >= startNumber; } - const number = +value - 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) - } + 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): number { if (names && names[value.toLowerCase()] !== undefined) { - return +names[value.toLowerCase()] + return +names[value.toLowerCase()]; + } else { + return +value; } - else { - return +value - } -} \ No newline at end of file +} diff --git a/actions-workflow-parser/src/model/converter/events.ts b/actions-workflow-parser/src/model/converter/events.ts index bb3e956..1db676f 100644 --- a/actions-workflow-parser/src/model/converter/events.ts +++ b/actions-workflow-parser/src/model/converter/events.ts @@ -13,7 +13,7 @@ import { TypesFilterConfig, WorkflowFilterConfig } from "../workflow-template"; -import {isValidCron} from "./cron" +import {isValidCron} from "./cron"; import {convertStringList} from "./string-list"; import {convertEventWorkflowDispatchInputs} from "./workflow-dispatch"; @@ -146,7 +146,7 @@ function convertSchedule(context: TemplateContext, token: SequenceToken): Schedu const cron = schedule.value.assertString(`schedule cron`); // Validate the cron string if (!isValidCron(cron.value)) { - context.error(cron, "Invalid cron string") + context.error(cron, "Invalid cron string"); } result.push({cron: cron.value}); } else {