From 2a3f30a9cb7a635c4608a7ddcb80bf59aa78ad5c Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 21 Sep 2026 14:55:47 -0400 Subject: [PATCH] fix(api): retarget holidays per account and ship App Home changelog (PLAT-216) --- .dockerignore | 1 + .../retarget_holiday_schedules_to_sqs.py | 11 +++++-- src/slack-bot/app.py | 32 +++++++++++++------ .../test_retarget_holiday_schedules_to_sqs.py | 11 +++++++ tests/slack_bot/test_app_home.py | 12 +++++++ 5 files changed, 55 insertions(+), 12 deletions(-) diff --git a/.dockerignore b/.dockerignore index d73868a..812bd41 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,7 @@ terraform tests docs *.md +!src/slack-bot/CHANGELOG.md __pycache__ .pytest_cache .mypy_cache diff --git a/scripts/cutover/retarget_holiday_schedules_to_sqs.py b/scripts/cutover/retarget_holiday_schedules_to_sqs.py index c70ef8b..3633121 100644 --- a/scripts/cutover/retarget_holiday_schedules_to_sqs.py +++ b/scripts/cutover/retarget_holiday_schedules_to_sqs.py @@ -21,6 +21,7 @@ from botocore.exceptions import ClientError PROD_ACCOUNT = "011934824531" DEV_ACCOUNT = "710827005802" +HOLIDAY_SCHEDULER_ROLE = "afterhours-shift-manager-holiday-scheduler" PREFIXES = ("holiday-activate-", "holiday-deactivate-") _AT = re.compile(r"^at\((\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\)$") _DATE = re.compile(r"(\d{4}-\d{2}-\d{2})") @@ -78,6 +79,10 @@ def action_and_date(name: str, existing_input: str) -> tuple[str, str]: return action, date +def holiday_scheduler_role_arn(account: str) -> str: + return f"arn:aws:iam::{account}:role/tf-managed/{HOLIDAY_SCHEDULER_ROLE}" + + def holiday_sqs_input(action: str, date: str) -> str: return json.dumps({"event": "holiday", "action": action, "date": date}) @@ -106,7 +111,8 @@ def main() -> int: parser.add_argument("--queue-arn", required=True) parser.add_argument( "--role-arn", - default="arn:aws:iam::011934824531:role/tf-managed/afterhours-shift-manager-holiday-scheduler", + default="", + help="Scheduler execution role. Empty uses the tf-managed holiday role in the caller account.", ) parser.add_argument("--execute", action="store_true") args = parser.parse_args() @@ -115,6 +121,7 @@ def main() -> int: if account not in {PROD_ACCOUNT, DEV_ACCOUNT}: print("profile is not seahaven-prod or seahaven-dev", file=sys.stderr) return 2 + role_arn = args.role_arn.strip() or holiday_scheduler_role_arn(account) client = _client(args.profile, args.region) now = datetime.now(timezone.utc) @@ -154,7 +161,7 @@ def main() -> int: ActionAfterCompletion=detail.get("ActionAfterCompletion", "DELETE"), Target={ "Arn": args.queue_arn, - "RoleArn": args.role_arn, + "RoleArn": role_arn, "Input": payload, }, ) diff --git a/src/slack-bot/app.py b/src/slack-bot/app.py index e23bc3e..e6f4c4e 100644 --- a/src/slack-bot/app.py +++ b/src/slack-bot/app.py @@ -2168,19 +2168,31 @@ def _admin_holiday_list(respond, schedule): @functools.lru_cache(maxsize=1) def _changelog_text() -> str: - """Read the CHANGELOG shipped in this function's package. + """Read the CHANGELOG shipped next to this module. Lazy (never at import) and tolerant of a missing file, so the App Home tab - degrades to "no What's New section" rather than erroring. The copy lives at - ``$LAMBDA_TASK_ROOT/CHANGELOG.md`` (synced from the repo root). + degrades to "no What's New section" rather than erroring. Lambda zips and + the Fargate image both keep ``CHANGELOG.md`` beside ``app.py``. + ``LAMBDA_TASK_ROOT`` remains a fallback for the zip layout. """ - path = os.path.join(os.environ.get("LAMBDA_TASK_ROOT", "."), "CHANGELOG.md") - try: - with open(path, encoding="utf-8") as fh: - return fh.read() - except OSError: - logger.warning("CHANGELOG.md not found at %s — App Home omits What's New", path) - return "" + tried = [] + for path in _changelog_paths(): + tried.append(path) + try: + with open(path, encoding="utf-8") as fh: + return fh.read() + except OSError: + continue + logger.warning("CHANGELOG.md not found at %s — App Home omits What's New", tried) + return "" + + +def _changelog_paths() -> list[str]: + paths = [os.path.join(os.path.dirname(os.path.abspath(__file__)), "CHANGELOG.md")] + task_root = os.environ.get("LAMBDA_TASK_ROOT", "").strip() + if task_root: + paths.append(os.path.join(task_root, "CHANGELOG.md")) + return paths _HOME_OVERVIEW_DAYS = 60 diff --git a/tests/scripts/test_retarget_holiday_schedules_to_sqs.py b/tests/scripts/test_retarget_holiday_schedules_to_sqs.py index 7adcf4f..0d97c24 100644 --- a/tests/scripts/test_retarget_holiday_schedules_to_sqs.py +++ b/tests/scripts/test_retarget_holiday_schedules_to_sqs.py @@ -39,3 +39,14 @@ def test_action_and_date_from_schedule_name_when_input_empty(): def test_holiday_sqs_input_shape(): payload = mod.holiday_sqs_input("activate", "2026-07-04") assert payload == '{"event": "holiday", "action": "activate", "date": "2026-07-04"}' + + +def test_holiday_scheduler_role_arn_follows_caller_account(): + assert mod.holiday_scheduler_role_arn(mod.PROD_ACCOUNT) == ( + "arn:aws:iam::011934824531:role/tf-managed/" + "afterhours-shift-manager-holiday-scheduler" + ) + assert mod.holiday_scheduler_role_arn(mod.DEV_ACCOUNT) == ( + "arn:aws:iam::710827005802:role/tf-managed/" + "afterhours-shift-manager-holiday-scheduler" + ) diff --git a/tests/slack_bot/test_app_home.py b/tests/slack_bot/test_app_home.py index 902966c..3a688b9 100644 --- a/tests/slack_bot/test_app_home.py +++ b/tests/slack_bot/test_app_home.py @@ -42,6 +42,18 @@ def test_publish_home_publishes_latest_entry(slackbot_app, client): assert "v1.9.2" not in str(kwargs["view"]) # only the newest entry +def test_changelog_paths_include_package_copy(slackbot_app): + paths = slackbot_app._changelog_paths() + assert any(p.endswith("src/slack-bot/CHANGELOG.md") for p in paths) + + +def test_changelog_text_reads_package_copy(slackbot_app): + slackbot_app._changelog_text.cache_clear() + text = slackbot_app._changelog_text() + assert text + assert "## " in text + + def test_publish_home_degrades_without_changelog(slackbot_app, client): slackbot_app.publish_home(client, "U_BOB", "") view = client.views_publish.call_args.kwargs["view"]