Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ inputs:
schedules:
description: 'An array of strings representing the meeting schedules in the format YYYY-MM-DD (ISO-8601 intervals).'
required: true
timezone:
description: 'Timezone to use for schedule times (e.g. "America/Chicago", "Europe/Madrid"). Useful for following daylight saving time (DST) / summer time. Default: "UTC"'
required: false
createWithin:
description: 'Defines when the meeting issues are created using ISO-8601 durations. Defaults to one week before the meeting (Using the ISO-8601 durations format, this is P7D).'
default: 'P7D'
Expand Down
25 changes: 20 additions & 5 deletions lib/meetings.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ module.exports.setMeetingIssueBody = async function (client, opts) {

function getNextIssue (opts) {
const now = opts.now || Temporal.Now.instant()
const date = getNextScheduledMeeting(opts.schedules, now)
const date = getNextScheduledMeeting(opts.schedules, now, opts.timezone)
const title = typeof opts.issueTitle === 'function' ? opts.issueTitle({ date }) : opts.issueTitle

const issue = {
Expand Down Expand Up @@ -76,18 +76,33 @@ const shouldCreateNextMeetingIssue = module.exports.shouldCreateNextMeetingIssue
return issue
}

const getNextScheduledMeeting = module.exports.getNextScheduledMeeting = function (schedules = [], now = Temporal.Now.instant()) {
const getNextScheduledMeeting = module.exports.getNextScheduledMeeting = function (schedules = [], now = Temporal.Now.instant(), timezone) {
return schedules
.map((s = `${now}/P7D`) => {
// Parse interval
const [startStr, durationStr] = s.split('/')
const start = Temporal.Instant.from(startStr)
const duration = Temporal.Duration.from(durationStr)
let next

if (timezone) {
// parse as local in specified tz
try {
const zonedStart = Temporal.ZonedDateTime.from(`${startStr}[${timezone}]`)
next = zonedStart.toInstant()
} catch (e) {
console.warn(`invalid timezone '${timezone}'; using UTC`)
console.error(e) // s/b caused by invalid timezone but log error just in case for troubleshooting
}
}

if (!next) {
// parse as UTC
next = Temporal.Instant.from(startStr)
}

// Get datetime for next event after now
let next = start
while (next.epochMilliseconds <= now.epochMilliseconds) {
const zonedNext = next.toZonedDateTimeISO('UTC')
const zonedNext = next.toZonedDateTimeISO(timezone || 'UTC')
next = zonedNext.add(duration).toInstant()
}

Expand Down
2 changes: 2 additions & 0 deletions run.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const pkg = require('./package.json')
// variables we use for timing
const schedules = list(core.getInput('schedules'))
const createWithin = core.getInput('createWithin')
const timezone = core.getInput('timezone')

// variables we use for labels
const agendaLabel = core.getInput('agendaLabel')
Expand Down Expand Up @@ -115,6 +116,7 @@ const pkg = require('./package.json')
const opts = {
...repo,
schedules,
timezone,
meetingLabels,
createWithin,
agendaLabel,
Expand Down
65 changes: 65 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,71 @@ suite(`${pkg.name} unit`, () => {
assert(formattedDate.includes('Mar'))
assert(formattedDate.includes('08:00 AM'))
})

test('schedule parsing with timezone - UTC default', () => {
const schedules = ['2024-03-27T21:00:00.0Z/P2W']
const now = Temporal.Instant.from('2024-03-20T13:00:00.0Z')
const next = meetings.getNextScheduledMeeting(schedules, now)
const expected = Temporal.Instant.from('2024-03-27T21:00:00.0Z')
assert.strictEqual(next.epochMilliseconds, expected.epochMilliseconds)
})

test('schedule parsing with timezone - Chicago', () => {
const schedules = ['2024-03-27T21:00:00.0/P2W']
const now = Temporal.Instant.from('2024-03-20T13:00:00.0Z')
const next = meetings.getNextScheduledMeeting(schedules, now, 'America/Chicago')
// 21:00 CDT == 02:00 UTC next day
const expected = Temporal.Instant.from('2024-03-28T02:00:00.0Z')
assert.strictEqual(next.epochMilliseconds, expected.epochMilliseconds)
})

test('schedule parsing with timezone - DST spring forward', () => {
const schedules = ['2025-03-09T01:00:00.0/P2W']

const beforeSpring = Temporal.Instant.from('2025-03-08T13:00:00.0Z')
const nextBefore = meetings.getNextScheduledMeeting(schedules, beforeSpring, 'America/Chicago')
// 01:00 CST == 07:00 UTC (before spring forward)
const expectedBefore = Temporal.Instant.from('2025-03-09T07:00:00.0Z')
assert.strictEqual(nextBefore.epochMilliseconds, expectedBefore.epochMilliseconds)

const afterSpring = Temporal.Instant.from('2025-03-09T13:00:00.0Z')
const nextAfter = meetings.getNextScheduledMeeting(schedules, afterSpring, 'America/Chicago')
// 01:00 CDT == 06:00 UTC (after spring forward)
const expectedAfter = Temporal.Instant.from('2025-03-23T06:00:00.0Z')
assert.strictEqual(nextAfter.epochMilliseconds, expectedAfter.epochMilliseconds)
})

test('schedule parsing with timezone - DST fall back', () => {
const schedules = ['2025-11-02T01:00:00.0/P2W']

const beforeFallBack = Temporal.Instant.from('2025-11-01T13:00:00.0Z')
const nextBefore = meetings.getNextScheduledMeeting(schedules, beforeFallBack, 'America/Chicago')
// 01:00 CDT == 06:00 UTC (before fall back)
const expectedBefore = Temporal.Instant.from('2025-11-02T06:00:00.0Z')
assert.strictEqual(nextBefore.epochMilliseconds, expectedBefore.epochMilliseconds)

const afterFallBack = Temporal.Instant.from('2025-11-02T13:00:00.0Z')
const nextAfter = meetings.getNextScheduledMeeting(schedules, afterFallBack, 'America/Chicago')
// 01:00 CST == 07:00 UTC (after fall back)
const expectedAfter = Temporal.Instant.from('2025-11-16T07:00:00.0Z')
assert.strictEqual(nextAfter.epochMilliseconds, expectedAfter.epochMilliseconds)
})

test('schedule parsing with timezone - Los Angeles PST', () => {
const schedules = ['2020-04-02T17:00:00/P7D']
const now = Temporal.Instant.from('2021-01-01T00:00:00.0Z')
const next = meetings.getNextScheduledMeeting(schedules, now, 'America/Los_Angeles')
const expected = Temporal.Instant.from('2020-12-31T17:00:00.0-08:00')
assert.strictEqual(next.epochMilliseconds, expected.epochMilliseconds)
})

test('schedule parsing with timezone - Los Angeles PDT', () => {
const schedules = ['2020-04-02T17:00:00/P7D']
const now = Temporal.Instant.from('2021-04-01T00:00:00.0Z')
const next = meetings.getNextScheduledMeeting(schedules, now, 'America/Los_Angeles')
const expected = Temporal.Instant.from('2021-04-01T17:00:00.0-07:00')
assert.strictEqual(next.epochMilliseconds, expected.epochMilliseconds)
})
})

test('shorthands transform', async () => {
Expand Down