* fix(cutover): write Slack secrets into empty Terraform shells DescribeSecret succeeds on HCP-created shells with no version, so skip-if-exists left roster and Slack tokens unset. * feat(infra): migrate afterhours to HCP Terraform (PLAT-74) Replace the mgmt SAM stack with a prod-only HCP workspace, in-repo hcptf IAM, stub Lambdas, and zip CD on push to main. * fix(cutover): retry DDB unprocessed items and skip past at() holidays Unprocessed BatchWriteItem rows and leftover past at() schedules would drop roster data or abort holiday recreation during prod cutover.
8.7 KiB
After-Hours Shift Manager — Setup Guide
1. Create a Slack App
- Go to api.slack.com/apps → Create New App → From scratch
- Name:
After-Hours Shift Manager, pick your workspace - Under OAuth & Permissions, add these Bot Token Scopes:
chat:write— post messages to channelscommands— register slash commandschat:write.public— post to channels the bot isn't in
- Under Slash Commands, create a new command:
- Command:
/oncall - Request URL:
(fill in after deploy — see step 4) - Short Description:
Manage after-hours on-call shifts - Usage Hint:
[schedule | pick <date> | drop <date> | swap <date> @person | register <ext> | roster | help]
- Command:
- Under Interactivity & Shortcuts, toggle ON:
- Request URL: same URL as the slash command (the
/slack/eventsendpoint)
- Request URL: same URL as the slash command (the
- Install to Workspace — approve the permissions
- Copy the Bot User OAuth Token (
xoxb-...) and Signing Secret (under Basic Information)
2. Store Secrets in AWS Secrets Manager
The stack reads these from Secrets Manager (the IAM roles grant
secretsmanager:GetSecretValue on afterhours-shift-manager/*):
# Slack
aws secretsmanager create-secret \
--name afterhours-shift-manager/slack-bot-token \
--secret-string "xoxb-YOUR-BOT-TOKEN"
aws secretsmanager create-secret \
--name afterhours-shift-manager/slack-signing-secret \
--secret-string "YOUR-SIGNING-SECRET"
# 3CX Queue XAPI (used by roster-sync and ring-scheduler)
aws secretsmanager create-secret \
--name afterhours-shift-manager/3cx-domain \
--secret-string "yourcompany.3cx.us"
aws secretsmanager create-secret \
--name afterhours-shift-manager/3cx-client-id \
--secret-string "YOUR-3CX-CLIENT-ID"
aws secretsmanager create-secret \
--name afterhours-shift-manager/3cx-client-secret \
--secret-string "YOUR-3CX-CLIENT-SECRET"
# Roster HTTP API bearer token (plain string). Duplicate the same value into
# seahaven-prod as paychex-integrations/afterhours-roster-token. Generate the
# token into a temp file, pass --secret-string file://..., then delete the file.
# Never paste the value into chat, Terraform, or a PR.
aws secretsmanager create-secret \
--name afterhours-shift-manager/roster-api-token \
--secret-string file://./roster-api-token.tmp
Create afterhours-shift-manager/roster-api-token before the first deploy that
includes afterhours-roster-api, or live PUT/DELETE calls return 503.
Rotation is coordinated: write the new value to both
afterhours-shift-manager/roster-api-token and
paychex-integrations/afterhours-roster-token, then recycle
afterhours-roster-api so cached execution environments pick it up. Updating
only one copy causes 401s. The identity processor AFTERHOURS_BASE_URL is the
Terraform output api_origin (origin only, no /roster suffix). Flip that HCP
variable on paychex-integrations-prod at cutover after DynamoDB is copied.
Daily roster-sync still removes DynamoDB rows that are not in the 3CX DEFAULT
group. Hire stays safe because 3CX create lands the extension in that group
before the identity processor PUTs /roster.
The Slack channel ID is not a secret. It is Terraform variable
shift_channel(defaultC0APATP612N), not stored in Secrets Manager.
3. HCP Terraform and GitHub Environment
Prod only. Workspace afterhours-shift-manager-prod in project seahaven-prod
(account 011934824531). No seahaven-dev workspace.
First apply uses the hcptf-bootstrap window (exact StringEquals trust, never
StringLike):
- Create the HCP workspace. Auto-apply off. No project-level variable set.
Working directory
terraform. File trigger prefixterraform/**only. Speculative plans on. VCS onmain. - From
seahaven-org-baseline:scripts/create-hcptf-bootstrap-roles.sh --account prod --allow-workspace afterhours-shift-manager-prod - Point workspace
TFC_AWS_APPLY_ROLE_ARN/TFC_AWS_PLAN_ROLE_ARNathcptf-bootstrap/hcptf-bootstrap-plan. SetTFC_AWS_PROVIDER_AUTH=true. - One manual apply with
schedules_enabled=false. This creates the scopedhcptf-*roles, the Lambda boundary, and the rest of the stack. If 3CX secrets already exist from seahaven-door-unlock-api, import those three names instead of creating them:terraform import 'aws_secretsmanager_secret.this["afterhours-shift-manager/3cx-domain"]' afterhours-shift-manager/3cx-domain(and the client-id / client-secret names). Do not overwrite 3CX values. - Retarget
TFC_AWS_*tohcptf-afterhours-shift-manager/hcptf-afterhours-shift-manager-plan. Re-run the create script with no--allow-workspace. - Second manual apply as the scoped role. Then seal auto-apply on.
GitHub Environment prod: reviewers, branch policy main only, Environment
variable DEPLOY_ROLE_ARN = Terraform output github_deploy_role_arn.
Function zips: Actions → Deploy on push to main, or workflow_dispatch.
Keep schedules_enabled=false until Slack and Paychex point at this stack.
HCP outputs to copy: slack_request_url, api_origin,
holiday_scheduler_role_arn, github_deploy_role_arn.
4. Set the Slack Request URL
Reuse the existing Slack app. After the zip deploy, copy slack_request_url
from HCP outputs:
- Slash Commands → edit
/oncall→ set Request URL to that URL - Interactivity & Shortcuts → set Request URL to the same URL
Do this in the cutover window, not before DynamoDB is copied.
5. Seed the Schedule
python scripts/seed_schedule.py
This populates the DynamoDB table with the employee roster and default weekly schedule.
6. Invite the Bot & Register Users
- Create a channel (e.g.
#after-hours-shifts) and invite the bot:/invite @After-Hours Shift Manager - Each employee links their Slack account by running:
(using their own extension number)/oncall register 114
7. Update the 3CX Scheduler (optional)
To have the 3CX scheduler read overrides from DynamoDB (so Slack-driven changes apply to future dates automatically), deploy the updated 3CX scheduler from the feature/dynamodb-shift-integration branch. See that branch's changes for details.
Without this step, the Slack bot still works — it invokes the 3CX scheduler Lambda directly for same-day changes. Future-date overrides would only take effect if the scheduler reads DynamoDB.
8. Prod cutover (PLAT-74)
Avoid Monday 06:00-08:00 ET and any holiday 08:00/17:00 ET window. Dry-run the
scripts first (--execute is required for writes).
- Merge this repo's PR (SAM CD is gone). First HCP apply is the bootstrap
window above with
schedules_enabled=false. - Copy DynamoDB
afterhours-shiftsmgmt → prod. Verify item counts:python scripts/cutover/copy_dynamodb.py --src-profile mgmt --dst-profile prodthen--execute. - Confirm secrets in prod.
copy_secrets.pywrites Slack bot token, Slack signing secret, and roster-api-token into empty Terraform shells and skips dest names that already have a value. It never writes 3CX secrets:python scripts/cutover/copy_secrets.py --src-profile mgmt --dst-profile prodthen--execute. Strip trailing newlines is built in. - GHA
workflow_dispatch(or the merge deploy; re-run if it raced apply) to overwrite stubs. - Recreate outstanding future
holiday-activate-*/holiday-deactivate-*in prod against the new router ARN and scheduler role:python scripts/cutover/recreate_holiday_schedules.py --src-profile mgmt --dst-profile prod - Instant cut: Slack Request URL → prod
/slack/events; Paychex HCP variableafterhours_base_urlonpaychex-integrations-prod→ prodapi_origin;schedules_enabled=truevia a terraform-only merge; disable mgmt EventBridge. Smoke: Slack/oncall, roster PUT/DELETE, weekly-post SendMessage (or simulate-principal-policy plus one smoke message), ring-scheduler invoke, holiday GetSchedule. - Seal auto-apply on. Delete the mgmt SAM stack. Remove the mgmt SQS principal
from
paychex-checkcomponents. Update Confluence AWS Architecture Map and check PLAT-71 item 4.
Do not dual-run 3CX writers. Do not flip afterhours_base_url before DynamoDB
is copied.
Commands Reference
| Command | Description |
|---|---|
/oncall |
Show this week's schedule |
/oncall next |
Show next week's schedule |
/oncall pick <date> |
Pick up a shift |
/oncall drop <date> |
Drop your shift (marks it open) |
/oncall swap <date> @person |
Hand your shift to someone else |
/oncall register <ext> |
Link your Slack to your extension |
/oncall roster |
Show all employees and their link status |
/oncall help |
Show help |
Dates can be: today, tomorrow, monday–sunday, 4/5, 2026-04-05