mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 12:33:13 +00:00
Some checks are pending
Deploy / Deploy to prod (push) Waiting to run
* 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.
190 lines
8.7 KiB
Markdown
190 lines
8.7 KiB
Markdown
# After-Hours Shift Manager — Setup Guide
|
||
|
||
## 1. Create a Slack App
|
||
|
||
1. Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App** → **From scratch**
|
||
2. Name: `After-Hours Shift Manager`, pick your workspace
|
||
3. Under **OAuth & Permissions**, add these **Bot Token Scopes**:
|
||
- `chat:write` — post messages to channels
|
||
- `commands` — register slash commands
|
||
- `chat:write.public` — post to channels the bot isn't in
|
||
4. 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]`
|
||
5. Under **Interactivity & Shortcuts**, toggle **ON**:
|
||
- Request URL: same URL as the slash command (the `/slack/events` endpoint)
|
||
6. **Install to Workspace** — approve the permissions
|
||
7. 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/*`):
|
||
|
||
```bash
|
||
# 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` (default `C0APATP612N`), 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`):
|
||
|
||
1. Create the HCP workspace. Auto-apply off. No project-level variable set.
|
||
Working directory `terraform`. File trigger prefix `terraform/**` only.
|
||
Speculative plans on. VCS on `main`.
|
||
2. From `seahaven-org-baseline`:
|
||
`scripts/create-hcptf-bootstrap-roles.sh --account prod --allow-workspace afterhours-shift-manager-prod`
|
||
3. Point workspace `TFC_AWS_APPLY_ROLE_ARN` / `TFC_AWS_PLAN_ROLE_ARN` at
|
||
`hcptf-bootstrap` / `hcptf-bootstrap-plan`. Set `TFC_AWS_PROVIDER_AUTH=true`.
|
||
4. One manual apply with `schedules_enabled=false`. This creates the scoped
|
||
`hcptf-*` 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.
|
||
5. Retarget `TFC_AWS_*` to `hcptf-afterhours-shift-manager` /
|
||
`hcptf-afterhours-shift-manager-plan`. Re-run the create script with no
|
||
`--allow-workspace`.
|
||
6. 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
|
||
|
||
```bash
|
||
python scripts/seed_schedule.py
|
||
```
|
||
|
||
This populates the DynamoDB table with the employee roster and default weekly schedule.
|
||
|
||
## 6. Invite the Bot & Register Users
|
||
|
||
1. Create a channel (e.g. `#after-hours-shifts`) and invite the bot: `/invite @After-Hours Shift Manager`
|
||
2. Each employee links their Slack account by running:
|
||
```
|
||
/oncall register 114
|
||
```
|
||
(using their own extension number)
|
||
|
||
## 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).
|
||
|
||
1. Merge this repo's PR (SAM CD is gone). First HCP apply is the bootstrap
|
||
window above with `schedules_enabled=false`.
|
||
2. Copy DynamoDB `afterhours-shifts` mgmt → prod. Verify item counts:
|
||
`python scripts/cutover/copy_dynamodb.py --src-profile mgmt --dst-profile prod`
|
||
then `--execute`.
|
||
3. Confirm secrets in prod. `copy_secrets.py` writes 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 prod`
|
||
then `--execute`. Strip trailing newlines is built in.
|
||
4. GHA `workflow_dispatch` (or the merge deploy; re-run if it raced apply) to
|
||
overwrite stubs.
|
||
5. 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`
|
||
6. Instant cut: Slack Request URL → prod `/slack/events`; Paychex HCP variable
|
||
`afterhours_base_url` on `paychex-integrations-prod` → prod `api_origin`;
|
||
`schedules_enabled=true` via 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.
|
||
7. 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`
|