mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 07:53:11 +00:00
Prod is already a human GitHub Release. Remove the SAM tagging path so CHANGELOG.md stays App Home copy and leftover notifier IAM is destroyed on the next apply.
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",
|
|
"portal_api": ROOT / "src" / "portal-api",
|
|
}
|
|
|
|
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())
|