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.
140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Build Lambda zips with src/shared bundled in. Used by deploy.yaml.
|
|
|
|
Each zip is functions/<name>/<sha>.zip on S3. GIT_SHA is written to
|
|
shared/build_info.py inside the zip so Sentry release is the commit, not a
|
|
runtime env var Terraform would own.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
# Keys match terraform/locals.tf local.functions.
|
|
FUNCTIONS = {
|
|
"slack_bot": ROOT / "src" / "slack-bot",
|
|
"weekly_post": ROOT / "src" / "weekly-post",
|
|
"roster_sync": ROOT / "src" / "roster-sync",
|
|
"roster_api": ROOT / "src" / "roster-api",
|
|
"ring_scheduler": ROOT / "src" / "ring-scheduler",
|
|
"holiday_router": ROOT / "src" / "holiday-router",
|
|
"release_notifier": ROOT / "src" / "release-notifier",
|
|
}
|
|
|
|
SKIP_INSTALL_PREFIXES = ("boto3", "botocore")
|
|
SKIP_COPY_NAMES = {"requirements.txt", "__pycache__"}
|
|
|
|
|
|
def _req_lines(path: Path) -> list[str]:
|
|
lines: list[str] = []
|
|
if not path.is_file():
|
|
return lines
|
|
for raw in path.read_text().splitlines():
|
|
line = raw.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
lower = line.lower()
|
|
if any(lower.startswith(prefix) for prefix in SKIP_INSTALL_PREFIXES):
|
|
continue
|
|
lines.append(line)
|
|
return lines
|
|
|
|
|
|
def _copy_tree(src: Path, dest: Path) -> None:
|
|
dest.mkdir(parents=True, exist_ok=True)
|
|
for item in src.iterdir():
|
|
if item.name in SKIP_COPY_NAMES or item.name.endswith(".pyc"):
|
|
continue
|
|
target = dest / item.name
|
|
if item.is_dir():
|
|
if item.name == "__pycache__":
|
|
continue
|
|
shutil.copytree(item, target, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
|
else:
|
|
shutil.copy2(item, target)
|
|
|
|
|
|
def build_function(name: str, src: Path, git_sha: str, out_dir: Path) -> Path:
|
|
with tempfile.TemporaryDirectory(prefix=f"afterhours-{name}-") as tmp:
|
|
dest = Path(tmp)
|
|
_copy_tree(src, dest)
|
|
shared_src = ROOT / "src" / "shared" / "shared"
|
|
_copy_tree(shared_src, dest / "shared")
|
|
(dest / "shared" / "build_info.py").write_text(
|
|
f'"""Pinned at zip time by scripts/package_lambdas.py."""\n\nGIT_SHA = "{git_sha}"\n',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
reqs = _req_lines(src / "requirements.txt") + _req_lines(
|
|
ROOT / "src" / "shared" / "requirements.txt"
|
|
)
|
|
# Preserve order, drop duplicates.
|
|
seen: set[str] = set()
|
|
unique: list[str] = []
|
|
for line in reqs:
|
|
if line not in seen:
|
|
seen.add(line)
|
|
unique.append(line)
|
|
if unique:
|
|
cmd = [
|
|
sys.executable,
|
|
"-m",
|
|
"pip",
|
|
"install",
|
|
"--disable-pip-version-check",
|
|
"--no-compile",
|
|
"--python-version",
|
|
"3.12",
|
|
"--platform",
|
|
"manylinux2014_aarch64",
|
|
"--only-binary=:all:",
|
|
"--target",
|
|
str(dest),
|
|
*unique,
|
|
]
|
|
subprocess.run(cmd, check=True)
|
|
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
zip_path = out_dir / f"{name}.zip"
|
|
if zip_path.exists():
|
|
zip_path.unlink()
|
|
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
for dirpath, dirnames, filenames in os.walk(dest):
|
|
dirnames[:] = [d for d in dirnames if d != "__pycache__"]
|
|
for filename in filenames:
|
|
if filename.endswith(".pyc"):
|
|
continue
|
|
full = Path(dirpath) / filename
|
|
rel = full.relative_to(dest)
|
|
zf.write(full, rel.as_posix())
|
|
return zip_path
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--git-sha", required=True)
|
|
parser.add_argument("--out-dir", type=Path, default=ROOT / "build" / "packages")
|
|
parser.add_argument("--only", nargs="*", default=())
|
|
args = parser.parse_args()
|
|
selected = args.only or list(FUNCTIONS)
|
|
missing = [name for name in selected if name not in FUNCTIONS]
|
|
if missing:
|
|
print(f"unknown function keys: {missing}", file=sys.stderr)
|
|
return 2
|
|
for name in selected:
|
|
path = build_function(name, FUNCTIONS[name], args.git_sha, args.out_dir)
|
|
print(path)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|