-
Notifications
You must be signed in to change notification settings - Fork 26
chore: store release schedule without timezone #878
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class ChangeKickoffAtToKickoffTime < ActiveRecord::Migration[7.0] | ||
| def up | ||
| # Step 1: Add new kickoff_time column (time without timezone) | ||
| add_column :trains, :kickoff_time, :time | ||
|
|
||
| # Step 2: Migrate existing data | ||
| # Convert kickoff_at (UTC datetime) to time in app's timezone | ||
| reversible do |dir| | ||
| dir.up do | ||
| Train.reset_column_information | ||
| Train.find_each do |train| | ||
| next if train.kickoff_at.blank? | ||
|
|
||
| # Convert kickoff_at (UTC) to app's timezone and extract time component | ||
| time_in_app_tz = train.kickoff_at.in_time_zone(train.app.timezone) | ||
| train.update_column(:kickoff_time, time_in_app_tz.strftime("%H:%M:%S")) | ||
| end | ||
| end | ||
| end | ||
|
Comment on lines
+10
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainDown-migration parsing bug for kickoff_time + minor migration cleanup Two points here:
Also applies to: 34-52 🏁 Script executed: #!/bin/bash
# Retrieve the full migration file to inspect down block
find . -name "20251118165553_change_kickoff_at_to_kickoff_time.rb" -type f -exec cat {} \;Length of output: 1764 🏁 Script executed: #!/bin/bash
# Find Train model to check next_run_at method
fd -e rb -path "*/models/*" -name "*train*" -type f | head -20Length of output: 168 🏁 Script executed: #!/bin/bash
# Search for next_run_at method in Train model
rg -n "def next_run_at" --type=rb -A 5Length of output: 26 🏁 Script executed: #!/bin/bash
# Check how time columns are typically used in migrations/models
rg -n "\.strftime\(" --type=rb -C 2 | grep -A 2 -B 2 "time"Length of output: 26 🏁 Script executed: #!/bin/bash
# Find Train model files
find . -name "*train*" -type f | grep -E "models?/" | head -10Length of output: 49 🏁 Script executed: #!/bin/bash
# Search for next_run_at method without type flag
rg -n "def next_run_at" -A 5Length of output: 649 🏁 Script executed: #!/bin/bash
# Search for how time columns are used in codebase
rg -n "kickoff_time" -C 3Length of output: 20885 🏁 Script executed: #!/bin/bash
# Look for strftime usage with time values to understand pattern
rg "strftime.*%H:%M:%S" -C 2Length of output: 8379 Down-migration parsing bug for kickoff_time + minor migration cleanup Two points here:
🤖 Prompt for AI Agents |
||
|
|
||
| # Step 3: Remove old column | ||
| safety_assured { remove_column :trains, :kickoff_at } | ||
| end | ||
|
|
||
| def down | ||
| # Add back the datetime column | ||
| add_column :trains, :kickoff_at, :datetime | ||
|
|
||
| # Attempt to reverse the migration | ||
| # Note: We can't perfectly reverse since we lose the date component | ||
| # We'll set it to the next occurrence from now | ||
| reversible do |dir| | ||
| dir.down do | ||
| Train.reset_column_information | ||
| Train.find_each do |train| | ||
| next if train.kickoff_time.blank? | ||
|
|
||
| # Set to next occurrence from now in app's timezone | ||
| tz = train.app.timezone | ||
| now = Time.current.in_time_zone(tz) | ||
| kickoff_today = tz.parse("#{now.to_date} #{train.kickoff_time}") | ||
| kickoff_datetime = kickoff_today > now ? kickoff_today : kickoff_today + 1.day | ||
|
|
||
| train.update_column(:kickoff_at, kickoff_datetime.utc) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| # Remove the time column | ||
| safety_assured { remove_column :trains, :kickoff_time } | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Ensure recurring runs stay at the same local time across DST + address lint hint
The new
next_run_atcorrectly usesapp.timezonefor the first occurrence, but when there are existingscheduled_releasesit does:Because
scheduled_atis stored in UTC, subsequent runs are computed purely in UTC. On DST transitions this will shift the wall-clock time (e.g., daily runs may move by ±1 hour), which conflicts with the stated goal of “same local time regardless of DST”.You can keep all recurrence math in the app’s timezone (for both first and subsequent runs) and only convert to UTC at the end. This also lets you reuse the same
now_in_tzand fixes the string-literal-in-interpolation lint by switching to double quotes:def next_run_at return unless automatic? - # Get the app's timezone - tz = app.timezone - - # If we have previous scheduled releases, calculate next occurrence from last scheduled time - if scheduled_releases.any? - base_time = scheduled_releases.last.scheduled_at - next_time = base_time + repeat_duration - - # Keep adding repeat_duration until we get a future time - while next_time <= Time.current - next_time += repeat_duration - end - - return next_time.utc - end - - # First time: combine kickoff_time with today's date in app timezone - now_in_tz = Time.current.in_time_zone(tz) - - # Parse kickoff_time as a time today in the app's timezone - # kickoff_time is stored as HH:MM:SS - kickoff_today = tz.parse("#{now_in_tz.to_date} #{kickoff_time.strftime('%H:%M:%S')}") - - # If kickoff_today already passed, start from the next occurrence - next_time = kickoff_today - while next_time <= now_in_tz - next_time += repeat_duration - end - - next_time.utc + tz = app.timezone + now_in_tz = Time.current.in_time_zone(tz) + + if scheduled_releases.any? + # Work in app timezone so recurring runs stay at the same local time + base_time_in_tz = scheduled_releases.last.scheduled_at.in_time_zone(tz) + next_time_in_tz = base_time_in_tz + repeat_duration + + while next_time_in_tz <= now_in_tz + next_time_in_tz += repeat_duration + end + + return next_time_in_tz.utc + end + + # First time: combine kickoff_time with today's date in app timezone + kickoff_today = tz.parse("#{now_in_tz.to_date} #{kickoff_time.strftime("%H:%M:%S")}") + + next_time_in_tz = kickoff_today + while next_time_in_tz <= now_in_tz + next_time_in_tz += repeat_duration + end + + next_time_in_tz.utc endThis keeps all scheduled runs anchored to the same local wall-clock time in each app’s timezone and also satisfies the
StringLiteralsInInterpolationlint.I'll verify the review comment by examining the actual code and understanding the timezone/DST concerns.
🏁 Script executed:
Length of output: 1030
🏁 Script executed:
Length of output: 1655
🏁 Script executed:
Length of output: 4059
🏁 Script executed:
Length of output: 19
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 1078
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 2907
🏁 Script executed:
Length of output: 544
🏁 Script executed:
Length of output: 1127
🏁 Script executed:
Length of output: 240
🏁 Script executed:
Length of output: 5199
🏁 Script executed:
Length of output: 1083
🏁 Script executed:
Length of output: 1358
🏁 Script executed:
Length of output: 997
🏁 Script executed:
Length of output: 222
🏁 Script executed:
Length of output: 144
🏁 Script executed:
Length of output: 33
🏁 Script executed:
Length of output: 4062
🏁 Script executed:
Length of output: 130
🏁 Script executed:
Length of output: 312
🏁 Script executed:
Length of output: 1550
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 143
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 356
🏁 Script executed:
Length of output: 680
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 2
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 93
🏁 Script executed:
Length of output: 97
🏁 Script executed:
Length of output: 1030
🏁 Script executed:
Length of output: 23
🏁 Script executed:
Length of output: 11
🏁 Script executed:
Length of output: 1287
🏁 Script executed:
Length of output: 1222
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 126
🏁 Script executed:
Length of output: 172
🏁 Script executed:
Length of output: 334
🏁 Script executed:
Length of output: 2898
🏁 Script executed:
Length of output: 3442
🏁 Script executed:
Length of output: 21
🏁 Script executed:
Length of output: 3149
🏁 Script executed:
Length of output: 930
🏁 Script executed:
Length of output: 45
🏁 Script executed:
Length of output: 1101
🏁 Script executed:
Length of output: 12
🏁 Script executed:
Length of output: 281
Recurring runs will drift from kickoff time across DST transitions; consolidate timezone handling and fix lint issue
The new
next_run_atcorrectly usesapp.timezonefor the first occurrence, but whenscheduled_releasesexist, it performs all recurrence math in UTC:Because
scheduled_atis stored as UTC, subsequent runs bypass timezone logic. On DST transitions this shifts the wall-clock time (e.g., a daily 9 AM run may become 10 AM after spring forward), contradicting the stated goal of "same local time regardless of DST".Convert the
scheduled_releasesbranch to use app timezone for all recurrence math and only convert to UTC at the end:def next_run_at return unless automatic? - # Get the app's timezone - tz = app.timezone - - # If we have previous scheduled releases, calculate next occurrence from last scheduled time - if scheduled_releases.any? - base_time = scheduled_releases.last.scheduled_at - next_time = base_time + repeat_duration - - # Keep adding repeat_duration until we get a future time - while next_time <= Time.current - next_time += repeat_duration - end - - return next_time.utc - end - - # First time: combine kickoff_time with today's date in app timezone - now_in_tz = Time.current.in_time_zone(tz) - - # Parse kickoff_time as a time today in the app's timezone - # kickoff_time is stored as HH:MM:SS - kickoff_today = tz.parse("#{now_in_tz.to_date} #{kickoff_time.strftime('%H:%M:%S')}") - - # If kickoff_today already passed, start from the next occurrence - next_time = kickoff_today - while next_time <= now_in_tz - next_time += repeat_duration - end - - next_time.utc + tz = app.timezone + now_in_tz = Time.current.in_time_zone(tz) + + if scheduled_releases.any? + # Work in app timezone so recurring runs stay at the same local time + base_time_in_tz = scheduled_releases.last.scheduled_at.in_time_zone(tz) + next_time_in_tz = base_time_in_tz + repeat_duration + + while next_time_in_tz <= now_in_tz + next_time_in_tz += repeat_duration + end + + return next_time_in_tz.utc + end + + # First time: combine kickoff_time with today's date in app timezone + kickoff_today = tz.parse("#{now_in_tz.to_date} #{kickoff_time.strftime("%H:%M:%S")}") + + next_time_in_tz = kickoff_today + while next_time_in_tz <= now_in_tz + next_time_in_tz += repeat_duration + end + + next_time_in_tz.utc endThis keeps all scheduled runs anchored to the same local wall-clock time in the app's timezone and also addresses the
StringLiteralsInInterpolationlint hint by using double quotes.🧰 Tools
🪛 GitHub Check: lint
[failure] 243-243:
Style/StringLiteralsInInterpolation: Prefer double-quoted strings inside interpolations.
🤖 Prompt for AI Agents