diff --git a/lib/types/later.d.ts b/lib/types/later.d.ts index ec2cd572dd882d3838877dad016ff8030527ef17..ae691223a8387b0a187ce4b7563fbe3e1f98944a 100644 --- a/lib/types/later.d.ts +++ b/lib/types/later.d.ts @@ -101,7 +101,36 @@ declare module '@breejs/later' { [timeperiodAndModifierName: string]: number[] | undefined; } - const later: { parse: { text: (s: string) => ScheduleData } }; + interface Schedule { + /** + * True if the specified value is valid for the specified date, false otherwise. + * + * @param date - The given date. + */ + isValid(date: Date): boolean; + } + + const later: { + /** + * Parse + * For generating schedule data. + */ + parse: { + /** + * Create schedule data by paring a human readable string. + * + * @param [input] - A string value to parse. + */ + text: (s: string) => ScheduleData; + }; + + /** + * Compiles schedule instances from schedule data. + * + * @param data - The given schedule data. + */ + schedule(data: ScheduleData): Schedule; + }; export = later; } diff --git a/lib/workers/branch/schedule.spec.ts b/lib/workers/branch/schedule.spec.ts index 0b0978d9a3dd9a0b31e552d80ab9d4638cc8c156..0ad1dfb7f00446202c406ad93f40087fd1248d6e 100644 --- a/lib/workers/branch/schedule.spec.ts +++ b/lib/workers/branch/schedule.spec.ts @@ -156,12 +156,30 @@ describe(getName(), () => { const res = schedule.isScheduledNow(config); expect(res).toBe(false); }); - it('supports timezone', () => { - config.schedule = ['after 4:00pm']; - config.timezone = 'Asia/Singapore'; - mockDate.set('2017-06-30T10:50:00.000Z'); // Globally 2017-06-30 10:50am - const res = schedule.isScheduledNow(config); - expect(res).toBe(true); + describe('supports timezone', () => { + const cases: [string, string, string, boolean][] = [ + ['after 4pm', 'Asia/Singapore', '2017-06-30T15:59:00.000+0800', false], + ['after 4pm', 'Asia/Singapore', '2017-06-30T16:01:00.000+0800', true], + [ + 'before 3am on Monday', + 'Asia/Tokyo', + '2017-06-26T02:59:00.000+0900', + true, + ], + [ + 'before 3am on Monday', + 'Asia/Tokyo', + '2017-06-26T03:01:00.000+0900', + false, + ], + ]; + + test.each(cases)('%p, %p, %p', (sched, tz, datetime, expected) => { + config.schedule = [sched]; + config.timezone = tz; + mockDate.set(datetime); + expect(schedule.isScheduledNow(config)).toBe(expected); + }); }); it('supports multiple schedules', () => { config.schedule = ['after 4:00pm', 'before 11:00am']; @@ -257,5 +275,19 @@ describe(getName(), () => { const res = schedule.isScheduledNow(config); expect(res).toBe(false); }); + it('supports weekday instances', () => { + config.schedule = ['on Monday on the first day instance']; + + const cases: [string, boolean][] = [ + ['2017-02-01T06:00:00.000', false], // Locally Thursday, 2 February 2017 6am + ['2017-02-06T06:00:00.000', true], // Locally Monday, 6 February 2017 6am + ['2017-02-13T06:00:00.000', false], // Locally Monday, 13 February 2017 6am + ]; + + cases.forEach(([datetime, expected]) => { + mockDate.set(datetime); + expect(schedule.isScheduledNow(config)).toBe(expected); + }); + }); }); }); diff --git a/lib/workers/branch/schedule.ts b/lib/workers/branch/schedule.ts index ec34edaff02a6f32d7fd9894bcf587f9ceec2fa0..443510053c43ef4faca0cec13bbe28c66fcf28bc 100644 --- a/lib/workers/branch/schedule.ts +++ b/lib/workers/branch/schedule.ts @@ -115,76 +115,23 @@ export function isScheduledNow(config: RenovateConfig): boolean { logger.trace(`currentSeconds=${currentSeconds}`); // Support a single string but massage to array for processing logger.debug(`Checking ${configSchedule.length} schedule(s)`); + + // later is timezone agnostic (as in, it purely relies on the underlying UTC date/time that is stored in the Date), + // which means we have to pass it a Date that has an underlying UTC date/time in the same timezone as the schedule + const jsNow = now.setZone('utc', { keepLocalTime: true }).toJSDate(); + // We run if any schedule matches const isWithinSchedule = configSchedule.some((scheduleText) => { const massagedText = scheduleMappings[scheduleText] || scheduleText; const parsedSchedule = later.parse.text(fixShortHours(massagedText)); logger.debug({ parsedSchedule }, `Checking schedule "${scheduleText}"`); - // Later library returns array of schedules - return parsedSchedule.schedules.some((schedule) => { - // Check if months are defined - if (schedule.M) { - const currentMonth = now.month; - if (!schedule.M.includes(currentMonth)) { - logger.debug( - `Does not match schedule because ${currentMonth} is not in ${String( - schedule.M - )}` - ); - return false; - } - } - // Check if days are defined - if (schedule.d) { - // We need to map because 'luxon' uses monday as first day - // and later uses sundays as first day of week - // http://bunkat.github.io/later/time-periods.html#day-of-week - const dowMap = [6, 7, 1, 2, 3, 4, 5, 6]; - const scheduledDays = schedule.d.map((day) => dowMap[day]); - logger.trace({ scheduledDays }, `scheduledDays`); - if (!scheduledDays.includes(currentDay)) { - logger.debug( - `Does not match schedule because ${currentDay} is not in ${String( - scheduledDays - )}` - ); - return false; - } - } - if (schedule.D) { - logger.debug({ schedule_D: schedule.D }, `schedule.D`); - const currentDayOfMonth = now.day; - if (!schedule.D.includes(currentDayOfMonth)) { - return false; - } - } - // Check for start time - if (schedule.t_a) { - const startSeconds = schedule.t_a[0]; - if (currentSeconds < startSeconds) { - logger.debug( - `Does not match schedule because ${currentSeconds} is earlier than ${startSeconds}` - ); - return false; - } - } - // Check for end time - if (schedule.t_b) { - const endSeconds = schedule.t_b[0]; - if (currentSeconds > endSeconds) { - logger.debug( - `Does not match schedule because ${currentSeconds} is later than ${endSeconds}` - ); - return false; - } - } - // Check for week of year - if (schedule.wy && !schedule.wy.includes(now.weekNumber)) { - return false; - } + + if (later.schedule(parsedSchedule).isValid(jsNow)) { logger.debug(`Matches schedule ${scheduleText}`); return true; - }); + } + + return false; }); if (!isWithinSchedule) { logger.debug('Package not scheduled');