From 27308a61b22acb227cd7271cdfea711428b394c2 Mon Sep 17 00:00:00 2001 From: Brent Champion Date: Sat, 15 Nov 2025 18:46:51 -0500 Subject: [PATCH 1/2] feat: update SAM template automatically and add execution role with required lambda permissions to template --- .github/workflows/update-sam-template.yml | 45 +++++++++ examples/cli.py | 65 ------------- examples/scripts/generate_sam_template.py | 106 ++++++++++++++++++++++ pyproject.toml | 2 +- 4 files changed, 152 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/update-sam-template.yml create mode 100644 examples/scripts/generate_sam_template.py diff --git a/.github/workflows/update-sam-template.yml b/.github/workflows/update-sam-template.yml new file mode 100644 index 0000000..6b4f239 --- /dev/null +++ b/.github/workflows/update-sam-template.yml @@ -0,0 +1,45 @@ +name: Update SAM Template + +on: + pull_request: + paths: + - "examples/**" + +permissions: + contents: write + +concurrency: + group: ${{ github.head_ref }}-${{ github.run_id}}-sam-template + cancel-in-progress: true + +jobs: + update-template: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ github.head_ref }} + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install PyYAML + run: pip install PyYAML + + - name: Generate SAM template + run: python examples/scripts/generate_sam_template.py + + - name: Commit and push changes + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add . + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "chore: update SAM template" --no-verify + git push + fi diff --git a/examples/cli.py b/examples/cli.py index 3de993e..8a7c0c4 100755 --- a/examples/cli.py +++ b/examples/cli.py @@ -215,59 +215,6 @@ def bootstrap_account(): return True -def generate_sam_template(*, skip_durable_config=False): - """Generate SAM template for all examples.""" - catalog = load_catalog() - - template = { - "AWSTemplateFormatVersion": "2010-09-09", - "Transform": "AWS::Serverless-2016-10-31", - "Globals": { - "Function": { - "Runtime": "python3.13", - "Timeout": 60, - "MemorySize": 128, - "Environment": { - "Variables": {"AWS_ENDPOINT_URL_LAMBDA": {"Ref": "LambdaEndpoint"}} - }, - } - }, - "Parameters": { - "LambdaEndpoint": { - "Type": "String", - "Default": "https://lambda.us-west-2.amazonaws.com", - } - }, - "Resources": {}, - } - - for example in catalog["examples"]: - # Convert handler name to PascalCase (e.g., hello_world -> HelloWorld) - handler_base = example["handler"].replace(".handler", "") - function_name = "".join(word.capitalize() for word in handler_base.split("_")) - template["Resources"][function_name] = { - "Type": "AWS::Serverless::Function", - "Properties": { - "CodeUri": "build/", - "Handler": example["handler"], - "Description": example["description"], - }, - } - - if not skip_durable_config and "durableConfig" in example: - template["Resources"][function_name]["Properties"]["DurableConfig"] = ( - example["durableConfig"] - ) - - import yaml - - template_path = Path(__file__).parent / "template.yaml" - with open(template_path, "w") as f: - yaml.dump(template, f, default_flow_style=False, sort_keys=False) - - return True - - def create_deployment_package(example_name: str) -> Path: """Create deployment package for example.""" @@ -484,16 +431,6 @@ def main(): # List command subparsers.add_parser("list", help="List available examples") - # SAM template command - sam_parser = subparsers.add_parser( - "sam", help="Generate SAM template for all examples" - ) - sam_parser.add_argument( - "--skip-durable-config", - action="store_true", - help="Skip adding DurableConfig properties to functions", - ) - # Deploy command deploy_parser = subparsers.add_parser("deploy", help="Deploy an example") deploy_parser.add_argument("example_name", help="Name of example to deploy") @@ -529,8 +466,6 @@ def main(): build_examples() elif args.command == "list": list_examples() - elif args.command == "sam": - generate_sam_template(skip_durable_config=args.skip_durable_config) elif args.command == "deploy": deploy_function(args.example_name, args.function_name) elif args.command == "invoke": diff --git a/examples/scripts/generate_sam_template.py b/examples/scripts/generate_sam_template.py new file mode 100644 index 0000000..56053e1 --- /dev/null +++ b/examples/scripts/generate_sam_template.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +import json +from pathlib import Path + +import yaml + + +def load_catalog(): + """Load examples catalog.""" + catalog_path = Path(__file__).parent.parent / "examples-catalog.json" + with open(catalog_path) as f: + return json.load(f) + + +def generate_sam_template(): + """Generate SAM template for all examples.""" + catalog = load_catalog() + + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Globals": { + "Function": { + "Runtime": "python3.13", + "Timeout": 60, + "MemorySize": 128, + "Environment": { + "Variables": {"AWS_ENDPOINT_URL_LAMBDA": {"Ref": "LambdaEndpoint"}} + }, + } + }, + "Parameters": { + "LambdaEndpoint": { + "Type": "String", + "Default": "https://lambda.us-west-2.amazonaws.com", + } + }, + "Resources": { + "DurableFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Policies": [ + { + "PolicyName": "DurableExecutionPolicy", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "lambda:CheckpointDurableExecution", + "lambda:GetDurableExecutionState", + ], + "Resource": "*", + } + ], + }, + } + ], + }, + } + }, + } + + for example in catalog["examples"]: + # Convert handler name to PascalCase (e.g., hello_world -> HelloWorld) + handler_base = example["handler"].replace(".handler", "") + function_name = "".join(word.capitalize() for word in handler_base.split("_")) + template["Resources"][function_name] = { + "Type": "AWS::Serverless::Function", + "Properties": { + "CodeUri": "build/", + "Handler": example["handler"], + "Description": example["description"], + "Role": {"Fn::GetAtt": ["DurableFunctionRole", "Arn"]}, + }, + } + + if "durableConfig" in example: + template["Resources"][function_name]["Properties"]["DurableConfig"] = ( + example["durableConfig"] + ) + + template_path = Path(__file__).parent.parent / "template.yaml" + with open(template_path, "w") as f: + yaml.dump(template, f, default_flow_style=False, sort_keys=False) + + print(f"Generated SAM template at {template_path}") + + +if __name__ == "__main__": + generate_sam_template() diff --git a/pyproject.toml b/pyproject.toml index a0fd191..326cb16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ dependencies = [ [tool.hatch.envs.examples.scripts] cli = "python examples/cli.py {args}" bootstrap = "python examples/cli.py bootstrap" -generate-sam = "python examples/cli.py sam {args}" +generate-sam-template = "python examples/scripts/generate_sam_template.py" build = "python examples/cli.py build" deploy = "python examples/cli.py deploy {args}" invoke = "python examples/cli.py invoke {args}" From fb35d1bca711f8f46ac4c05b8824e6011803dc6c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 16 Nov 2025 00:25:31 +0000 Subject: [PATCH 2/2] chore: update SAM template --- examples/template.yaml | 190 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 179 insertions(+), 11 deletions(-) diff --git a/examples/template.yaml b/examples/template.yaml index 5559af9..368d31e 100644 --- a/examples/template.yaml +++ b/examples/template.yaml @@ -1,4 +1,4 @@ -AWSTemplateFormatVersion: "2010-09-09" +AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Globals: Function: @@ -14,12 +14,38 @@ Parameters: Type: String Default: https://lambda.us-west-2.amazonaws.com Resources: + DurableFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: DurableExecutionPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - lambda:CheckpointDurableExecution + - lambda:GetDurableExecutionState + Resource: '*' HelloWorld: Type: AWS::Serverless::Function Properties: CodeUri: build/ Handler: hello_world.handler Description: A simple hello world example with no durable operations + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -29,6 +55,10 @@ Resources: CodeUri: build/ Handler: step.handler Description: Basic usage of context.step() to checkpoint a simple operation + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -38,6 +68,10 @@ Resources: CodeUri: build/ Handler: step_with_name.handler Description: Step operation with explicit name parameter + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -47,6 +81,10 @@ Resources: CodeUri: build/ Handler: step_with_retry.handler Description: Usage of context.step() with retry configuration for fault tolerance + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -56,6 +94,23 @@ Resources: CodeUri: build/ Handler: wait.handler Description: Basic usage of context.wait() to pause execution + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn + DurableConfig: + RetentionPeriodInDays: 7 + ExecutionTimeout: 300 + MultipleWait: + Type: AWS::Serverless::Function + Properties: + CodeUri: build/ + Handler: multiple_wait.handler + Description: Usage of demonstrating multiple sequential wait operations. + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -64,9 +119,12 @@ Resources: Properties: CodeUri: build/ Handler: callback.handler - Description: - Basic usage of context.create_callback() to create a callback for + Description: Basic usage of context.create_callback() to create a callback for external systems + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -75,9 +133,12 @@ Resources: Properties: CodeUri: build/ Handler: wait_for_callback.handler - Description: - Usage of context.wait_for_callback() to wait for external system + Description: Usage of context.wait_for_callback() to wait for external system responses + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -86,9 +147,12 @@ Resources: Properties: CodeUri: build/ Handler: run_in_child_context.handler - Description: - Usage of context.run_in_child_context() to execute operations in + Description: Usage of context.run_in_child_context() to execute operations in isolated contexts + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -98,6 +162,10 @@ Resources: CodeUri: build/ Handler: parallel.handler Description: Executing multiple durable operations in parallel + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -107,6 +175,10 @@ Resources: CodeUri: build/ Handler: map_operations.handler Description: Processing collections using map-like durable operations + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -116,6 +188,10 @@ Resources: CodeUri: build/ Handler: block_example.handler Description: Nested child contexts demonstrating block operations + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -125,6 +201,10 @@ Resources: CodeUri: build/ Handler: logger_example.handler Description: Demonstrating logger usage and enrichment in DurableContext + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -134,6 +214,10 @@ Resources: CodeUri: build/ Handler: steps_with_retry.handler Description: Multiple steps with retry logic in a polling pattern + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -143,6 +227,37 @@ Resources: CodeUri: build/ Handler: wait_for_condition.handler Description: Polling pattern that waits for a condition to be met + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn + DurableConfig: + RetentionPeriodInDays: 7 + ExecutionTimeout: 300 + RunInChildContextLargeData: + Type: AWS::Serverless::Function + Properties: + CodeUri: build/ + Handler: run_in_child_context_large_data.handler + Description: Usage of context.run_in_child_context() to execute operations in + isolated contexts with large data + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn + DurableConfig: + RetentionPeriodInDays: 7 + ExecutionTimeout: 300 + SimpleExecution: + Type: AWS::Serverless::Function + Properties: + CodeUri: build/ + Handler: simple_execution.handler + Description: Simple execution without durable execution + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -152,6 +267,10 @@ Resources: CodeUri: build/ Handler: map_with_max_concurrency.handler Description: Map operation with maxConcurrency limit + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -161,6 +280,10 @@ Resources: CodeUri: build/ Handler: map_with_min_successful.handler Description: Map operation with min_successful completion config + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -170,6 +293,10 @@ Resources: CodeUri: build/ Handler: map_with_failure_tolerance.handler Description: Map operation with failure tolerance + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -179,6 +306,10 @@ Resources: CodeUri: build/ Handler: parallel_with_max_concurrency.handler Description: Parallel operation with maxConcurrency limit + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -188,6 +319,10 @@ Resources: CodeUri: build/ Handler: parallel_with_wait.handler Description: Parallel operation with wait operations in branches + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 @@ -197,42 +332,75 @@ Resources: CodeUri: build/ Handler: parallel_with_failure_tolerance.handler Description: Parallel operation with failure tolerance + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 - MapWithCustomSerDes: + MapWithCustomSerdes: Type: AWS::Serverless::Function Properties: CodeUri: build/ Handler: map_with_custom_serdes.handler Description: Map operation with custom item-level serialization + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 - MapWithBatchSerDes: + MapWithBatchSerdes: Type: AWS::Serverless::Function Properties: CodeUri: build/ Handler: map_with_batch_serdes.handler Description: Map operation with custom batch-level serialization + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 - ParallelWithCustomSerDes: + ParallelWithCustomSerdes: Type: AWS::Serverless::Function Properties: CodeUri: build/ Handler: parallel_with_custom_serdes.handler Description: Parallel operation with custom item-level serialization + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 - ParallelWithBatchSerDes: + ParallelWithBatchSerdes: Type: AWS::Serverless::Function Properties: CodeUri: build/ Handler: parallel_with_batch_serdes.handler Description: Parallel operation with custom batch-level serialization + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn + DurableConfig: + RetentionPeriodInDays: 7 + ExecutionTimeout: 300 + HandlerError: + Type: AWS::Serverless::Function + Properties: + CodeUri: build/ + Handler: handler_error.handler + Description: Simple function with handler error + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300