From f66dc81129d6758f494be78ae0dbb5c418cb19db Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Mon, 11 Aug 2025 00:31:40 -0700 Subject: [PATCH 01/32] feat(gha): more output logging --- scripts/build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build.sh b/scripts/build.sh index 7025796..83a1450 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -106,6 +106,7 @@ function main() { echo "sha256=${zip_sha256}" if [ -n "$GITHUB_OUTPUT" ]; then + echo "Writing Github Actions outputs" write_gha_outputs "$zip_size" "$zip_sha256" fi } From a7e5ec9fe38e84fbd0837a9c70c067b74762c19f Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 12:30:04 -0700 Subject: [PATCH 02/32] Split zip and image build scripts --- scripts/build-image.sh | 39 ++++++++++++++ scripts/build-zip.sh | 114 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100755 scripts/build-image.sh create mode 100755 scripts/build-zip.sh diff --git a/scripts/build-image.sh b/scripts/build-image.sh new file mode 100755 index 0000000..01f7aea --- /dev/null +++ b/scripts/build-image.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -e # Exit on any error + +AWS_ACCOUNT_ID="${1}" +AWS_REGION="${2}" + +function ensure_env_vars_are_set() { + USAGE="Usage: $0 " + if [ -z "${AWS_ACCOUNT_ID}" ]; then + echo "AWS_ACCOUNT_ID is required" + echo "${USAGE}" + exit 1 + elif [ -z "${AWS_REGION}" ]; then + echo "AWS_REGION is required" + echo "${USAGE}" + exit 1 + fi +} + +function build() { + echo "Building Lambda deployment package..." + docker build -t lambda-application . + echo "Build completed successfully." +} + +function tag() { + echo "Tagging image..." + docker tag lambda-application:latest ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/lambda-application:latest + echo "Tagging completed successfully." +} + +function main() { + ensure_env_vars_are_set; + build; + tag; +} + +main \ No newline at end of file diff --git a/scripts/build-zip.sh b/scripts/build-zip.sh new file mode 100755 index 0000000..83a1450 --- /dev/null +++ b/scripts/build-zip.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +set -e # Exit on any error + + +ENTRYPOINT_CODE_FILE="main.py" +DIST_FILE="lambda.zip" +DIST_PATH="dist/${DIST_FILE}" + + +function cleanup() { + echo "Cleaning up previous build..." + rm -rf dist + mkdir -p dist +} + + +function build() { + echo "Building Lambda deployment package..." + # Create zip with source files at root level, excluding test and cache files + pushd src > /dev/null + zip -r ../${DIST_PATH} . \ + -x "*.pyc" \ + -x "__pycache__/*" \ + -x "*.pyo" \ + -x "*.pyd" \ + -x "*.so" \ + -x "*.egg" \ + -x "*.egg-info" \ + -x "*.test.py" \ + -x "*_test.py" \ + -x "test_*.py" \ + -x "tests/*" \ + -x ".pytest_cache/*" \ + -x ".coverage" \ + -x "*.log" \ + -x ".DS_Store" \ + -x "Thumbs.db" + + popd > /dev/null + + echo "Build completed successfully!" +} + +function verify_build() { + echo "Verifying archive structure..." + + # Verify no test files are included + if unzip -l ${DIST_PATH} | grep -q "test"; then + echo "ERROR: Test files found in archive ${DIST_PATH}!" + echo "Contents:" + unzip -l ${DIST_PATH} | grep "test" + exit 1 + fi + + # Verify no cache files are included + if unzip -l ${DIST_PATH} | grep -q "__pycache__\|\.pyc\|\.pyo"; then + echo "ERROR: Cache files found in archive ${DIST_PATH}!" + echo "Contents:" + unzip -l ${DIST_PATH} | grep "__pycache__\|\.pyc\|\.pyo" + exit 1 + fi + + # Verify source files are at root level + if ! zipinfo -1 ${DIST_PATH} | grep -q "^${ENTRYPOINT_CODE_FILE}$"; then + echo "ERROR: Entrypoint file ${ENTRYPOINT_CODE_FILE} not found at root level in ${DIST_PATH}!" + echo "Contents:" + unzip -l ${DIST_PATH} | grep "^${ENTRYPOINT_CODE_FILE}$" + exit 1 + fi + + # Verify archive is not empty + ARCHIVE_SIZE=$(unzip -l ${DIST_PATH} | tail -1 | awk '{print $2}') + if [ "$ARCHIVE_SIZE" -eq 0 ]; then + echo "ERROR: Archive ${DIST_PATH} is empty!" + exit 1 + fi + + echo "Archive structure is correct." +} + +function sha256_hash() { + # shasum is available on macOS, sha256sum is available on Linux + if uname -a | grep -q "Darwin"; then + shasum -a 256 ${DIST_PATH} | awk '{print $1}' + else + sha256sum ${DIST_PATH} | awk '{print $1}' + fi +} + +function write_gha_outputs() { + echo "size=$1" >> "$GITHUB_OUTPUT" + echo "sha256=$2" >> "$GITHUB_OUTPUT" +} + +function main() { + cleanup + build + verify_build + + zip_size=$(ls -lh dist/lambda.zip | awk '{print $5}') + zip_sha256=$(sha256_hash) + echo "Lambda deployment package built" + echo "path=${DIST_PATH}" + echo "size=${zip_size}" + echo "sha256=${zip_sha256}" + + if [ -n "$GITHUB_OUTPUT" ]; then + echo "Writing Github Actions outputs" + write_gha_outputs "$zip_size" "$zip_sha256" + fi +} + +main From 1394727ef242ab908fd23127ac77826a343148f6 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 12:34:32 -0700 Subject: [PATCH 03/32] Update semantic version config for main VS feature branches --- pyproject.toml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c727376..54b6c0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,14 +53,25 @@ exclude_lines = [ "pragma: no cover", ] -# Decide next SemVer, create the tag, create the GitHub release [tool.semantic_release] -version_source = "tag" -tag_format = "v{version}" +version_source = "tag" +tag_format = "v{version}" +commit_parser = "conventional" allow_zero_version = true -# Handled by separate CI job. upload_to_vcs_release = false -# Use tag to set __version__/project version for build artifact -[tool.setuptools_scm] -tag_regex = "v?(?P\\d+\\.\\d+\\.\\d+.*)" +# Tune conventional commits behavior +[tool.semantic_release.commit_parser_options] +parse_squash_commits = true +ignore_merge_commits = true + +# Full releases from main +[tool.semantic_release.branches.main] +match = "^(main|master)$" +prerelease = false + +# Everything else => prerelease with token "dev" +[tool.semantic_release.branches.dev] +match = ".*" +prerelease = true +prerelease_token = "dev" From 287afbbd84bfd8a522ee8e6bf46820ac896868f6 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 12:34:45 -0700 Subject: [PATCH 04/32] Add script to get next version --- scripts/get-version.sh | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100755 scripts/get-version.sh diff --git a/scripts/get-version.sh b/scripts/get-version.sh new file mode 100755 index 0000000..00c8eef --- /dev/null +++ b/scripts/get-version.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -euo pipefail + +# A dummy token is set to avoid the missing token warning. We just need the version. +version="$(GH_TOKEN="dummy" uv run semantic-release version --print)" +echo "$version" \ No newline at end of file From 2e0bf8d40c5552f39b6922bcaf0a489fae54eadc Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 12:38:05 -0700 Subject: [PATCH 05/32] Use existing token if present --- scripts/get-version.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/get-version.sh b/scripts/get-version.sh index 00c8eef..38bffbd 100755 --- a/scripts/get-version.sh +++ b/scripts/get-version.sh @@ -3,5 +3,6 @@ set -euo pipefail # A dummy token is set to avoid the missing token warning. We just need the version. -version="$(GH_TOKEN="dummy" uv run semantic-release version --print)" +export GH_TOKEN="${GH_TOKEN:-dummy}" +version="$(uv run semantic-release version --print)" echo "$version" \ No newline at end of file From e62e66265820aaff1943540abcc91b2643aed311 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 12:52:35 -0700 Subject: [PATCH 06/32] Prerelease versions include metadata --- scripts/get-version.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/get-version.sh b/scripts/get-version.sh index 38bffbd..45c4c45 100755 --- a/scripts/get-version.sh +++ b/scripts/get-version.sh @@ -4,5 +4,17 @@ set -euo pipefail # A dummy token is set to avoid the missing token warning. We just need the version. export GH_TOKEN="${GH_TOKEN:-dummy}" -version="$(uv run semantic-release version --print)" +base_version="$(uv run semantic-release version --print)" + +if [[ "$base_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+- ]]; then + # Pre-release version. Add build metadata. + sha="$(git rev-parse --short HEAD)" + branch_raw="$(git rev-parse --abbrev-ref HEAD)" + branch_slug="$(echo "$branch_raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^0-9a-z-]+/-/g' | sed -E 's/^-+|-+$//g')" + version="$(uv run semantic-release version --print --build-metadata $sha.$branch_slug)" +else + # Full release version. + version="$base_version" +fi + echo "$version" \ No newline at end of file From 94dd211dd55014cea94ea7955d2c86d60ae24cb2 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 14:14:37 -0700 Subject: [PATCH 07/32] Just use the standard dev metadata in versions --- scripts/get-version.sh | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/scripts/get-version.sh b/scripts/get-version.sh index 45c4c45..c0e5e16 100755 --- a/scripts/get-version.sh +++ b/scripts/get-version.sh @@ -4,17 +4,6 @@ set -euo pipefail # A dummy token is set to avoid the missing token warning. We just need the version. export GH_TOKEN="${GH_TOKEN:-dummy}" -base_version="$(uv run semantic-release version --print)" -if [[ "$base_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+- ]]; then - # Pre-release version. Add build metadata. - sha="$(git rev-parse --short HEAD)" - branch_raw="$(git rev-parse --abbrev-ref HEAD)" - branch_slug="$(echo "$branch_raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^0-9a-z-]+/-/g' | sed -E 's/^-+|-+$//g')" - version="$(uv run semantic-release version --print --build-metadata $sha.$branch_slug)" -else - # Full release version. - version="$base_version" -fi - -echo "$version" \ No newline at end of file +version="$(uv run semantic-release version --print)" +echo "$version" From d7183e387aff958cf8a21829e7a4031cf93d2b43 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 15:35:24 -0700 Subject: [PATCH 08/32] Update demo app with metadata --- src/main.py | 86 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/src/main.py b/src/main.py index d211d78..809f370 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,86 @@ +import json +import logging +import os + +# Configure logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + def handler(event, context): - print("Hello from lambda-application!") + """ + Basic Lambda function handler + + Args: + event: AWS Lambda event data + context: AWS Lambda context object + + Returns: + dict: Response with status code and message + """ + + try: + APP_NAME = os.environ["APP_NAME"] + APP_VERSION = os.environ["APP_VERSION"] + COMMIT_SHA = os.environ["COMMIT_SHA"] + BRANCH = os.environ["BRANCH"] + BUILD_DATE = os.environ["BUILD_DATE"] + + logger.debug(f"APP_NAME: {APP_NAME}") + logger.debug(f"APP_VERSION: {APP_VERSION}") + logger.debug(f"COMMIT_SHA: {COMMIT_SHA}") + logger.debug(f"BRANCH: {BRANCH}") + logger.debug(f"BUILD_DATE: {BUILD_DATE}") + + except KeyError as e: + logger.error(f"Missing environment variable: {e}") + # Set default values if environment variables are missing + APP_NAME = os.environ.get("APP_NAME", "unknown") + APP_VERSION = os.environ.get("APP_VERSION", "unknown") + COMMIT_SHA = os.environ.get("COMMIT_SHA", "unknown") + BRANCH = os.environ.get("BRANCH", "unknown") + BUILD_DATE = os.environ.get("BUILD_DATE", "unknown") + + try: + # Log the incoming event + logger.info(f"Event received: {json.dumps(event)}") + + # Extract name from event if available, otherwise use default + name = event.get("name", "World") if event else "World" + + # Create response with proper structure + response = { + "statusCode": 200, + "body": { + "message": f"Hello, {name}!", + "metadata": { + "timestamp": context.get_remaining_time_in_millis() + if context and hasattr(context, "get_remaining_time_in_millis") + else None, + "function_name": context.function_name + if context and hasattr(context, "function_name") + else "unknown", + "function_version": context.function_version + if context and hasattr(context, "function_version") + else "unknown", + "app_name": APP_NAME, + "app_version": APP_VERSION, + "commit_sha": COMMIT_SHA, + "branch": BRANCH, + "build_date": BUILD_DATE, + }, + }, + } + + logger.info(f"Response: {json.dumps(response)}") + return response - return { - "statusCode": 200, - "body": "Hello from lambda-application!", - } + except Exception as e: + logger.error(f"Error: {str(e)}") + return { + "statusCode": 500, + "body": {"error": "Internal server error", "message": str(e)}, + } if __name__ == "__main__": From e8edf57b1d016a644b7cc36e287d24f052929014 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 15:35:32 -0700 Subject: [PATCH 09/32] Update tests to pass --- src/main_test.py | 101 ++++++++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 36 deletions(-) diff --git a/src/main_test.py b/src/main_test.py index 2362798..b364c59 100644 --- a/src/main_test.py +++ b/src/main_test.py @@ -1,51 +1,80 @@ -from unittest.mock import patch +import os from main import handler -def test_handler_prints_hello_message(): - """Test that handler() prints the expected hello message.""" - with patch("builtins.print") as mock_print: - handler(None, None) - mock_print.assert_called_once_with("Hello from lambda-application!") +class TestHandler: + def setup_class(self): + self.test_event = {"test": "data"} + self.test_context = {"function_name": "test-function"} -def test_handler_returns_correct_response(): - """Test that handler() returns the expected response structure.""" - result = handler(None, None) + os.environ["APP_NAME"] = "lambda-application" + os.environ["APP_VERSION"] = "1.0.0" + os.environ["COMMIT_SHA"] = "1234567890" + os.environ["BRANCH"] = "main" + os.environ["BUILD_DATE"] = "2021-01-01" - expected_response = {"statusCode": 200, "body": "Hello from lambda-application!"} + def teardown_class(self): + self.test_event = None + self.test_context = None - assert result == expected_response - assert result["statusCode"] == 200 - assert result["body"] == "Hello from lambda-application!" + def test_handler_returns_correct_response(self): + """Test that handler() returns the expected response structure.""" + result = handler(None, None) + # Check that the response has the expected structure + assert result["statusCode"] == 200 + assert "body" in result + assert "message" in result["body"] + assert "metadata" in result["body"] + + # Check message + assert result["body"]["message"] == "Hello, World!" + + # Check metadata structure + metadata = result["body"]["metadata"] + assert metadata["app_name"] == "lambda-application" + assert metadata["app_version"] == "1.0.0" + assert metadata["commit_sha"] == "1234567890" + assert metadata["branch"] == "main" + assert metadata["build_date"] == "2021-01-01" + + # Context-related fields should be None or 'unknown' when context is None + assert metadata["function_name"] in [None, "unknown"] + assert metadata["function_version"] in [None, "unknown"] + assert metadata["timestamp"] in [None, "unknown"] + + def test_handler_with_event_and_context(self): + """Test that handler() works with event and context parameters.""" + test_event = {"test": "data"} + test_context = {"function_name": "test-function"} -def test_handler_calls_print(): - """Test that handler() actually calls print function.""" - with patch("builtins.print") as mock_print: - handler(None, None) - assert mock_print.called - - -def test_handler_with_event_and_context(): - """Test that handler() works with event and context parameters.""" - test_event = {"test": "data"} - test_context = {"function_name": "test-function"} - - with patch("builtins.print") as mock_print: result = handler(test_event, test_context) - # Verify print was called - mock_print.assert_called_once_with("Hello from lambda-application!") - - # Verify response structure + # Check that the response has the expected structure assert result["statusCode"] == 200 - assert result["body"] == "Hello from lambda-application!" - - -def test_main_module_execution(): - """Test that the module can be executed directly.""" - with patch("builtins.print"): + assert "body" in result + assert "message" in result["body"] + assert "metadata" in result["body"] + + # Check message + assert result["body"]["message"] == "Hello, World!" + + # Check metadata structure + metadata = result["body"]["metadata"] + assert metadata["app_name"] == "lambda-application" + assert metadata["app_version"] == "1.0.0" + assert metadata["commit_sha"] == "1234567890" + assert metadata["branch"] == "main" + assert metadata["build_date"] == "2021-01-01" + + # Context-related fields should be 'unknown' when context is a dict + assert metadata["function_name"] == "unknown" + assert metadata["function_version"] == "unknown" + assert metadata["timestamp"] is None + + def test_main_module_execution(self): + """Test that the module can be executed directly.""" # Import and execute the main block import main From d9029863a781d51125a1be17bcd36bf08f6345b5 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 15:36:42 -0700 Subject: [PATCH 10/32] Update pyproject.toml with new SemVer config --- pyproject.toml | 4 ++-- uv.lock | 24 ------------------------ 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 54b6c0c..eff3b62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ dev = [ "python-semantic-release>=10.3.1", "requests>=2.32.4", "ruff>=0.12", - "setuptools-scm>=8.3.1", ] [tool.pytest.ini_options] @@ -57,6 +56,7 @@ exclude_lines = [ version_source = "tag" tag_format = "v{version}" commit_parser = "conventional" +version_variables = ["pyproject.toml:project.version"] allow_zero_version = true upload_to_vcs_release = false @@ -74,4 +74,4 @@ prerelease = false [tool.semantic_release.branches.dev] match = ".*" prerelease = true -prerelease_token = "dev" +prerelease_token = "dev" \ No newline at end of file diff --git a/uv.lock b/uv.lock index e3b17c7..36fecfa 100644 --- a/uv.lock +++ b/uv.lock @@ -281,7 +281,6 @@ dev = [ { name = "python-semantic-release" }, { name = "requests" }, { name = "ruff" }, - { name = "setuptools-scm" }, ] [package.metadata] @@ -293,7 +292,6 @@ dev = [ { name = "python-semantic-release", specifier = ">=10.3.1" }, { name = "requests", specifier = ">=2.32.4" }, { name = "ruff", specifier = ">=0.12" }, - { name = "setuptools-scm", specifier = ">=8.3.1" }, ] [[package]] @@ -604,28 +602,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718 }, ] -[[package]] -name = "setuptools" -version = "80.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, -] - -[[package]] -name = "setuptools-scm" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/19/7ae64b70b2429c48c3a7a4ed36f50f94687d3bfcd0ae2f152367b6410dff/setuptools_scm-8.3.1.tar.gz", hash = "sha256:3d555e92b75dacd037d32bafdf94f97af51ea29ae8c7b234cf94b7a5bd242a63", size = 78088 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/ac/8f96ba9b4cfe3e4ea201f23f4f97165862395e9331a424ed325ae37024a8/setuptools_scm-8.3.1-py3-none-any.whl", hash = "sha256:332ca0d43791b818b841213e76b1971b7711a960761c5bea5fc5cdb5196fbce3", size = 43935 }, -] - [[package]] name = "shellingham" version = "1.5.4" From 496a308d9fd7fc5e86c69981a4fea5a0f4e808cc Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 15:39:22 -0700 Subject: [PATCH 11/32] Add multi-stage Dockerfile for Lambda image --- Dockerfile | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a7abbfa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +######################################################## +# Stage 1: Install dependencies with uv and copy to /opt/python +######################################################## +FROM python:3.12-slim AS deps + +# Avoid prompts & keep images small +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_NO_CACHE_DIR=1 + +ARG APP_ROOT="/app" +WORKDIR ${APP_ROOT} + +# Bring in lockfiles first for better caching +COPY pyproject.toml ${APP_ROOT}/ +COPY uv.lock ${APP_ROOT}/ + +# Install uv and create a local venv, then sync (no dev deps) +RUN pip install --no-cache-dir uv +RUN uv sync + +# Materialize a Lambda-style "layer" dir with only runtime packages +# (Lambda adds /opt/python to sys.path automatically) +RUN mkdir -p /opt/python +RUN cp -a ${APP_ROOT}/.venv/lib/python3.12/site-packages/. /opt/python/ + + +######################################################## +# Stage 2: Create final AWS Lambda image +######################################################## +#FROM public.ecr.aws/lambda/python:3.12-arm64 +FROM public.ecr.aws/lambda/python:3.12 + +ARG APP_NAME +ARG APP_VERSION +ARG COMMIT_SHA +ARG BRANCH +ARG BUILD_DATE + +ENV APP_NAME="${APP_NAME}" +ENV APP_VERSION="${APP_VERSION}" +ENV COMMIT_SHA="${COMMIT_SHA}" +ENV BRANCH="${BRANCH}" +ENV BUILD_DATE="${BUILD_DATE}" + +LABEL org.opencontainers.image.name="${APP_NAME}" \ + org.opencontainers.image.version="${APP_VERSION}" \ + org.opencontainers.image.revision="${COMMIT_SHA}" \ + org.opencontainers.image.ref.branch="${BRANCH}" \ + org.opencontainers.image.created="${BUILD_DATE}" + +# This image automatically adds packages in /opt/python to sys.path +COPY --from=deps /opt/python /opt/python +COPY src/ ${LAMBDA_TASK_ROOT}/ + +ENV _HANDLER="main.handler" +CMD ["main.handler"] From fbb45b3d76a4c0ea5fee9a786b2643b7b25e7e52 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 15:41:36 -0700 Subject: [PATCH 12/32] Docker build script works --- scripts/build-image.sh | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/scripts/build-image.sh b/scripts/build-image.sh index 01f7aea..b7c3b9b 100755 --- a/scripts/build-image.sh +++ b/scripts/build-image.sh @@ -1,39 +1,44 @@ #!/bin/bash -set -e # Exit on any error +set -euo pipefail +# Arguments AWS_ACCOUNT_ID="${1}" AWS_REGION="${2}" -function ensure_env_vars_are_set() { - USAGE="Usage: $0 " - if [ -z "${AWS_ACCOUNT_ID}" ]; then - echo "AWS_ACCOUNT_ID is required" - echo "${USAGE}" - exit 1 - elif [ -z "${AWS_REGION}" ]; then - echo "AWS_REGION is required" - echo "${USAGE}" - exit 1 - fi -} +# Derived variables +APP_NAME="lambda-application" +APP_VERSION=$(uv run ./scripts/get-version.sh) +SHA=$(git rev-parse --short HEAD) +BRANCH=$(git rev-parse --abbrev-ref HEAD) +DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + function build() { echo "Building Lambda deployment package..." - docker build -t lambda-application . + docker build \ + --build-arg APP_NAME="$APP_NAME" \ + --build-arg APP_VERSION="$APP_VERSION" \ + --build-arg COMMIT_SHA="$SHA" \ + --build-arg BRANCH="$BRANCH" \ + --build-arg BUILD_DATE="$DATE" \ + -t "$APP_NAME:$APP_VERSION" . echo "Build completed successfully." } -function tag() { +function tag_ecr() { echo "Tagging image..." - docker tag lambda-application:latest ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/lambda-application:latest + docker tag "$APP_NAME:$APP_VERSION" "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/$APP_NAME:$APP_VERSION" echo "Tagging completed successfully." } function main() { - ensure_env_vars_are_set; build; - tag; + if [ -n "${AWS_ACCOUNT_ID}" ] && [ -n "${AWS_REGION}" ]; then + tag_ecr; + else + echo "AWS_ACCOUNT_ID and AWS_REGION are not both set. Skipping ECR tagging." + fi } main \ No newline at end of file From 356a61ad85f1c61027b865e1640aca9b7b39d2d9 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 15:46:08 -0700 Subject: [PATCH 13/32] Minor tweaks --- src/main_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main_test.py b/src/main_test.py index b364c59..4d1db25 100644 --- a/src/main_test.py +++ b/src/main_test.py @@ -5,9 +5,7 @@ class TestHandler: def setup_class(self): - self.test_event = {"test": "data"} - self.test_context = {"function_name": "test-function"} - + # Set environment variables expected by handler() os.environ["APP_NAME"] = "lambda-application" os.environ["APP_VERSION"] = "1.0.0" os.environ["COMMIT_SHA"] = "1234567890" From 58a5c4e25fb8b548b6030d3200ddef8cb1f8dc97 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 15:47:56 -0700 Subject: [PATCH 14/32] Update tests --- src/main_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main_test.py b/src/main_test.py index 4d1db25..8e9e65f 100644 --- a/src/main_test.py +++ b/src/main_test.py @@ -23,8 +23,8 @@ def test_handler_returns_correct_response(self): # Check that the response has the expected structure assert result["statusCode"] == 200 assert "body" in result - assert "message" in result["body"] - assert "metadata" in result["body"] + for key in ["message", "metadata"]: + assert key in result["body"], f"Key {key} not found in result['body']" # Check message assert result["body"]["message"] == "Hello, World!" From 147dd6c86c77a69ff7d5b7382c20f405554be69c Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 15:48:22 -0700 Subject: [PATCH 15/32] Tear down env vars --- src/main_test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main_test.py b/src/main_test.py index 8e9e65f..daabd70 100644 --- a/src/main_test.py +++ b/src/main_test.py @@ -13,8 +13,11 @@ def setup_class(self): os.environ["BUILD_DATE"] = "2021-01-01" def teardown_class(self): - self.test_event = None - self.test_context = None + del os.environ["APP_NAME"] + del os.environ["APP_VERSION"] + del os.environ["COMMIT_SHA"] + del os.environ["BRANCH"] + del os.environ["BUILD_DATE"] def test_handler_returns_correct_response(self): """Test that handler() returns the expected response structure.""" From 392307a52ba563448a2107bd0661e377d4474862 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 15:49:20 -0700 Subject: [PATCH 16/32] Better test failure messages --- src/main_test.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main_test.py b/src/main_test.py index daabd70..8061795 100644 --- a/src/main_test.py +++ b/src/main_test.py @@ -34,16 +34,19 @@ def test_handler_returns_correct_response(self): # Check metadata structure metadata = result["body"]["metadata"] - assert metadata["app_name"] == "lambda-application" - assert metadata["app_version"] == "1.0.0" - assert metadata["commit_sha"] == "1234567890" - assert metadata["branch"] == "main" - assert metadata["build_date"] == "2021-01-01" + for key in ["app_name", "app_version", "commit_sha", "branch", "build_date"]: + assert key in metadata, f"Key {key} not found in result['body']['metadata']" + + assert metadata["app_name"] == "lambda-application", f"App name {metadata['app_name']} != lambda-application" + assert metadata["app_version"] == "1.0.0", f"App version {metadata['app_version']} != 1.0.0" + assert metadata["commit_sha"] == "1234567890", f"Commit sha {metadata['commit_sha']} != 1234567890" + assert metadata["branch"] == "main", f"Branch {metadata['branch']} != main" + assert metadata["build_date"] == "2021-01-01", f"Build date {metadata['build_date']} != 2021-01-01" # Context-related fields should be None or 'unknown' when context is None - assert metadata["function_name"] in [None, "unknown"] - assert metadata["function_version"] in [None, "unknown"] - assert metadata["timestamp"] in [None, "unknown"] + assert metadata["function_name"] in [None, "unknown"], f"Function name {metadata['function_name']} not in [None, 'unknown']" + assert metadata["function_version"] in [None, "unknown"], f"Function version {metadata['function_version']} not in [None, 'unknown']" + assert metadata["timestamp"] in [None, "unknown"], f"Timestamp {metadata['timestamp']} not in [None, 'unknown']" def test_handler_with_event_and_context(self): """Test that handler() works with event and context parameters.""" From aac1f9a7782593b3a249eb6d66d9eb49aa7437ab Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 15:54:32 -0700 Subject: [PATCH 17/32] Refactor --- src/main.py | 19 ++++++++++--------- src/main_test.py | 25 ++++++++----------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/main.py b/src/main.py index 809f370..8ce5ca2 100644 --- a/src/main.py +++ b/src/main.py @@ -48,21 +48,22 @@ def handler(event, context): # Extract name from event if available, otherwise use default name = event.get("name", "World") if event else "World" + function_name = "unknown" + if context and hasattr(context, "function_name"): + function_name = context.function_name + + function_version = "unknown" + if context and hasattr(context, "function_version"): + function_version = context.function_version + # Create response with proper structure response = { "statusCode": 200, "body": { "message": f"Hello, {name}!", "metadata": { - "timestamp": context.get_remaining_time_in_millis() - if context and hasattr(context, "get_remaining_time_in_millis") - else None, - "function_name": context.function_name - if context and hasattr(context, "function_name") - else "unknown", - "function_version": context.function_version - if context and hasattr(context, "function_version") - else "unknown", + "function_name": function_name, + "function_version": function_version, "app_name": APP_NAME, "app_version": APP_VERSION, "commit_sha": COMMIT_SHA, diff --git a/src/main_test.py b/src/main_test.py index 8061795..630d41d 100644 --- a/src/main_test.py +++ b/src/main_test.py @@ -34,7 +34,7 @@ def test_handler_returns_correct_response(self): # Check metadata structure metadata = result["body"]["metadata"] - for key in ["app_name", "app_version", "commit_sha", "branch", "build_date"]: + for key in ["app_name", "app_version", "commit_sha", "branch", "build_date", "function_name", "function_version"]: assert key in metadata, f"Key {key} not found in result['body']['metadata']" assert metadata["app_name"] == "lambda-application", f"App name {metadata['app_name']} != lambda-application" @@ -46,36 +46,27 @@ def test_handler_returns_correct_response(self): # Context-related fields should be None or 'unknown' when context is None assert metadata["function_name"] in [None, "unknown"], f"Function name {metadata['function_name']} not in [None, 'unknown']" assert metadata["function_version"] in [None, "unknown"], f"Function version {metadata['function_version']} not in [None, 'unknown']" - assert metadata["timestamp"] in [None, "unknown"], f"Timestamp {metadata['timestamp']} not in [None, 'unknown']" def test_handler_with_event_and_context(self): """Test that handler() works with event and context parameters.""" - test_event = {"test": "data"} - test_context = {"function_name": "test-function"} + test_event = {"name": "test-name"} + test_context = {"function_name": "test-function-name", "function_version": "1"} result = handler(test_event, test_context) # Check that the response has the expected structure assert result["statusCode"] == 200 assert "body" in result - assert "message" in result["body"] - assert "metadata" in result["body"] + for key in ["message", "metadata"]: + assert key in result["body"], f"Key {key} not found in result['body']" # Check message - assert result["body"]["message"] == "Hello, World!" + assert result["body"]["message"] == "Hello, test-name!" # Check metadata structure metadata = result["body"]["metadata"] - assert metadata["app_name"] == "lambda-application" - assert metadata["app_version"] == "1.0.0" - assert metadata["commit_sha"] == "1234567890" - assert metadata["branch"] == "main" - assert metadata["build_date"] == "2021-01-01" - - # Context-related fields should be 'unknown' when context is a dict - assert metadata["function_name"] == "unknown" - assert metadata["function_version"] == "unknown" - assert metadata["timestamp"] is None + for key in ["app_name", "app_version", "commit_sha", "branch", "build_date", "function_name", "function_version"]: + assert key in metadata, f"Key {key} not found in result['body']['metadata']" def test_main_module_execution(self): """Test that the module can be executed directly.""" From 8672d76cd15fc75a6aeec33540e34f67e29f50ca Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 15:56:44 -0700 Subject: [PATCH 18/32] Allow default value --- scripts/build-image.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-image.sh b/scripts/build-image.sh index b7c3b9b..718144c 100755 --- a/scripts/build-image.sh +++ b/scripts/build-image.sh @@ -3,8 +3,8 @@ set -euo pipefail # Arguments -AWS_ACCOUNT_ID="${1}" -AWS_REGION="${2}" +AWS_ACCOUNT_ID="${1:-}" +AWS_REGION="${2:-}" # Derived variables APP_NAME="lambda-application" From b039299bbda1e385fcb1438a34b9c02f21ce610c Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 17:48:33 -0700 Subject: [PATCH 19/32] Update local-instance.sh script to work with Docker image --- scripts/local-instance.sh | 48 ++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/scripts/local-instance.sh b/scripts/local-instance.sh index d529bdc..072aab7 100755 --- a/scripts/local-instance.sh +++ b/scripts/local-instance.sh @@ -3,25 +3,24 @@ set -e operation="$1" +image="$2" -dist_path="$PWD/dist" -lambda_zip_path="$dist_path/lambda.zip" -lambda_task_path=".lambda_task" -lambda_task_handler="main.handler" - -image="public.ecr.aws/lambda/python:3.11" -container_name="lambda-integration-test" -container_port=9000 -invoke_url="http://localhost:${container_port}/2015-03-31/functions/function/invocations" - -function unzip_lambda_archive() { - rm -rf "${lambda_task_path}" && mkdir -p "${lambda_task_path}" - unzip -q "${lambda_zip_path}" -d "${lambda_task_path}" +function usage() { + echo "Usage: $0 " + echo "Example: $0 start lambda-application:3.2.1-dev.1" } -function clean_lambda_archive() { - if [ -d "${lambda_task_path}" ]; then - rm -rf "${lambda_task_path}" +function ensure_parameters() { + if [ -z "${operation}" ]; then + echo "Error: Operation is required" + usage + exit 1 + fi + + if [ -z "${image}" ]; then + echo "Error: Image is required" + usage + exit 1 fi } @@ -58,16 +57,15 @@ function wait_for_container_ready() { function start_container() { echo "Starting container ${container_name} with image: ${image}" echo "Port mapping: ${container_port}:8080" - echo "Handler: ${lambda_task_handler}" docker run --rm -d \ --name "${container_name}" \ -p "${container_port}:8080" \ - -v "$PWD/${lambda_task_path}":/var/task:ro \ - "${image}" \ - "${lambda_task_handler}" + "${image}" wait_for_container_ready + + echo "Example invocation: curl -s -X POST ${invoke_url} -d '{\"name\":\"Foo\"}' | jq" } function stop_container() { @@ -83,16 +81,20 @@ function main() { "start") echo "Starting container ${container_name}..." stop_container || true - clean_lambda_archive || true - unzip_lambda_archive start_container ;; "stop") echo -e "\n\nStopping container ${container_name}..." stop_container - clean_lambda_archive ;; esac } +ensure_parameters +image_name="$(echo ${image} | cut -d ':' -f 1)" +image_tag="$(echo ${image} | cut -d ':' -f 2)" +container_name="${image_name}-${image_tag}-integration-test" +container_port=9000 +invoke_url="http://localhost:${container_port}/2015-03-31/functions/function/invocations" + main \ No newline at end of file From fea8018826b666bde89556ae45fb1affd6349a00 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 17:59:28 -0700 Subject: [PATCH 20/32] Update integration test to match --- tests/integration_test.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 8fedd6e..5e701ac 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -22,5 +22,24 @@ def test_handler_function_returns_expected_message(self): # Verify response structure assert isinstance(result, dict), "Handler should return a dictionary" + assert "statusCode" in result, "Response should contain statusCode" assert "body" in result, "Response should contain body" - assert result["body"] == "Hello from lambda-application!", "Body should match expected message" + + # Check status code + assert result["statusCode"] == 200, f"Status code should be 200, got {result['statusCode']}" + + # Check body structure + body = result["body"] + assert isinstance(body, dict), "Body should be a dictionary" + assert "message" in body, "Body should contain message" + assert "metadata" in body, "Body should contain metadata" + + # Check message content + assert body["message"] == "Hello, World!", f"Message should be 'Hello, World!', got '{body['message']}'" + + # Check metadata structure + metadata = body["metadata"] + expected_metadata_keys = ["app_name", "app_version", "branch", "build_date"] + for key in expected_metadata_keys: + assert key in metadata, f"Metadata should contain {key}" + assert metadata[key] is not None, f"Metadata {key} should not be None" From 17a5e472759159ee84dd62dc600baf38b08f6687 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 18:04:58 -0700 Subject: [PATCH 21/32] Update integration-test task with deferred cleanup --- Taskfile.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 8639cfa..1b61ae7 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -44,15 +44,17 @@ tasks: - build cmds: - task: start-local-instance + - defer: { task: stop-local-instance } - ./scripts/integration-test.sh - - task: stop-local-instance start-local-instance: + desc: Start a local Lambda instance for testing run: once cmds: - - ./scripts/local-instance.sh start + - ./scripts/local-instance.sh start lambda-application:$(./scripts/get-version.sh) stop-local-instance: + desc: Stop the local Lambda instance run: once cmds: - - ./scripts/local-instance.sh stop + - ./scripts/local-instance.sh stop lambda-application:$(./scripts/get-version.sh) From ffb228fc37db2317d8fca043a24a337b5d006794 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 18:09:51 -0700 Subject: [PATCH 22/32] Refactor --- Taskfile.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 1b61ae7..268e2ec 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,6 +2,12 @@ version: "3" +vars: + DOCKER_IMAGE_NAME: lambda-application + DOCKER_IMAGE_TAG: + sh: ./scripts/get-version.sh + DOCKER_IMAGE: "{{.DOCKER_IMAGE_NAME}}:{{.DOCKER_IMAGE_TAG}}" + tasks: default: cmds: @@ -51,10 +57,10 @@ tasks: desc: Start a local Lambda instance for testing run: once cmds: - - ./scripts/local-instance.sh start lambda-application:$(./scripts/get-version.sh) + - ./scripts/local-instance.sh start {{.DOCKER_IMAGE}} stop-local-instance: desc: Stop the local Lambda instance run: once cmds: - - ./scripts/local-instance.sh stop lambda-application:$(./scripts/get-version.sh) + - ./scripts/local-instance.sh stop {{.DOCKER_IMAGE}} From 8ff7186a6ea8b0a9ca6321270c59a1ee6a95655d Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 20:53:59 -0700 Subject: [PATCH 23/32] Just use Task in PR workflow --- .github/workflows/pull-request-code.yml | 157 ---------------------- .github/workflows/pull-request-format.yml | 21 --- .github/workflows/pull-request.yml | 54 ++++++++ 3 files changed, 54 insertions(+), 178 deletions(-) delete mode 100644 .github/workflows/pull-request-code.yml delete mode 100644 .github/workflows/pull-request-format.yml create mode 100644 .github/workflows/pull-request.yml diff --git a/.github/workflows/pull-request-code.yml b/.github/workflows/pull-request-code.yml deleted file mode 100644 index de1b0e9..0000000 --- a/.github/workflows/pull-request-code.yml +++ /dev/null @@ -1,157 +0,0 @@ -name: PR Code Checks - -on: - pull_request: - branches: - - main - - master - types: - - opened - - synchronize - -env: - PYTHON_VERSION: "3.11" - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install uv - uses: astral-sh/setup-uv@v6.4.3 - with: - version: latest - - - name: Generate cache key - id: cache-key - run: | - echo "value=build-${{ hashFiles('pyproject.toml', 'uv.lock') }}-${{ runner.os }}" >> $GITHUB_OUTPUT - - - name: Install dependencies - run: ./scripts/setup.sh - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: | - .venv - .uv - key: ${{ steps.cache-key.outputs.value }} - - - name: Run linting - run: ./scripts/lint.sh - - build: - name: Build - runs-on: ubuntu-latest - needs: lint - outputs: - cache-key: ${{ steps.cache-key.outputs.value }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install uv - uses: astral-sh/setup-uv@v6.4.3 - with: - version: latest - - - name: Install dependencies - run: ./scripts/setup.sh - - - name: Build Lambda package - run: ./scripts/build.sh - - - name: Generate cache key - id: cache-key - run: | - echo "value=build-${{ hashFiles('pyproject.toml', 'uv.lock') }}-${{ runner.os }}" >> $GITHUB_OUTPUT - - - name: Cache dependencies and build artifacts - uses: actions/cache@v4 - with: - path: | - .venv - .uv - dist - key: ${{ steps.cache-key.outputs.value }} - - unit-test: - name: Unit Test - runs-on: ubuntu-latest - needs: build - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install uv - uses: astral-sh/setup-uv@v6.4.3 - with: - version: latest - - - name: Restore dependencies cache - uses: actions/cache@v4 - with: - path: | - .venv - .uv - dist - key: ${{ needs.build.outputs.cache-key }} - - - name: Run unit tests - run: ./scripts/unit-test.sh - - integration-test: - name: Integration Test - runs-on: ubuntu-latest - needs: build - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install uv - uses: astral-sh/setup-uv@v6.4.3 - with: - version: latest - - - name: Restore dependencies and build cache - uses: actions/cache@v4 - with: - path: | - .venv - .uv - dist - key: ${{ needs.build.outputs.cache-key }} - - - name: Start Lambda container - run: ./scripts/local-instance.sh start - - - name: Run integration tests - run: ./scripts/integration-test.sh - - - name: Stop Lambda container - if: always() - run: ./scripts/local-instance.sh stop diff --git a/.github/workflows/pull-request-format.yml b/.github/workflows/pull-request-format.yml deleted file mode 100644 index 37c24e0..0000000 --- a/.github/workflows/pull-request-format.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: PR Format Checks -on: - pull_request: - types: - - opened - - edited - - synchronize - - reopened - -jobs: - conventional-commit: - name: Conventional Commit Title - runs-on: ubuntu-latest - # permissions: - # pull-requests: read - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Validate PR title - run: ./scripts/conventional-commit.sh "${{ github.event.pull_request.title }}" diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..7a8b2a6 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,54 @@ +name: PR Checks + +on: + pull_request: + branches: + - main + - master + types: + - opened + - synchronize + - reopened + - edited + +jobs: + pr-code-checks: + name: PR Code Checks + runs-on: ubuntu-latest + if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Install Python + uses: actions/setup-python@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v6.4.3 + with: + version: latest + + - name: Setup + run: uv run task setup + + - name: Lint + run: uv run task lint + + - name: Unit Test + run: uv run task unit-test + + - name: Build + run: uv run task build + + - name: Integration Test + run: uv run task integration-test + + pr-content-checks: + name: PR Content Checks + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Validate PR title + run: ./scripts/conventional-commit.sh "${{ github.event.pull_request.title }}" From acec50e50cadeaec4c45d8b4a1980d5aee88e463 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 20:54:38 -0700 Subject: [PATCH 24/32] Add task binary to Python deps --- pyproject.toml | 3 ++- uv.lock | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index eff3b62..265d46b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ dependencies = [ [dependency-groups] dev = [ + "go-task-bin>=3.44.1", "pytest>=8.3", "pytest-cov>=6.2", "python-semantic-release>=10.3.1", @@ -74,4 +75,4 @@ prerelease = false [tool.semantic_release.branches.dev] match = ".*" prerelease = true -prerelease_token = "dev" \ No newline at end of file +prerelease_token = "dev" diff --git a/uv.lock b/uv.lock index 36fecfa..2f64ef6 100644 --- a/uv.lock +++ b/uv.lock @@ -230,6 +230,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168 }, ] +[[package]] +name = "go-task-bin" +version = "3.44.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/5e/12cda017fa77ee235745d2d67f696bbafc53918a9727806eaf090523804f/go_task_bin-3.44.1.tar.gz", hash = "sha256:dbb38dde0d3ca6445a51ed1b1c79969c40b27c6df58449012972f25f3051f54a", size = 571418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/80/b06753bf02af0cbd28721295a6dea8c951c06407d0e0a202133a0215535e/go_task_bin-3.44.1-py3-none-android_33_arm64_v8a.whl", hash = "sha256:7d7cf5df2729d20ed9d4fc5aa49c18b832b837cd5357b3f11f6b65824a29ee32", size = 6462583 }, + { url = "https://files.pythonhosted.org/packages/ba/24/9fe13951b9961810edfa39bed66c9c8c4fe88ec777e89033888c5c4c1311/go_task_bin-3.44.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5050861a1144fbd2c0e773aa23d918c0a68abcc9c9df02299feaa642e7dd1685", size = 6391126 }, + { url = "https://files.pythonhosted.org/packages/f5/c1/b626c8dbc5a956ed7d6ab9ad7fadf924245748e2b85f7ec9a50c57ac96ed/go_task_bin-3.44.1-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:be0baaf29567d430ac9275802290d3b43b9c5f4af79a9faf9893e11a73ec1eb0", size = 6846631 }, + { url = "https://files.pythonhosted.org/packages/3d/f3/b50ef4286a1dc2e600cf8890104c3b8d21713a592de8287d502f7b1e90e7/go_task_bin-3.44.1-py3-none-manylinux_2_28_aarch64.musllinux_1_2_aarch64.whl", hash = "sha256:8ab0174ebaa9113134f3b60f3e538ba926afd8e429e9e117b9d9776fd8f75fe4", size = 6153057 }, + { url = "https://files.pythonhosted.org/packages/b8/71/586a050b55953e48f09d17429c3f6a3101d26b37614065f974fcf8ae5c23/go_task_bin-3.44.1-py3-none-manylinux_2_28_ppc64le.musllinux_1_2_ppc64le.whl", hash = "sha256:7d02aefefa487edfe0e684054452302d60d683e5c36cb1f21712d2a7c4051acf", size = 6127582 }, + { url = "https://files.pythonhosted.org/packages/29/8c/cd1424ef91987e595b16a910a40efec343f1929402b82bbd36dd0ab358ae/go_task_bin-3.44.1-py3-none-manylinux_2_28_riscv64.musllinux_1_2_riscv64.whl", hash = "sha256:3d7b9d04171b4a901e40bf1ddf22db65db7b650bd05725691cf63bede4f293b7", size = 6355433 }, + { url = "https://files.pythonhosted.org/packages/c8/cb/03625bb25fe924e79e474c59a17bbbdbb2fedde2368373458db4e07e858b/go_task_bin-3.44.1-py3-none-manylinux_2_28_s390x.musllinux_1_2_s390x.whl", hash = "sha256:a49f0e15b353b9a501cf7de7a46e795c96f2e5139559aec5fcedf4526a53a9bf", size = 6602738 }, + { url = "https://files.pythonhosted.org/packages/68/19/eaaadf3c81f8179e3039fcd9e996d24190affde48bc89732209a0b773251/go_task_bin-3.44.1-py3-none-manylinux_2_28_x86_64.musllinux_1_2_x86_64.whl", hash = "sha256:2040cf1cf848fa9b6c2292c4d994f6322d237c76ba2c78c25735fc5528cf3032", size = 6735464 }, + { url = "https://files.pythonhosted.org/packages/74/42/1f7b0b27d60e7978bf87e227b5c482796b36d33e212a9ae476425a03407b/go_task_bin-3.44.1-py3-none-win_amd64.whl", hash = "sha256:ebfd39ef70433ec8adaa5b4c8b705dcbdfc9ff4998887a45572c9108f226fb09", size = 6929044 }, + { url = "https://files.pythonhosted.org/packages/da/24/87e864764d148bd9b18c40a7eef824d4340e44f2aa09d2e8c3a405d1e58e/go_task_bin-3.44.1-py3-none-win_arm64.whl", hash = "sha256:abf61a43a42854a0cc2e0036460d8f0fa924eaab7d9dab5069a431b61e06bd04", size = 6257152 }, +] + [[package]] name = "idna" version = "3.10" @@ -276,6 +294,7 @@ source = { virtual = "." } [package.dev-dependencies] dev = [ + { name = "go-task-bin" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "python-semantic-release" }, @@ -287,6 +306,7 @@ dev = [ [package.metadata.requires-dev] dev = [ + { name = "go-task-bin", specifier = ">=3.44.1" }, { name = "pytest", specifier = ">=8.3" }, { name = "pytest-cov", specifier = ">=6.2" }, { name = "python-semantic-release", specifier = ">=10.3.1" }, From c2b1bbcdcbe6c30f6923ffcf9310fa702eb15f13 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 20:54:56 -0700 Subject: [PATCH 25/32] Add get-version task --- Taskfile.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Taskfile.yml b/Taskfile.yml index 268e2ec..47e0fe2 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -24,6 +24,11 @@ tasks: cmds: - ./scripts/setup.sh + get-version: + run: once + cmds: + - ./scripts/get-version.sh + format: run: once cmds: From f3e3f20eae0c1ec790899a1467818ab606ae58d5 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 21:06:14 -0700 Subject: [PATCH 26/32] Checkout full history and tags --- .github/workflows/pull-request.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7a8b2a6..652e4f0 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -19,6 +19,14 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v5 + with: + ref: ${{ github.head_ref || github.ref }} + # Need full history and tags to find the last release. + fetch-depth: 0 + fetch-tags: true + + # Work around occasional fetch-tags quirks. + - run: git fetch --tags --force - name: Install Python uses: actions/setup-python@v5 From ae69e6ceac0b00f5d5473beb4222d5e2865d9460 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 21:18:05 -0700 Subject: [PATCH 27/32] Ignore dev dependencies in Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a7abbfa..b207694 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ COPY uv.lock ${APP_ROOT}/ # Install uv and create a local venv, then sync (no dev deps) RUN pip install --no-cache-dir uv -RUN uv sync +RUN uv sync --no-group dev --frozen # Materialize a Lambda-style "layer" dir with only runtime packages # (Lambda adds /opt/python to sys.path automatically) From a3caa7ddc11bc0db40e1e53d1e81c7e46a229424 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 21:18:22 -0700 Subject: [PATCH 28/32] Use build-image.sh for build task --- Taskfile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Taskfile.yml b/Taskfile.yml index 47e0fe2..4544ca0 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -42,7 +42,7 @@ tasks: build: run: once cmds: - - ./scripts/build.sh + - ./scripts/build-image.sh unit-test: run: once From 0d7fbb8abb88fa861b75b38f50da5c45959b028d Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 21:24:42 -0700 Subject: [PATCH 29/32] Add integration-test task without build dependency --- Taskfile.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Taskfile.yml b/Taskfile.yml index 4544ca0..571fc89 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -49,6 +49,13 @@ tasks: cmds: - ./scripts/unit-test.sh + integration-test-no-build: + run: once + cmds: + - task: start-local-instance + - defer: { task: stop-local-instance } + - ./scripts/integration-test.sh + integration-test: run: once deps: From 6e0b88b30f802a5124c3f80860a5af09a835c09a Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 21:24:57 -0700 Subject: [PATCH 30/32] Use integration-test-no-build in PR workflow --- .github/workflows/pull-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 652e4f0..30c501c 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -49,10 +49,10 @@ jobs: run: uv run task build - name: Integration Test - run: uv run task integration-test + run: uv run task integration-test-no-build pr-content-checks: - name: PR Content Checks + name: Content runs-on: ubuntu-latest steps: - name: Checkout code From 09dfc734ef397b168516c5d179dd39de88308b47 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 21:27:00 -0700 Subject: [PATCH 31/32] Update pr-code-checks name --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 30c501c..60dd096 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -13,7 +13,7 @@ on: jobs: pr-code-checks: - name: PR Code Checks + name: Code runs-on: ubuntu-latest if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' steps: From 1f1bebcd4f347173929f2e6b00f6b80fd1142c15 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Sat, 23 Aug 2025 21:32:17 -0700 Subject: [PATCH 32/32] Trigger PR workflow --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9040964..db0d4ff 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ -# lambda-application -Lambda app code and release, decoupled from Terraformed infrastructure +# Demo Lambda Application + +Lambda app code and release, decoupled from Terraformed infrastructure.