Skip to content

Commit ecadfe8

Browse files
ManmathXNssGourav
authored andcommitted
Fix/dynamic call reminder time (hiero-ledger#2213)
Signed-off-by: ManmathX <manmath2006n@gmail.com>
1 parent 55e9171 commit ecadfe8

4 files changed

Lines changed: 359 additions & 4 deletions

File tree

.github/scripts/cron-calls-community.sh

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ DRY_RUN="${DRY_RUN:-true}"
66
ANCHOR_DATE="2025-11-12"
77
MEETING_LINK="https://zoom-lfx.platform.linuxfoundation.org/meeting/92041330205?password=2f345bee-0c14-4dd5-9883-06fbc9c60581"
88
CALENDAR_LINK="https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week"
9+
MEETING_HOUR="${MEETING_HOUR:-14}" # 14:00 UTC — change this to adjust the meeting time
10+
11+
if ! [[ "$MEETING_HOUR" =~ ^[0-9]+$ ]] || [ "$MEETING_HOUR" -lt 0 ] || [ "$MEETING_HOUR" -gt 23 ]; then
12+
echo "ERROR: MEETING_HOUR must be an integer between 0 and 23, got '$MEETING_HOUR'."
13+
exit 1
14+
fi
915

1016
CANCELLED_DATES=(
1117
"2025-12-24"
@@ -63,15 +69,25 @@ if [ -z "$ISSUE_DATA" ] || [ "$ISSUE_DATA" = "[]" ]; then
6369
exit 0
6470
fi
6571

72+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
73+
TIME_UNTIL_MEETING=$(python3 "$SCRIPT_DIR/utils/compute-time-until-meeting.py" "$TODAY" "$MEETING_HOUR")
74+
75+
if [ "$TIME_UNTIL_MEETING" = "has already started" ]; then
76+
echo "Meeting has already started. Skipping reminder."
77+
exit 0
78+
fi
79+
80+
TIME_PHRASE="will begin in approximately $TIME_UNTIL_MEETING"
81+
6682
COMMENT_BODY=$(cat <<EOF
6783
Hello, this is CommunityCallBot.
6884
69-
This is a reminder that the Hiero Python SDK Community Call will begin in approximately 4 hours (14:00 UTC).
85+
This is a reminder that the Hiero Python SDK Community Call $TIME_PHRASE (${MEETING_HOUR}:00 UTC).
7086
7187
The call is an open forum where contributors and users can discuss topics, raise issues, and influence the direction of the Python SDK.
7288
7389
Details:
74-
- Time: 14:00 UTC
90+
- Time: ${MEETING_HOUR}:00 UTC
7591
- Join Link: [Zoom Meeting]($MEETING_LINK)
7692
7793
Disclaimer: This is an automated reminder. Please verify the schedule [here]($CALENDAR_LINK) for any changes.

.github/scripts/cron-calls-office-hours.sh

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ DRY_RUN="${DRY_RUN:-true}"
66
ANCHOR_DATE="2025-12-03"
77
MEETING_LINK="https://zoom-lfx.platform.linuxfoundation.org/meeting/99912667426?password=5b584a0e-1ed7-49d3-b2fc-dc5ddc888338"
88
CALENDAR_LINK="https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week"
9+
MEETING_HOUR="${MEETING_HOUR:-14}" # 14:00 UTC — change this to adjust the meeting time
10+
11+
if ! [[ "$MEETING_HOUR" =~ ^[0-9]+$ ]] || [ "$MEETING_HOUR" -lt 0 ] || [ "$MEETING_HOUR" -gt 23 ]; then
12+
echo "ERROR: MEETING_HOUR must be an integer between 0 and 23, got '$MEETING_HOUR'."
13+
exit 1
14+
fi
915

1016
CANCELLED_DATES=(
1117
"2025-12-31"
@@ -63,15 +69,25 @@ if [ -z "$PR_DATA" ] || [ "$PR_DATA" = "[]" ]; then
6369
exit 0
6470
fi
6571

72+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
73+
TIME_UNTIL_MEETING=$(python3 "$SCRIPT_DIR/utils/compute-time-until-meeting.py" "$TODAY" "$MEETING_HOUR")
74+
75+
if [ "$TIME_UNTIL_MEETING" = "has already started" ]; then
76+
echo "Meeting has already started. Skipping reminder."
77+
exit 0
78+
fi
79+
80+
TIME_PHRASE="will begin in approximately $TIME_UNTIL_MEETING"
81+
6682
COMMENT_BODY=$(cat <<EOF
6783
Hello, this is the OfficeHourBot.
6884
69-
This is a reminder that the Hiero Python SDK Office Hours are scheduled in approximately 4 hours (14:00 UTC).
85+
This is a reminder that the Hiero Python SDK Office Hours $TIME_PHRASE (${MEETING_HOUR}:00 UTC).
7086
7187
This session provides an opportunity to ask questions regarding this Pull Request.
7288
7389
Details:
74-
- Time: 14:00 UTC
90+
- Time: ${MEETING_HOUR}:00 UTC
7591
- Join Link: [Zoom Meeting]($MEETING_LINK)
7692
7793
Disclaimer: This is an automated reminder. Please verify the schedule [here]($CALENDAR_LINK) for any changes.
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
"""Unit tests for compute-time-until-meeting.py helper.
2+
3+
Run with:
4+
python3 -m unittest .github/scripts/tests/test_compute_time_until_meeting.py -v
5+
6+
Or from the tests directory:
7+
python3 -m unittest test_compute_time_until_meeting -v
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import os
13+
import sys
14+
import unittest
15+
from datetime import datetime, timedelta, timezone
16+
from importlib import import_module
17+
18+
19+
# Add the utils directory to sys.path so we can import the helper
20+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "utils"))
21+
22+
# Import the module using importlib since the filename has hyphens
23+
_mod = import_module("compute-time-until-meeting")
24+
compute_time_until_meeting = _mod.compute_time_until_meeting
25+
_format_duration = _mod._format_duration
26+
_pluralize = _mod._pluralize
27+
_parse_mock_time = _mod._parse_mock_time
28+
29+
30+
class TestComputeTimeUntilMeeting(unittest.TestCase):
31+
"""Tests for the compute_time_until_meeting function."""
32+
33+
def test_standard_4_hours_before(self):
34+
"""Standard case: 10:00 run for 14:00 meeting = 4 hours."""
35+
result = compute_time_until_meeting("today", 14, "10:00")
36+
self.assertEqual(result, "4 hours")
37+
38+
def test_delayed_run_3h7m(self):
39+
"""Delayed run: 10:53 run for 14:00 meeting = 3 hours and 7 minutes."""
40+
result = compute_time_until_meeting("today", 14, "10:53")
41+
self.assertEqual(result, "3 hours and 7 minutes")
42+
43+
def test_just_minutes_remaining(self):
44+
"""Only minutes left: 13:45 run for 14:00 meeting = 15 minutes."""
45+
result = compute_time_until_meeting("today", 14, "13:45")
46+
self.assertEqual(result, "15 minutes")
47+
48+
def test_exact_whole_hours(self):
49+
"""Exact hours: 12:00 run for 14:00 meeting = 2 hours."""
50+
result = compute_time_until_meeting("today", 14, "12:00")
51+
self.assertEqual(result, "2 hours")
52+
53+
def test_past_meeting_time(self):
54+
"""Past meeting: 15:00 run for 14:00 meeting = has already started."""
55+
result = compute_time_until_meeting("today", 14, "15:00")
56+
self.assertEqual(result, "has already started")
57+
58+
def test_singular_hour(self):
59+
"""Singular: 13:00 run for 14:00 meeting = 1 hour (not '1 hours')."""
60+
result = compute_time_until_meeting("today", 14, "13:00")
61+
self.assertEqual(result, "1 hour")
62+
63+
def test_singular_minute(self):
64+
"""Singular: 12:59 run for 14:00 meeting = 1 hour and 1 minute."""
65+
result = compute_time_until_meeting("today", 14, "12:59")
66+
self.assertEqual(result, "1 hour and 1 minute")
67+
68+
def test_different_meeting_hour(self):
69+
"""Different hour: 08:30 run for 12:00 meeting = 3 hours and 30 minutes."""
70+
result = compute_time_until_meeting("today", 12, "08:30")
71+
self.assertEqual(result, "3 hours and 30 minutes")
72+
73+
def test_rounding_with_seconds(self):
74+
"""Seconds should ceil: 10:53:30 run for 14:00 = 3 hours and 7 minutes."""
75+
result = compute_time_until_meeting("today", 14, "10:53:30")
76+
self.assertEqual(result, "3 hours and 7 minutes")
77+
78+
def test_seconds_just_past_boundary(self):
79+
"""Just past minute: 13:00:01 run for 14:00 rounds up to 1 hour."""
80+
result = compute_time_until_meeting("today", 14, "13:00:01")
81+
self.assertEqual(result, "1 hour")
82+
83+
def test_utc_day_boundary(self):
84+
"""Cross-day: 23:00 run for 02:00 next day = 3 hours."""
85+
tomorrow = (datetime.now(timezone.utc) + timedelta(days=1)).strftime("%Y-%m-%d")
86+
result = compute_time_until_meeting(tomorrow, 2, "23:00")
87+
self.assertEqual(result, "3 hours")
88+
89+
def test_same_day_morning_meeting_already_passed(self):
90+
"""Same-day early hour: 10:00 run with MEETING_HOUR=2 = has already started.
91+
92+
This is the key edge case that the old -12h rollover heuristic got wrong.
93+
"""
94+
result = compute_time_until_meeting("today", 2, "10:00")
95+
self.assertEqual(result, "has already started")
96+
97+
def test_explicit_date(self):
98+
"""Explicit date: passing a specific YYYY-MM-DD date works correctly."""
99+
tomorrow = (datetime.now(timezone.utc) + timedelta(days=1)).strftime("%Y-%m-%d")
100+
result = compute_time_until_meeting(tomorrow, 14, "10:00")
101+
self.assertIn("hour", result)
102+
103+
def test_meeting_at_exact_time(self):
104+
"""Exact meeting time: 14:00 run for 14:00 meeting = has already started."""
105+
result = compute_time_until_meeting("today", 14, "14:00")
106+
self.assertEqual(result, "has already started")
107+
108+
def test_invalid_date_format(self):
109+
"""Invalid date format raises ValueError."""
110+
with self.assertRaises(ValueError):
111+
compute_time_until_meeting("not-a-date", 14, "10:00")
112+
113+
def test_midnight_meeting_hour_0(self):
114+
"""Edge of range: MEETING_HOUR=0, run at 22:00 yesterday = 2 hours."""
115+
tomorrow = (datetime.now(timezone.utc) + timedelta(days=1)).strftime("%Y-%m-%d")
116+
result = compute_time_until_meeting(tomorrow, 0, "22:00")
117+
self.assertEqual(result, "2 hours")
118+
119+
def test_late_meeting_hour_23(self):
120+
"""Edge of range: MEETING_HOUR=23, run at 20:00 = 3 hours."""
121+
result = compute_time_until_meeting("today", 23, "20:00")
122+
self.assertEqual(result, "3 hours")
123+
124+
def test_one_minute_remaining(self):
125+
"""Edge: 13:59 run for 14:00 meeting = 1 minute."""
126+
result = compute_time_until_meeting("today", 14, "13:59")
127+
self.assertEqual(result, "1 minute")
128+
129+
def test_cli_invocation(self):
130+
"""CLI: calling via subprocess produces expected output."""
131+
import subprocess
132+
133+
helper_path = os.path.join(os.path.dirname(__file__), "..", "utils", "compute-time-until-meeting.py")
134+
result = subprocess.run(
135+
["python3", helper_path, "today", "14", "10:00"], capture_output=True, text=True, check=True
136+
)
137+
self.assertEqual(result.returncode, 0)
138+
self.assertEqual(result.stdout.strip(), "4 hours")
139+
140+
def test_cli_missing_args(self):
141+
"""CLI: missing arguments should exit with error."""
142+
import subprocess
143+
144+
helper_path = os.path.join(os.path.dirname(__file__), "..", "utils", "compute-time-until-meeting.py")
145+
result = subprocess.run(["python3", helper_path], capture_output=True, text=True, check=False)
146+
self.assertEqual(result.returncode, 1)
147+
148+
149+
class TestFormatDuration(unittest.TestCase):
150+
"""Tests for the _format_duration helper."""
151+
152+
def test_hours_and_minutes(self):
153+
self.assertEqual(_format_duration(3, 7), "3 hours and 7 minutes")
154+
155+
def test_only_hours(self):
156+
self.assertEqual(_format_duration(4, 0), "4 hours")
157+
158+
def test_only_minutes(self):
159+
self.assertEqual(_format_duration(0, 15), "15 minutes")
160+
161+
def test_singular_hour(self):
162+
self.assertEqual(_format_duration(1, 0), "1 hour")
163+
164+
def test_singular_minute(self):
165+
self.assertEqual(_format_duration(0, 1), "1 minute")
166+
167+
def test_singular_both(self):
168+
self.assertEqual(_format_duration(1, 1), "1 hour and 1 minute")
169+
170+
171+
class TestPluralize(unittest.TestCase):
172+
"""Tests for the _pluralize helper."""
173+
174+
def test_singular(self):
175+
self.assertEqual(_pluralize(1, "hour"), "hour")
176+
177+
def test_plural(self):
178+
self.assertEqual(_pluralize(2, "hour"), "hours")
179+
180+
def test_zero_is_plural(self):
181+
self.assertEqual(_pluralize(0, "minute"), "minutes")
182+
183+
184+
class TestParseMockTime(unittest.TestCase):
185+
"""Tests for the _parse_mock_time helper."""
186+
187+
def test_valid_hh_mm(self):
188+
self.assertEqual(_parse_mock_time("10:30"), (10, 30, 0))
189+
190+
def test_valid_hh_mm_ss(self):
191+
self.assertEqual(_parse_mock_time("10:30:45"), (10, 30, 45))
192+
193+
def test_invalid_format(self):
194+
with self.assertRaises(ValueError):
195+
_parse_mock_time("10")
196+
197+
def test_non_numeric(self):
198+
with self.assertRaises(ValueError):
199+
_parse_mock_time("ab:cd")
200+
201+
def test_out_of_range_hour(self):
202+
with self.assertRaises(ValueError):
203+
_parse_mock_time("25:00")
204+
205+
def test_out_of_range_minute(self):
206+
with self.assertRaises(ValueError):
207+
_parse_mock_time("10:60")
208+
209+
210+
if __name__ == "__main__":
211+
unittest.main()

0 commit comments

Comments
 (0)