mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 07:53:11 +00:00
* Add changelog-driven releases and App Home tab Version the bot continuously from CHANGELOG.md (the single source of truth for both the version and the staff-readable notes) and surface changes to users in two ways: - A new afterhours-release-notifier Lambda posts a "What's New" message to the shift channel on minor/major releases (patches stay silent). - The bot gains an App Home "About" tab showing what it does, the command list, and the current version's notes. release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN events don't start downstream workflows), checks out the deployed commit, and tags + publishes a GitHub Release + invokes the notifier. It assumes a dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the notifier; the account's cfn role gates role creation on that boundary. The manual Version Bump workflow is retired. A CI guard enforces that a CHANGELOG edit is a clean SemVer bump and that the in-package copy matches. * Harden release workflow and regex against CodeQL findings Address three code-scanning alerts on the PR: - Critical (actions/untrusted-checkout): split release.yaml into a read-only `prepare` job that checks out and runs repo code, and a privileged `publish` job (contents:write + OIDC) that never checks out repo code — it tags, releases, and invokes purely through the GitHub and AWS APIs. Also assert head_branch == main. - High x2 (py/polynomial-redos): rewrite the italic and link regexes in markdown_to_mrkdwn with possessive quantifiers and exclusive character classes so they run in linear time on adversarial input. Adds a regression test. * Move release/announce into Deploy workflow to clear CodeQL The workflow_run-triggered release.yaml kept tripping CodeQL's privileged-context rules (untrusted-checkout, then cache-poisoning) — CodeQL distrusts any workflow_run that checks out a ref, regardless of the main-only guarantee, and there is no autofix. Fold the release job into deploy.yaml gated on `needs: deploy`. A push-to-main run is a trusted context, so checking out and running repo code with write/OIDC is safe there. This still gates on deploy success and serializes via the deploy concurrency group, and removes the separate workflow entirely.
364 lines
13 KiB
YAML
364 lines
13 KiB
YAML
AWSTemplateFormatVersion: "2010-09-09"
|
|
Transform: AWS::Serverless-2016-10-31
|
|
Description: After-Hours Shift Manager — Slack bot for managing on-call shifts with 3CX integration
|
|
|
|
Parameters:
|
|
Timezone:
|
|
Type: String
|
|
Default: "America/New_York"
|
|
ShiftChannel:
|
|
Type: String
|
|
Description: Slack channel ID for schedule posts and shift notifications
|
|
QueueNumber:
|
|
Type: String
|
|
Default: "801"
|
|
Description: 3CX queue extension number to update
|
|
|
|
Globals:
|
|
Function:
|
|
Runtime: python3.12
|
|
Timeout: 30
|
|
MemorySize: 1024
|
|
Architectures:
|
|
- arm64
|
|
PermissionsBoundary: arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary
|
|
# Access logging + default throttling on the implicit HTTP API (audit M-18).
|
|
HttpApi:
|
|
AccessLogSettings:
|
|
DestinationArn: !GetAtt ApiAccessLogGroup.Arn
|
|
Format: '{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","requestTime":"$context.requestTime","method":"$context.httpMethod","routeKey":"$context.routeKey","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength","integrationError":"$context.integrationErrorMessage"}'
|
|
DefaultRouteSettings:
|
|
ThrottlingBurstLimit: 50
|
|
ThrottlingRateLimit: 100
|
|
|
|
Resources:
|
|
ApiAccessLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: /aws/apigateway/afterhours-shift-manager
|
|
RetentionInDays: 90
|
|
|
|
# --- Shared Lambda Layer ---
|
|
SharedLayer:
|
|
Type: AWS::Serverless::LayerVersion
|
|
Properties:
|
|
LayerName: afterhours-shared
|
|
ContentUri: src/shared/
|
|
CompatibleRuntimes:
|
|
- python3.12
|
|
CompatibleArchitectures:
|
|
- arm64
|
|
Metadata:
|
|
BuildMethod: python3.12
|
|
BuildArchitecture: arm64
|
|
|
|
# --- DynamoDB ---
|
|
ShiftTable:
|
|
Type: AWS::DynamoDB::Table
|
|
Properties:
|
|
TableName: afterhours-shifts
|
|
BillingMode: PAY_PER_REQUEST
|
|
AttributeDefinitions:
|
|
- AttributeName: PK
|
|
AttributeType: S
|
|
- AttributeName: SK
|
|
AttributeType: S
|
|
KeySchema:
|
|
- AttributeName: PK
|
|
KeyType: HASH
|
|
- AttributeName: SK
|
|
KeyType: RANGE
|
|
TimeToLiveSpecification:
|
|
AttributeName: expires_at
|
|
Enabled: true
|
|
|
|
# --- Slack Bot Lambda ---
|
|
SlackBotFunction:
|
|
Type: AWS::Serverless::Function
|
|
Properties:
|
|
FunctionName: afterhours-shift-manager
|
|
Handler: handler.handler
|
|
CodeUri: src/slack-bot/
|
|
Layers:
|
|
- !Ref SharedLayer
|
|
Environment:
|
|
Variables:
|
|
SHIFT_TABLE: !Ref ShiftTable
|
|
SLACK_BOT_TOKEN_SECRET: afterhours-shift-manager/slack-bot-token
|
|
SLACK_SIGNING_SECRET: afterhours-shift-manager/slack-signing-secret
|
|
SHIFT_CHANNEL: !Ref ShiftChannel
|
|
TCX_SECRET_PREFIX: afterhours-shift-manager/3cx-
|
|
QUEUE_NUMBER: !Ref QueueNumber
|
|
TZ: !Ref Timezone
|
|
Policies:
|
|
- DynamoDBCrudPolicy:
|
|
TableName: !Ref ShiftTable
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- secretsmanager:GetSecretValue
|
|
Resource:
|
|
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*"
|
|
Events:
|
|
SlackEvents:
|
|
Type: HttpApi
|
|
Properties:
|
|
Path: /slack/events
|
|
Method: POST
|
|
|
|
# --- Weekly Schedule Post (Monday 7am ET) ---
|
|
WeeklyPostFunction:
|
|
Type: AWS::Serverless::Function
|
|
Properties:
|
|
FunctionName: afterhours-weekly-post
|
|
Handler: app.handler
|
|
CodeUri: src/weekly-post/
|
|
Layers:
|
|
- !Ref SharedLayer
|
|
Environment:
|
|
Variables:
|
|
SHIFT_TABLE: !Ref ShiftTable
|
|
SLACK_BOT_TOKEN_SECRET: afterhours-shift-manager/slack-bot-token
|
|
SHIFT_CHANNEL: !Ref ShiftChannel
|
|
SES_SENDER: noreply@seahaven.com
|
|
PAYROLL_RECIPIENTS: payroll@seahaven.com
|
|
PAY_REPORT_USER: U0A3SC48T47
|
|
TZ: !Ref Timezone
|
|
Policies:
|
|
- DynamoDBCrudPolicy:
|
|
TableName: !Ref ShiftTable
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- secretsmanager:GetSecretValue
|
|
Resource:
|
|
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*"
|
|
- Effect: Allow
|
|
Action:
|
|
- ses:SendEmail
|
|
Resource:
|
|
- !Sub "arn:aws:ses:${AWS::Region}:${AWS::AccountId}:identity/*"
|
|
Events:
|
|
# EST: 7am ET = 12:00 UTC (Nov-Mar)
|
|
WeeklyPostEST:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 12 ? * MON *)
|
|
Description: "Post weekly schedule Monday 7am EST"
|
|
Enabled: true
|
|
# EDT: 7am ET = 11:00 UTC (Mar-Nov)
|
|
WeeklyPostEDT:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 11 ? * MON *)
|
|
Description: "Post weekly schedule Monday 7am EDT"
|
|
Enabled: true
|
|
|
|
# --- Roster Sync Lambda (daily sync from 3CX) ---
|
|
RosterSyncFunction:
|
|
Type: AWS::Serverless::Function
|
|
Properties:
|
|
FunctionName: afterhours-roster-sync
|
|
Handler: app.handler
|
|
CodeUri: src/roster-sync/
|
|
Layers:
|
|
- !Ref SharedLayer
|
|
Timeout: 60
|
|
Environment:
|
|
Variables:
|
|
SHIFT_TABLE: !Ref ShiftTable
|
|
TCX_SECRET_PREFIX: afterhours-shift-manager/3cx-
|
|
SYNC_GROUP: DEFAULT
|
|
TZ: !Ref Timezone
|
|
Policies:
|
|
- DynamoDBCrudPolicy:
|
|
TableName: !Ref ShiftTable
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- secretsmanager:GetSecretValue
|
|
Resource:
|
|
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*"
|
|
Events:
|
|
# Daily at 6am ET (before the 7am schedule post and 8am 3CX scheduler)
|
|
# EST: 6am ET = 11:00 UTC (Nov-Mar)
|
|
RosterSyncEST:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 11 ? * * *)
|
|
Description: "Sync roster from 3CX at 6am EST"
|
|
Enabled: true
|
|
# EDT: 6am ET = 10:00 UTC (Mar-Nov)
|
|
RosterSyncEDT:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 10 ? * * *)
|
|
Description: "Sync roster from 3CX at 6am EDT"
|
|
Enabled: true
|
|
|
|
# --- Ring Scheduler (daily 3CX queue routing updates) ---
|
|
RingSchedulerFunction:
|
|
Type: AWS::Serverless::Function
|
|
Properties:
|
|
FunctionName: afterhours-ring-scheduler
|
|
Handler: app.handler
|
|
CodeUri: src/ring-scheduler/
|
|
Layers:
|
|
- !Ref SharedLayer
|
|
Timeout: 60
|
|
Environment:
|
|
Variables:
|
|
SHIFT_TABLE: !Ref ShiftTable
|
|
TCX_SECRET_PREFIX: afterhours-shift-manager/3cx-
|
|
QUEUE_NUMBER: !Ref QueueNumber
|
|
TZ: !Ref Timezone
|
|
Policies:
|
|
- DynamoDBCrudPolicy:
|
|
TableName: !Ref ShiftTable
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- secretsmanager:GetSecretValue
|
|
Resource:
|
|
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*"
|
|
Events:
|
|
# Daily at 8am ET — update after-hours routing
|
|
DailyScheduleEST:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 13 ? * * *)
|
|
Description: "Update 3CX queue at 8am EST"
|
|
Enabled: true
|
|
DailyScheduleEDT:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 12 ? * * *)
|
|
Description: "Update 3CX queue at 8am EDT"
|
|
Enabled: true
|
|
# Weekends at 5pm ET — switch to night shift person
|
|
WeekendEveningEST:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 22 ? * SAT,SUN *)
|
|
Description: "Update 3CX queue at 5pm EST weekends"
|
|
Enabled: true
|
|
WeekendEveningEDT:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 21 ? * SAT,SUN *)
|
|
Description: "Update 3CX queue at 5pm EDT weekends"
|
|
Enabled: true
|
|
|
|
# --- Release Notifier (invoked by release.yaml on minor/major releases) ---
|
|
ReleaseNotifierFunction:
|
|
Type: AWS::Serverless::Function
|
|
Properties:
|
|
FunctionName: afterhours-release-notifier
|
|
Handler: app.handler
|
|
CodeUri: src/release-notifier/
|
|
Layers:
|
|
- !Ref SharedLayer
|
|
Environment:
|
|
Variables:
|
|
SLACK_BOT_TOKEN_SECRET: afterhours-shift-manager/slack-bot-token
|
|
SHIFT_CHANNEL: !Ref ShiftChannel
|
|
TZ: !Ref Timezone
|
|
Policies:
|
|
# Least privilege: only the Slack bot token, not the whole namespace.
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- secretsmanager:GetSecretValue
|
|
Resource:
|
|
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/slack-bot-token-*"
|
|
|
|
# GitHub-OIDC role assumed by release.yaml to invoke the notifier. Auto-named
|
|
# (no RoleName) so the deploy's CAPABILITY_IAM is sufficient — cd-sam does not
|
|
# pass CAPABILITY_NAMED_IAM. Trust + permission are scoped to the minimum: this
|
|
# repo's main ref + release workflow, and InvokeFunction on the notifier alone.
|
|
# Its ARN is surfaced as a stack output and set once as the
|
|
# RELEASE_NOTIFY_INVOKE_ROLE_ARN repo variable (see README).
|
|
#
|
|
# The permissions boundary is REQUIRED, not optional: the scoped
|
|
# github-cfn-execution-role's IAM policy gates iam:CreateRole/PutRolePolicy on
|
|
# the role carrying exactly this boundary, so the CI deploy is denied without
|
|
# it. The boundary itself permits lambda:InvokeFunction (Sid LambdaInvoke), so
|
|
# it does not restrict this role's one job.
|
|
ReleaseNotifyInvokeRole:
|
|
Type: AWS::IAM::Role
|
|
Properties:
|
|
PermissionsBoundary: arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary
|
|
AssumeRolePolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Principal:
|
|
Federated: !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com"
|
|
Action: sts:AssumeRoleWithWebIdentity
|
|
Condition:
|
|
StringEquals:
|
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
StringLike:
|
|
token.actions.githubusercontent.com:sub: "repo:Sea-Haven-Industries/afterhours-shift-manager:ref:refs/heads/main"
|
|
# Defense-in-depth: only the release workflow may assume this role,
|
|
# not any workflow running on main.
|
|
token.actions.githubusercontent.com:job_workflow_ref: "Sea-Haven-Industries/afterhours-shift-manager/.github/workflows/release.yaml@refs/heads/main"
|
|
Policies:
|
|
- PolicyName: invoke-release-notifier
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Action: lambda:InvokeFunction
|
|
Resource: !GetAtt ReleaseNotifierFunction.Arn
|
|
|
|
# --- CloudWatch Log Groups (explicit 60-day retention) ---
|
|
SlackBotLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub "/aws/lambda/${SlackBotFunction}"
|
|
RetentionInDays: 60
|
|
|
|
WeeklyPostLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub "/aws/lambda/${WeeklyPostFunction}"
|
|
RetentionInDays: 60
|
|
|
|
RosterSyncLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub "/aws/lambda/${RosterSyncFunction}"
|
|
RetentionInDays: 60
|
|
|
|
RingSchedulerLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub "/aws/lambda/${RingSchedulerFunction}"
|
|
RetentionInDays: 60
|
|
|
|
ReleaseNotifierLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub "/aws/lambda/${ReleaseNotifierFunction}"
|
|
RetentionInDays: 60
|
|
|
|
Outputs:
|
|
SlackBotApiUrl:
|
|
Description: URL for Slack app Request URL configuration
|
|
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/slack/events"
|
|
ShiftTableName:
|
|
Value: !Ref ShiftTable
|
|
SlackBotFunctionArn:
|
|
Value: !GetAtt SlackBotFunction.Arn
|
|
WeeklyPostFunctionArn:
|
|
Value: !GetAtt WeeklyPostFunction.Arn
|
|
RosterSyncFunctionArn:
|
|
Value: !GetAtt RosterSyncFunction.Arn
|
|
RingSchedulerFunctionArn:
|
|
Value: !GetAtt RingSchedulerFunction.Arn
|
|
ReleaseNotifierFunctionArn:
|
|
Value: !GetAtt ReleaseNotifierFunction.Arn
|
|
ReleaseNotifyInvokeRoleArn:
|
|
Description: Set this as the RELEASE_NOTIFY_INVOKE_ROLE_ARN repo variable for release.yaml
|
|
Value: !GetAtt ReleaseNotifyInvokeRole.Arn
|