mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-10-07 12:49:00 +00:00
Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9e0573114 | ||
|
|
4c4050d9e1 | ||
|
|
14f9ceb184 | ||
|
|
5704e27f33 | ||
|
|
eab3e97285 | ||
|
|
54ad5a7e34 | ||
|
|
3030b134fa | ||
|
|
9ca1ef48bd | ||
|
|
e9893a6f7b |
30 changed files with 179 additions and 69 deletions
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
|
|
@ -13,7 +13,7 @@ permissions:
|
|||
jobs:
|
||||
autofix:
|
||||
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-autofix.yaml@2e2b3a282fbd148352a2b7433b58668a47e6bfeb # v1.0.16
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-autofix.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
|
||||
permissions:
|
||||
contents: write
|
||||
secrets: inherit
|
||||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
lint:
|
||||
needs: autofix
|
||||
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-app.yaml@2e2b3a282fbd148352a2b7433b58668a47e6bfeb # v1.0.16
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-app.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ jobs:
|
|||
terraform:
|
||||
needs: autofix
|
||||
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@2e2b3a282fbd148352a2b7433b58668a47e6bfeb # v1.0.16
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
|
||||
with:
|
||||
terraform-version: "1.16.0"
|
||||
|
||||
|
|
|
|||
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
|
|
@ -7,4 +7,4 @@ permissions:
|
|||
|
||||
jobs:
|
||||
review:
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@2e2b3a282fbd148352a2b7433b58668a47e6bfeb # v1.0.16
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
|
||||
|
|
|
|||
4
.github/workflows/deploy-api.yaml
vendored
4
.github/workflows/deploy-api.yaml
vendored
|
|
@ -43,7 +43,7 @@ jobs:
|
|||
deploy-dev:
|
||||
name: Deploy API to dev
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.environment == 'dev')
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-fargate.yaml@2e2b3a282fbd148352a2b7433b58668a47e6bfeb # v1.0.16
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-fargate.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
|
@ -57,7 +57,7 @@ jobs:
|
|||
deploy-prod:
|
||||
name: Deploy API to prod
|
||||
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.environment == 'prod')
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-fargate.yaml@2e2b3a282fbd148352a2b7433b58668a47e6bfeb # v1.0.16
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-fargate.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
|
|
|||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
|
|
@ -10,4 +10,4 @@ permissions:
|
|||
|
||||
jobs:
|
||||
label:
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@2e2b3a282fbd148352a2b7433b58668a47e6bfeb # v1.0.16
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
|
||||
|
|
|
|||
|
|
@ -12,10 +12,6 @@ Use one of: `feature/`, `fix/`, `hotfix/`, `chore/`, `docs/`, `refactor/`, `rele
|
|||
- **Body sections** (exactly, in order): `Summary`, `Validation`, `Tests`, `Notes`. Use "None." under Notes when empty.
|
||||
- State verifiable facts only. Do not justify changes by citing the handbook. No AI-attribution footers.
|
||||
|
||||
## Security and Cross-Review
|
||||
- Sensitive surfaces (payment flows, authentication, secrets handling, untrusted input) require security review.
|
||||
- IAM role, policy, or resource-permission changes require cross-family review. Lambda handler signature changes alone do not.
|
||||
|
||||
## CI and Workflow References
|
||||
- CI must pass before merge.
|
||||
- Org-level reusable workflow refs must be pinned to a full commit SHA with a `# vX.Y.Z` comment.
|
||||
|
|
|
|||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -8,13 +8,13 @@
|
|||
"name": "afterhours-shift-manager",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.53.3"
|
||||
"@redocly/cli": "2.57.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redocly/cli": {
|
||||
"version": "2.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.53.3.tgz",
|
||||
"integrity": "sha512-hzNAWzHCOZ05vwRx0ehTxNeJaxxizjGV505eKtfs9MR/8ieD/8lYhAK4GF5FaqRRwOEpLO0PvzQwBSKYXmxhRA==",
|
||||
"version": "2.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.57.0.tgz",
|
||||
"integrity": "sha512-1d5fVyUaYlMNgCHUoyE2vUyxBhs/jmOHgsX/kFmfWzESw4f/G/OV/SU9E55rU7aUNmw9rHj1vXmL6yUdIn+KKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@
|
|||
"openapi:lint": "redocly lint --config .redocly.yaml openapi.yaml"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.53.3"
|
||||
"@redocly/cli": "2.57.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ def _deactivate(schedule: ShiftSchedule, date: str) -> dict:
|
|||
|
||||
|
||||
def handler(event, context):
|
||||
if os.environ.get("STAGE", "prod") != "prod":
|
||||
logger.info("Skipping holiday router because STAGE is not prod")
|
||||
return {"skipped": "non_prod"}
|
||||
|
||||
action = event.get("action")
|
||||
date = event.get("date")
|
||||
logger.info("Holiday router invoked: action=%s date=%s", action, date)
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
boto3>=1.43.99
|
||||
boto3>=1.43.107
|
||||
requests>=2.34.2
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
PyJWT[crypto]==2.14.0
|
||||
boto3>=1.43.99
|
||||
PyJWT[crypto]==2.15.1
|
||||
boto3>=1.43.107
|
||||
requests>=2.34.2
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ EASTERN = ZoneInfo("America/New_York")
|
|||
|
||||
|
||||
def handler(event, context):
|
||||
if os.environ.get("STAGE", "prod") != "prod":
|
||||
logger.info("Skipping 3CX queue scheduler because STAGE is not prod")
|
||||
return {"skipped": "non_prod"}
|
||||
|
||||
now = datetime.now(EASTERN)
|
||||
current_hour = now.hour
|
||||
day_name = now.strftime("%A")
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
boto3>=1.43.99
|
||||
boto3>=1.43.107
|
||||
requests>=2.34.2
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
boto3>=1.43.99
|
||||
boto3>=1.43.107
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ EXCLUDE_NAMES = {"Voicemail", "IVR", "Fax"}
|
|||
|
||||
|
||||
def handler(event, context):
|
||||
if os.environ.get("STAGE", "prod") != "prod":
|
||||
logger.info("Skipping roster sync because STAGE is not prod")
|
||||
return {"skipped": "non_prod"}
|
||||
|
||||
now = datetime.now(EASTERN)
|
||||
|
||||
# DST guard — two EventBridge rules fire, only one is at 6am ET
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
boto3>=1.43.99
|
||||
boto3>=1.43.107
|
||||
requests>=2.34.2
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ def create_app() -> Flask:
|
|||
|
||||
@app.route("/slack/events", methods=["POST"])
|
||||
def slack_events():
|
||||
if os.environ.get("STAGE", "prod") != "prod":
|
||||
return jsonify({"error": "slack_disabled"}), 404
|
||||
return _get_slack_handler().handle(request)
|
||||
|
||||
@app.route("/api/shifts", methods=["GET", "POST", "DELETE", "OPTIONS"])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
boto3>=1.43.99
|
||||
boto3>=1.43.107
|
||||
requests>=2.34.2
|
||||
sentry-sdk==2.69.2
|
||||
sentry-sdk==2.71.0
|
||||
|
||||
|
|
|
|||
14
src/shared/shared/effects.py
Normal file
14
src/shared/shared/effects.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"""External side effects. Only production may call Slack or 3CX.
|
||||
|
||||
A missing STAGE is treated as prod so a task that lost its environment
|
||||
variable does not silently drop production notifications. Dev and any
|
||||
other named stage skip Slack and 3CX entirely.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def prod_side_effects_enabled() -> bool:
|
||||
return os.environ.get("STAGE", "prod") == "prod"
|
||||
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
import logging
|
||||
import os
|
||||
|
||||
from shared.effects import prod_side_effects_enabled
|
||||
from shared.schedule import FALLBACK_EXTENSION, ShiftSchedule
|
||||
from shared.secrets import get_secret
|
||||
from shared.three_cx_client import ThreeCXClient, oauth_client
|
||||
|
|
@ -28,6 +29,9 @@ def holiday_extensions(holiday: dict) -> list[str]:
|
|||
|
||||
|
||||
def activate(schedule: ShiftSchedule, date: str, client_factory=None) -> dict:
|
||||
if not prod_side_effects_enabled():
|
||||
logger.info("Skipping holiday activate because STAGE is not prod")
|
||||
return {"action": "activate", "date": date, "skipped": "non_prod"}
|
||||
holiday = schedule.get_holiday(date)
|
||||
if holiday is None:
|
||||
logger.info("No holiday record for %s — nothing to activate", date)
|
||||
|
|
@ -77,6 +81,9 @@ def activate(schedule: ShiftSchedule, date: str, client_factory=None) -> dict:
|
|||
|
||||
|
||||
def deactivate(schedule: ShiftSchedule, date: str, client_factory=None) -> dict:
|
||||
if not prod_side_effects_enabled():
|
||||
logger.info("Skipping holiday deactivate because STAGE is not prod")
|
||||
return {"action": "deactivate", "date": date, "skipped": "non_prod"}
|
||||
holiday = schedule.get_holiday(date)
|
||||
if holiday is None:
|
||||
logger.info("No holiday record for %s — nothing to deactivate", date)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from shared.blocks import (
|
|||
build_swap_request_blocks,
|
||||
build_week_schedule,
|
||||
)
|
||||
from shared.effects import prod_side_effects_enabled
|
||||
from shared.ring_scheduler import update_queue_routing
|
||||
from shared.schedule import FALLBACK_EXTENSION, week_start
|
||||
from shared.secrets import get_secret
|
||||
|
|
@ -31,6 +32,8 @@ SLACK_API = "https://slack.com/api"
|
|||
|
||||
|
||||
def slack_token() -> str | None:
|
||||
if not prod_side_effects_enabled():
|
||||
return None
|
||||
secret_id = os.environ.get("SLACK_BOT_TOKEN_SECRET")
|
||||
if not secret_id:
|
||||
return None
|
||||
|
|
@ -42,6 +45,9 @@ def slack_token() -> str | None:
|
|||
|
||||
|
||||
def slack_call(method: str, token: str, **payload) -> bool:
|
||||
if not prod_side_effects_enabled():
|
||||
logger.info("Skipping Slack %s because STAGE is not prod", method)
|
||||
return False
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{SLACK_API}/{method}",
|
||||
|
|
@ -63,6 +69,9 @@ def slack_call(method: str, token: str, **payload) -> bool:
|
|||
|
||||
|
||||
def update_3cx_routing(extension: str) -> None:
|
||||
if not prod_side_effects_enabled():
|
||||
logger.info("Skipping 3CX routing because STAGE is not prod")
|
||||
return
|
||||
queue_number = os.environ.get("QUEUE_NUMBER")
|
||||
secret_prefix = os.environ.get("TCX_SECRET_PREFIX")
|
||||
if not queue_number or not secret_prefix:
|
||||
|
|
@ -89,6 +98,8 @@ def maybe_repoint_today(date_str: str, shift_type: str, extension: str) -> bool:
|
|||
|
||||
def make_3cx_client() -> ThreeCXClient | None:
|
||||
"""Return the process OAuth client, refreshing it when the token or secret changed."""
|
||||
if not prod_side_effects_enabled():
|
||||
return None
|
||||
secret_prefix = os.environ.get("TCX_SECRET_PREFIX")
|
||||
if not secret_prefix:
|
||||
logger.warning("3CX env vars not set — skipping 3CX call")
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
slack_bolt>=1.30.0,<2.0
|
||||
boto3>=1.43.99
|
||||
boto3>=1.43.107
|
||||
|
|
|
|||
|
|
@ -289,6 +289,10 @@ def _send_checkcomponents(pay_record: dict) -> bool:
|
|||
|
||||
|
||||
def handler(event, context):
|
||||
if os.environ.get("STAGE", "prod") != "prod":
|
||||
logger.info("Skipping weekly post because STAGE is not prod")
|
||||
return {"skipped": "non_prod"}
|
||||
|
||||
now = datetime.now(EASTERN)
|
||||
|
||||
# DST guard — same pattern as the 3CX scheduler
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
boto3>=1.43.99
|
||||
slack_sdk>=3.44.1,<4.0
|
||||
boto3>=1.43.107
|
||||
slack_sdk>=3.45.0,<4.0
|
||||
|
|
|
|||
64
terraform/.terraform.lock.hcl
generated
64
terraform/.terraform.lock.hcl
generated
|
|
@ -23,39 +23,39 @@ provider "registry.terraform.io/hashicorp/archive" {
|
|||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/aws" {
|
||||
version = "6.66.0"
|
||||
constraints = "~> 6.66"
|
||||
version = "6.67.0"
|
||||
constraints = "~> 6.67"
|
||||
hashes = [
|
||||
"h1:/yJhdVJVyi5k1KA4z1lDoYWQlTOPjR3NIAh/v4cLgLo=",
|
||||
"h1:5COqw8J20qkGI2ao1u8RTTguYEgiE3LJSbToD5fYUg4=",
|
||||
"h1:5t1vkYwqDYRN27RliDkyWRmQfQCnNHFqWxC2UDVCK78=",
|
||||
"h1:7Qtd6MjgS/ymociMyFCG9EKK6Jy5kGOy7EfXngFwsl4=",
|
||||
"h1:LgU7nnuiiD9m9YuYBNMArIgHex/RZqe4VFevYb/1kJU=",
|
||||
"h1:OnLj4nhqJnEcUzyyRKUjp1FgWG00Y8maikJEYSf9Zjw=",
|
||||
"h1:RhHqC2ugIjXrVhSu/Q6WYd/WOjjQ6rH27bh7LZuIW1w=",
|
||||
"h1:Rx4Ktpqk2eSvoPEIWB240IC67BkQxEXGmY/eRu6PIGk=",
|
||||
"h1:S8gYRM7I6/ufvF4dhBaAAGHm3f3+FKBUnEKR93Wcprs=",
|
||||
"h1:ZbkpwuEfpWTZDKdJnEZSDKN5tGEQTUYRkKuK6Cz2wcc=",
|
||||
"h1:c9A3yNQ0xB0wlJfG1XK3pfEHOEYx3HyJaQ56WnNv190=",
|
||||
"h1:cXBw4chYKvv6XlSvyVGAfSGKCAXwC0/fOAuq7xv7hME=",
|
||||
"h1:hBEaeBm9nm7A/u1nnD0nfolTPP55/BoKRFWk8zG8/fk=",
|
||||
"h1:mIolsCn33slp3F7Zd4KCTScXAWuUQsjtIzA/a6TFG6Q=",
|
||||
"h1:xehZnyesOrJ1/R9tmnRSu7FRkwoDDKelEHI3WdnJ72g=",
|
||||
"zh:156fe7164a3d26ef6b35734c43e99fb198df90575ed897d1182b8e930b8cd523",
|
||||
"zh:1af52b22b35be00f8d16e3ebebff9fa699ec4db2ef69e6032ba5c536f80c03d9",
|
||||
"zh:2545a8478bd551fdc9694f6cc1a1ad24617f6736f8bde0ad6cae90987c65380f",
|
||||
"zh:4070db1ee369ccb41cb610bfd887386bc0a9b9ecad60aeb4dbce58443d2519dd",
|
||||
"zh:53da7d3c1840ef875c7d34e967732502a64fe677af0e78824773d4c15a8fe740",
|
||||
"zh:576a93a28bf611a4de2a2e6ced697a41d5126b8fd31d30782b16797e410a9706",
|
||||
"zh:58fed5fa9a033355b9d4f3092c817b70d934100e0d8678d6e4c93f3c9493d4e4",
|
||||
"zh:6a9ca2f24e2ee9156dd785d159a850b35d190e9cf7eca21cb9582970c2db80cd",
|
||||
"zh:729edd30f99cc16009deba5c013265b0c81eda261a3d0821cbd011d3287fd230",
|
||||
"zh:7ae460049b75bd4aefee465ef7c53a01ac2df46d4d3e3ac00824afa8b5cb83fb",
|
||||
"zh:9051fa85c8034ade8a57a5c6f232fd33da28f3800bb5aa40bc8625dbc5e27632",
|
||||
"zh:906547e4319805e7acf7fbdf2bac28a4b1a7370790a2a430c7adb1b29bb934eb",
|
||||
"zh:998f27410a66158a35ee5ed142c27e5b21fe8601941da55da2157f8042d6dcca",
|
||||
"h1:01Y50Z+67vWZmsW9e8FQTj9NT/XW+x1n0YGdz2x4BpY=",
|
||||
"h1:6dffiL4BGVg+Ac2/64N+svhucYstBnVTTu55hWEhmq8=",
|
||||
"h1:8kRViFkyn96SufnuV3Oi3QmfL+DiyxlhAPcSX2jH0T0=",
|
||||
"h1:EAfdlvAeLCxhO9G5nnZ6Ti1zsmQP1aClgS41rneOijY=",
|
||||
"h1:EB9ixYOZrSlYD7wtJxf88qwoyyWrlKDKxhzaCLIb3t4=",
|
||||
"h1:Ksjd+RJVccOFRlbg3gpoJjL7T3SMPHxso2dPzAVTyRE=",
|
||||
"h1:OgdIUAQDtJBxlKjoPgChD1w57vl6hm6PyJHiBYgsgQA=",
|
||||
"h1:Syy68cIDOz7sXpxhjrkCwNHvmOj9VeaLVTJ/XnUKSuU=",
|
||||
"h1:Tz5wi4X4Gkj2I1Gif4MOtfrj4JerCz+5KqSi6Xj+n/A=",
|
||||
"h1:XaBXhLvtX5lUYkv5fHKzzMfoZXaIh5zU8hvM4jG2TXI=",
|
||||
"h1:fOwuAcOFTGOU0GY1m4NHhDgTAdTSiU+9qZ+REdp5HIU=",
|
||||
"h1:gyduBRrLO8EiRf8zA1aiLRoWydrUMw+TG0pUbOXo1g4=",
|
||||
"h1:iDfjW95J79sAMaOxeln73bKerm9SBemvLTrKepODumw=",
|
||||
"h1:xH+es0QQOteWlNptLqZzI6k8y94Aq/11S7lozotvO2c=",
|
||||
"h1:zLmFaFw5LptpWftHL3O6+7uykOH/0F9AmyVQ7V6kslY=",
|
||||
"zh:111d5686a1f4ccbc888bb5e2229308bcdd9149898c6af97969fcdfb0e7bd2aa0",
|
||||
"zh:21b3b7693bd9754c039fd546f03ed09e7510c6189aa5ee37176f144cf8dd5f95",
|
||||
"zh:25e03c7f025ff4851889537605aa560d2e39bd738b33a5204b8037eb164b475f",
|
||||
"zh:2817a046f1060e25bf04b0eb78f4e251130bb92dc82ec1264ce156dc9bf78e67",
|
||||
"zh:2d318d674c3ec9dbd97ddb10b12ad4827be4f508c43ba2ae1e0523baa9e367e3",
|
||||
"zh:2f07ab356718267299e10b6072472ded29d82753883027bf43bcd687ac72feb2",
|
||||
"zh:32307e67f83bc0dc94d38cfcd23d782166a09e0e975e2a5aa7d7a3e7ca5d3da8",
|
||||
"zh:4ce94853264097dd7f4542b3cfd18d2b98b06d23321db45d5e384ea275a57f63",
|
||||
"zh:869656c41cc7c7412f213e488f98167cff61deafcb8cf245d25d056e8dfdc263",
|
||||
"zh:8fd8c814e9d8edf152552047db23bf5b5d63f22e6b0b5d47b2af851376e99349",
|
||||
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
|
||||
"zh:9c1804eff1dda0446dc2d215231015bb65a2fc6c3b7ba24584fe45f1ddd3fa9f",
|
||||
"zh:b03ff5efdee310502aaaeb460144dc059bce72a0d8217e6b989099ef8aef9283",
|
||||
"zh:9cbc02ceef9bd469da497a1e7065ff984bdacfbe919ac3cce804a86c13b6e2c6",
|
||||
"zh:9ea55dda2767acfc1f6337fc7d29b1d4d79d6f8f04871f8ad99a6078d2865994",
|
||||
"zh:d7149df0819fb57a160489db45d33951765b8f0118daa3b4cd549a0135bc97a1",
|
||||
"zh:e5819937bb7043d08c9829b32d52d9fb55a5b9a4d8d55d550d47a3d7392db546",
|
||||
"zh:f93bc38dbc0ad53842303f30eec7a22c525c4d56209b54ec33a8d375cfc4e4fd",
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,16 @@ check "correct_account" {
|
|||
}
|
||||
}
|
||||
|
||||
check "dev_has_no_external_side_effects" {
|
||||
assert {
|
||||
condition = local.is_prod || alltrue([
|
||||
for name in ["SHIFT_CHANNEL", "QUEUE_NUMBER", "PAY_REPORT_USER", "TCX_SECRET_PREFIX"] :
|
||||
one([for env in local.api_environment : env.value if env.name == name]) == ""
|
||||
])
|
||||
error_message = "Non-prod must leave SHIFT_CHANNEL, QUEUE_NUMBER, PAY_REPORT_USER, and TCX_SECRET_PREFIX empty so the task cannot post to Slack or move the production phone queue."
|
||||
}
|
||||
}
|
||||
|
||||
check "dev_has_no_paychex" {
|
||||
assert {
|
||||
condition = local.is_prod || var.checkcomponents_queue_url == ""
|
||||
|
|
|
|||
|
|
@ -182,9 +182,9 @@ locals {
|
|||
{ name = "SHIFT_TABLE", value = aws_dynamodb_table.shifts.name },
|
||||
{ name = "SLACK_BOT_TOKEN_SECRET", value = "afterhours-shift-manager/slack-bot-token" },
|
||||
{ name = "SLACK_SIGNING_SECRET", value = "afterhours-shift-manager/slack-signing-secret" },
|
||||
{ name = "SHIFT_CHANNEL", value = var.shift_channel },
|
||||
{ name = "TCX_SECRET_PREFIX", value = "afterhours-shift-manager/3cx-" },
|
||||
{ name = "QUEUE_NUMBER", value = var.queue_number },
|
||||
{ name = "SHIFT_CHANNEL", value = local.is_prod ? var.shift_channel : "" },
|
||||
{ name = "TCX_SECRET_PREFIX", value = local.is_prod ? "afterhours-shift-manager/3cx-" : "" },
|
||||
{ name = "QUEUE_NUMBER", value = local.is_prod ? var.queue_number : "" },
|
||||
{ name = "TZ", value = var.timezone },
|
||||
{ name = "HOLIDAY_SCHEDULER_ROLE_ARN", value = local.holiday_scheduler_role_arn },
|
||||
{ name = "HOLIDAY_SCHEDULE_GROUP", value = "default" },
|
||||
|
|
@ -195,7 +195,7 @@ locals {
|
|||
var.portal_cognito_issuer != "" && var.portal_cognito_audience != "" ? [{ issuer = var.portal_cognito_issuer, audience = var.portal_cognito_audience }] : [],
|
||||
var.portal_cognito_extra_trust,
|
||||
)) },
|
||||
{ name = "PAY_REPORT_USER", value = var.pay_report_user },
|
||||
{ name = "PAY_REPORT_USER", value = local.is_prod ? var.pay_report_user : "" },
|
||||
{ name = "CHECKCOMPONENTS_QUEUE_URL", value = var.checkcomponents_queue_url },
|
||||
{ name = "ROSTER_API_TOKEN_SECRET", value = "afterhours-shift-manager/roster-api-token" },
|
||||
{ name = "SYNC_GROUP", value = "DEFAULT" },
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ terraform {
|
|||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.66"
|
||||
version = "~> 6.67"
|
||||
}
|
||||
archive = {
|
||||
source = "hashicorp/archive"
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ def test_ecs_task_boundary_uses_static_arns():
|
|||
|
||||
def test_deploy_api_workflow_exists():
|
||||
deploy_api = (ROOT / ".github" / "workflows" / "deploy-api.yaml").read_text()
|
||||
pin = "cd-hcp-fargate.yaml@2e2b3a282fbd148352a2b7433b58668a47e6bfeb # v1.0.16"
|
||||
pin = "cd-hcp-fargate.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21"
|
||||
assert deploy_api.count(pin) == 2
|
||||
assert "ssm-prefix: /afterhours-shift-manager/deploy" in deploy_api
|
||||
assert "docker-platform: linux/arm64" in deploy_api
|
||||
|
|
@ -119,9 +119,9 @@ def test_in_repo_hcptf_roles():
|
|||
|
||||
def test_ci_runs_pytest_and_terraform_validate():
|
||||
assert "ci-python-sam" not in CI
|
||||
assert "ci-python-app.yaml@2e2b3a282fbd148352a2b7433b58668a47e6bfeb # v1.0.16" in CI
|
||||
assert "ci-terraform.yaml@2e2b3a282fbd148352a2b7433b58668a47e6bfeb # v1.0.16" in CI
|
||||
assert "ci-autofix.yaml@2e2b3a282fbd148352a2b7433b58668a47e6bfeb # v1.0.16" in CI
|
||||
assert "ci-python-app.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21" in CI
|
||||
assert "ci-terraform.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21" in CI
|
||||
assert "ci-autofix.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21" in CI
|
||||
assert "name: ci-complete" in CI
|
||||
assert "pytest" in CI
|
||||
assert "terraform fmt -check" not in CI
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ moto[dynamodb,ses,secretsmanager]>=5.2.2
|
|||
responses>=0.26.2
|
||||
freezegun>=1.5.5
|
||||
sentry-sdk==2.68.1
|
||||
PyJWT[crypto]==2.14.0
|
||||
PyJWT[crypto]==2.15.0
|
||||
flask==3.1.3
|
||||
|
||||
|
|
|
|||
54
tests/shared/test_effects.py
Normal file
54
tests/shared/test_effects.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""Non-prod must not call Slack or 3CX."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from shared.effects import prod_side_effects_enabled
|
||||
from shared.holiday_flow import activate
|
||||
from shared.side_effects import slack_call, slack_token, update_3cx_routing
|
||||
|
||||
|
||||
def test_missing_stage_keeps_prod_effects(monkeypatch):
|
||||
monkeypatch.delenv("STAGE", raising=False)
|
||||
assert prod_side_effects_enabled() is True
|
||||
|
||||
|
||||
def test_dev_stage_disables_effects(monkeypatch):
|
||||
monkeypatch.setenv("STAGE", "dev")
|
||||
assert prod_side_effects_enabled() is False
|
||||
|
||||
|
||||
def test_dev_slack_token_does_not_read_secrets(monkeypatch):
|
||||
monkeypatch.setenv("STAGE", "dev")
|
||||
monkeypatch.setenv(
|
||||
"SLACK_BOT_TOKEN_SECRET", "afterhours-shift-manager/slack-bot-token"
|
||||
)
|
||||
read = MagicMock(side_effect=AssertionError("secret read"))
|
||||
monkeypatch.setattr("shared.side_effects.get_secret", read)
|
||||
assert slack_token() is None
|
||||
read.assert_not_called()
|
||||
|
||||
|
||||
def test_dev_slack_call_does_not_post(monkeypatch):
|
||||
monkeypatch.setenv("STAGE", "dev")
|
||||
post = MagicMock(side_effect=AssertionError("slack post"))
|
||||
monkeypatch.setattr("shared.side_effects.requests.post", post)
|
||||
assert slack_call("chat.postMessage", "xoxb-token", channel="C0APATP612N") is False
|
||||
post.assert_not_called()
|
||||
|
||||
|
||||
def test_dev_skips_3cx_even_when_queue_is_configured(monkeypatch):
|
||||
monkeypatch.setenv("STAGE", "dev")
|
||||
monkeypatch.setenv("QUEUE_NUMBER", "801")
|
||||
monkeypatch.setenv("TCX_SECRET_PREFIX", "afterhours-shift-manager/3cx-")
|
||||
route = MagicMock(side_effect=AssertionError("3cx"))
|
||||
monkeypatch.setattr("shared.side_effects.update_queue_routing", route)
|
||||
update_3cx_routing("101")
|
||||
route.assert_not_called()
|
||||
|
||||
|
||||
def test_dev_holiday_activate_does_not_build_a_client(monkeypatch):
|
||||
monkeypatch.setenv("STAGE", "dev")
|
||||
factory = MagicMock(side_effect=AssertionError("3cx client"))
|
||||
result = activate(MagicMock(), "2026-07-04", client_factory=factory)
|
||||
assert result["skipped"] == "non_prod"
|
||||
factory.assert_not_called()
|
||||
Loading…
Add table
Reference in a new issue