mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 11:23:12 +00:00
Some checks are pending
Deploy / Deploy to prod (push) Waiting to run
* feat(portal-api): add Cognito shift API for the employee portal (DEV-287) Employees and admins can pick, drop, swap, and manage coverage through GET/POST/DELETE /api/shifts. Roster PUT accepts optional email for portal identity. Slack slash commands and App Home admin modals stay in place. Co-authored-by: adam <adam@seahavenind.com> * fix(portal-api): preserve shift and deployment invariants (DEV-287) Co-authored-by: adam <adam@seahavenind.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
141 lines
4.7 KiB
Python
141 lines
4.7 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",
|
|
"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())
|