Format and remove unused code
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user