fix(api): retarget holidays per account and ship App Home changelog (PLAT-216)

This commit is contained in:
Adam Moussa 2026-09-21 14:55:47 -04:00
parent 5b272f9b4a
commit 2a3f30a9cb
No known key found for this signature in database
5 changed files with 55 additions and 12 deletions

View file

@ -6,6 +6,7 @@ terraform
tests tests
docs docs
*.md *.md
!src/slack-bot/CHANGELOG.md
__pycache__ __pycache__
.pytest_cache .pytest_cache
.mypy_cache .mypy_cache

View file

@ -21,6 +21,7 @@ from botocore.exceptions import ClientError
PROD_ACCOUNT = "011934824531" PROD_ACCOUNT = "011934824531"
DEV_ACCOUNT = "710827005802" DEV_ACCOUNT = "710827005802"
HOLIDAY_SCHEDULER_ROLE = "afterhours-shift-manager-holiday-scheduler"
PREFIXES = ("holiday-activate-", "holiday-deactivate-") PREFIXES = ("holiday-activate-", "holiday-deactivate-")
_AT = re.compile(r"^at\((\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\)$") _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})") _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 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: def holiday_sqs_input(action: str, date: str) -> str:
return json.dumps({"event": "holiday", "action": action, "date": date}) 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("--queue-arn", required=True)
parser.add_argument( parser.add_argument(
"--role-arn", "--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") parser.add_argument("--execute", action="store_true")
args = parser.parse_args() args = parser.parse_args()
@ -115,6 +121,7 @@ def main() -> int:
if account not in {PROD_ACCOUNT, DEV_ACCOUNT}: if account not in {PROD_ACCOUNT, DEV_ACCOUNT}:
print("profile is not seahaven-prod or seahaven-dev", file=sys.stderr) print("profile is not seahaven-prod or seahaven-dev", file=sys.stderr)
return 2 return 2
role_arn = args.role_arn.strip() or holiday_scheduler_role_arn(account)
client = _client(args.profile, args.region) client = _client(args.profile, args.region)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
@ -154,7 +161,7 @@ def main() -> int:
ActionAfterCompletion=detail.get("ActionAfterCompletion", "DELETE"), ActionAfterCompletion=detail.get("ActionAfterCompletion", "DELETE"),
Target={ Target={
"Arn": args.queue_arn, "Arn": args.queue_arn,
"RoleArn": args.role_arn, "RoleArn": role_arn,
"Input": payload, "Input": payload,
}, },
) )

View file

@ -2168,19 +2168,31 @@ def _admin_holiday_list(respond, schedule):
@functools.lru_cache(maxsize=1) @functools.lru_cache(maxsize=1)
def _changelog_text() -> str: 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 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 degrades to "no What's New section" rather than erroring. Lambda zips and
``$LAMBDA_TASK_ROOT/CHANGELOG.md`` (synced from the repo root). 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") tried = []
try: for path in _changelog_paths():
with open(path, encoding="utf-8") as fh: tried.append(path)
return fh.read() try:
except OSError: with open(path, encoding="utf-8") as fh:
logger.warning("CHANGELOG.md not found at %s — App Home omits What's New", path) return fh.read()
return "" 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 _HOME_OVERVIEW_DAYS = 60

View file

@ -39,3 +39,14 @@ def test_action_and_date_from_schedule_name_when_input_empty():
def test_holiday_sqs_input_shape(): def test_holiday_sqs_input_shape():
payload = mod.holiday_sqs_input("activate", "2026-07-04") payload = mod.holiday_sqs_input("activate", "2026-07-04")
assert payload == '{"event": "holiday", "action": "activate", "date": "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"
)

View file

@ -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 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): def test_publish_home_degrades_without_changelog(slackbot_app, client):
slackbot_app.publish_home(client, "U_BOB", "") slackbot_app.publish_home(client, "U_BOB", "")
view = client.views_publish.call_args.kwargs["view"] view = client.views_publish.call_args.kwargs["view"]