- Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query)
- Button pickup: update 3CX for active shift type, not just night
- Pick/drop/swap commands: only update 3CX when shift type is active
- Swap command: add missing past-date guard
- add_roster_entry: reject if extension already exists
- Apply ruff formatting
The 3CX routing was changed from ring group 800 to queue 801 in a
previous PR on ring-scheduler-3cx. Updates all callers and the SAM
template parameter default accordingly.
- Reject /oncall pick and /oncall drop for past dates
- Show ephemeral error when stale pickup buttons are clicked
- Hide pickup buttons for dates in the past
- Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to
schedule lines, pickup buttons, and shift change notifications
- Store schedule message timestamp in DynamoDB (SCHEDULE_POST record)
- Delete previous week's schedule post before posting the new one
- Live-update the schedule post via chat_update after any
pick/drop/swap/button-pickup so it always reflects current state
- Add afterhours-ring-scheduler Lambda with 4 EventBridge rules
(daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group
routing updates
- Extract shared ring_scheduler.py module for direct ring group
updates from both the scheduled Lambda and the Slack bot
- Replace cross-Lambda invoke with direct update_ring_group() call
in the Slack bot — eliminates lambda:InvokeFunction dependency
- Use RingGroup API (correct) instead of Queue API (was wrong in
the original ring-scheduler repo)
- Eliminate YAML config fallback — DynamoDB is the sole schedule
source
- Add RingGroupNumber CloudFormation parameter
- Slack bot token and signing secret now read from Secrets Manager
- 3CX credentials (domain, client-id, client-secret) moved to
Secrets Manager under afterhours-shift-manager/3cx-* prefix
- Channel ID is now a non-secret CloudFormation parameter (ShiftChannel)
- Add shared secrets.py helper for Secrets Manager reads
- Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue
Move from flat src/ to per-function directories:
- src/slack-bot/ — Slack Bolt Lambda handler
- src/weekly-post/ — Monday schedule + pay post
- src/roster-sync/ — Daily 3CX roster sync
- src/shared/ — Lambda Layer with schedule, blocks, three_cx_client
Each function has its own requirements.txt and CodeUri. Shared
modules are deployed as a SAM Layer (afterhours-shared) importable
as `from shared.X import Y`.
- Set arm64 architecture globally for all Lambda functions
- Add explicit CloudWatch log groups with 60-day retention
- Add missing WeeklyPostFunctionArn to stack outputs
- Add Dependabot assignees for both ecosystems
- Add samconfig.toml.example for onboarding
The date comparison used git's author date in its stored timezone
(e.g. -04:00) against GitHub API mergedAt values in UTC (Z suffix).
Lexicographic string comparison across different timezone formats
caused every PR from the tagged commit's day to be re-included in
subsequent versions. Normalize to UTC with format-local so both
sides match.
Also replace the hardcoded canvas URL with the CANVAS_ID env var
already available in the step.
Two root causes:
- claim_open_shift ConditionExpression failed when no OVERRIDE record
existed (shift available via weekly fallback). Added attribute_not_exists
check so claims succeed for both missing and OPEN overrides.
- Slack ack timeout: chat_postMessage took too long before respond() was
called, causing Slack to show an error. Moved respond() first and made
channel notifications best-effort with try/except.
Reworks the daily version bump workflow to:
- Pull merged PR titles via gh CLI instead of raw commit messages
- Post a polished, non-technical channel message with a link to the
changelog canvas
- Prepend the new version entry to the Slack canvas via canvases.edit
API so the canvas stays in sync automatically
Channel message format:
"After-Hours Scheduler has been updated to vXXXX.XX.XX
Here's a brief summary of what changed:
• ...
Click here to read the full changelog"
Replaces per-push notifications with a daily 6pm ET cron job that
batches all commits since the last version tag into a single Slack
message. Tags main with a date-based version (v2026.05.01) so there
is at most one version bump per day. Includes DST guard matching the
pattern used by the other scheduled Lambdas.
Slash command handlers (drop, pick, swap) were posting notifications to
command["channel_id"] — wherever the command was run. If someone ran
/oncall drop from a DM, the notification went there instead of the
schedule channel. Pickup buttons didn't have this problem because
body["channel"]["id"] is always the channel where the button lives.
Added SHIFT_CHANNEL_PARAM to the SlackBotFunction env vars, read it on
cold start, and route all slash command shift-change notifications to
the configured schedule channel.
Closes#21
The pickup button handler used an unconditional put_item, so concurrent
clicks would both succeed with last-write-wins. Added claim_open_shift()
which uses a DynamoDB ConditionExpression to only write if the shift is
still OPEN. The button handler now returns an ephemeral "already taken"
message when the condition fails.
Closes#19
The handler passed `now` as start_date, so mid-week runs showed
14 days from today instead of Mon–Sun x2. Also, day names were
derived from a static index (DAY_ORDER[i%7]) which assumed
index 0 = Monday — when start_date wasn't Monday, Saturday and
Sunday were mislabeled as Monday and Tuesday.
Fix: pass this week's Monday from the handler, and derive day
names from the actual date via strftime instead of a static list.
Fixes#17