From a68f1d95c444a484aedc90b2130c62a3410dc965 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 13 Oct 2026 11:21:18 +0530 Subject: [PATCH 1/4] Add CI that deploys and tests each example A matrix job deploys each of the five examples with lstk and checks the site and its backend respond. Fix make install skipping the CDK CLI when lstk is present, and make deploy-dynamodb deploying the api-gateway example. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- .github/workflows/main.yml | 62 ++++++++++++++++++++++++ Makefile | 15 ++++-- test.sh | 99 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100755 test.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..ead98c7 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,62 @@ +name: Deploy on LocalStack + +on: + push: + paths-ignore: + - '**.md' + branches: + - master + pull_request: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + +jobs: + example: + name: ${{ matrix.example }} + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + example: + - s3 + - cf-s3 + - lambda + - api-gateway + - dynamodb + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install lstk and the CDK CLI + run: | + npm install -g @localstack/lstk aws-cdk + lstk setup aws --force --non-interactive + + - name: Install dependencies + working-directory: ${{ matrix.example }} + run: npm ci + + - name: Start LocalStack + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + run: lstk start + + - name: Deploy the example + run: make deploy EXAMPLE=${{ matrix.example }} + + - name: Run tests + run: make test EXAMPLE=${{ matrix.example }} + + - name: Show LocalStack logs + if: failure() + run: lstk logs | tail -300 diff --git a/Makefile b/Makefile index f1b0829..34c51ba 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ +EXAMPLE ?= s3 + usage: ## Shows usage for this Makefile @cat Makefile | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' install: ## Install dependencies - which lstk || npm install -g @localstack/lstk aws-cdk + which lstk || npm install -g @localstack/lstk + which cdk || npm install -g aws-cdk cd api-gateway && npm install cd cf-s3 && npm install cd dynamodb && npm install @@ -30,8 +33,14 @@ deploy-api-gateway: ## Deploy the app to LocalStack lstk cdk deploy --require-approval=never deploy-dynamodb: ## Deploy the app to LocalStack - cd api-gateway && \ + cd dynamodb && \ lstk cdk bootstrap && \ lstk cdk deploy --require-approval=never -.PHONY: usage install deploy-s3 deploy-cf-s3 deploy-lambda deploy-api-gateway deploy-dynamodb \ No newline at end of file +deploy: ## Deploy one example to LocalStack, e.g. make deploy EXAMPLE=lambda + $(MAKE) deploy-$(EXAMPLE) + +test: ## Test a deployed example, e.g. make test EXAMPLE=lambda + ./test.sh $(EXAMPLE) + +.PHONY: usage install deploy deploy-s3 deploy-cf-s3 deploy-lambda deploy-api-gateway deploy-dynamodb test diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..067f0ea --- /dev/null +++ b/test.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# Check a deployed example serves the demo site and, for the examples with a +# backend, that the backend URL in config.json returns the expected message. +# Usage: ./test.sh s3|cf-s3|lambda|api-gateway|dynamodb +set -euo pipefail + +EXAMPLE=${1:?usage: ./test.sh s3|cf-s3|lambda|api-gateway|dynamodb} +case "$EXAMPLE" in + s3) STACK=S3Stack ;; + cf-s3) STACK=CFS3Stack ;; + lambda) STACK=LambdaStack ;; + api-gateway) STACK=APIGatewayStack ;; + dynamodb) STACK=DynamoDBStack ;; + *) echo "Unknown example $EXAMPLE"; exit 1 ;; +esac + +SITE_MARKER="AWS for Frontend Developers Demo Site" + +output() { + lstk aws cloudformation describe-stacks --stack-name "$STACK" \ + --query "Stacks[0].Outputs[?OutputKey=='$1'].OutputValue" --output text +} + +# Print the body, or fail with the status and body when the status differs +expect_status() { + local expected=$1 body status + shift + body=$(curl -s -m 30 -w '\n%{http_code}' "$@") + status=${body##*$'\n'} + body=${body%$'\n'*} + if [ "$status" != "$expected" ]; then + echo "Test failed: $* returned HTTP $status, expected $expected: $body" >&2 + return 1 + fi + echo "$body" +} + +if [ "$EXAMPLE" = s3 ]; then + bucket=$(lstk aws cloudformation describe-stack-resources --stack-name "$STACK" \ + --query "StackResources[?ResourceType=='AWS::S3::Bucket'].PhysicalResourceId" --output text) + SITE="http://$bucket.s3-website.localhost.localstack.cloud:4566" +else + SITE="http://$(output CloudFrontDistributionURL):4566" +fi +echo "Site: $SITE" + +if [[ "$(expect_status 200 "$SITE/")" != *"$SITE_MARKER"* ]]; then + echo "Test failed: $SITE/ did not serve index.html" + exit 1 +fi +echo "PASS: the site serves index.html" + +if [ "$EXAMPLE" = s3 ]; then + if [[ "$(expect_status 404 "$SITE/missing-page")" != *"Something didn't work"* ]]; then + echo "Test failed: a missing page did not serve error.html" + exit 1 + fi + echo "PASS: a missing page returns 404 with error.html" +fi + +case "$EXAMPLE" in + s3|cf-s3) + echo "Test passed: $EXAMPLE serves the demo site" + exit 0 + ;; + lambda) + URL=$(output LambdaFunctionURL) + CONFIG_KEY=function_url + EXPECTED="You successfully called the Lambda from a function URL!" + ;; + api-gateway) + URL="$(output APIGatewayURL)hello" + CONFIG_KEY=api_url + EXPECTED="You successfully called the Lambda from API Gateway!" + ;; + dynamodb) + URL="$(output APIGatewayURL)hello" + CONFIG_KEY=api_url + EXPECTED="You successfully received data from a DynamoDB table!" + ;; +esac + +config=$(expect_status 200 "$SITE/config.json") +configured=$(echo "$config" | jq -r ".$CONFIG_KEY") +if [ "$configured" = null ] || [[ "$URL" != "$configured"* ]]; then + echo "Test failed: config.json has $CONFIG_KEY '$configured', expected the stack output for $URL" + exit 1 +fi +echo "PASS: config.json points the site at $configured" + +echo "Backend: $URL" +response=$(expect_status 200 "$URL") +if ! echo "$response" | jq -e --arg m "$EXPECTED" '.message == $m' > /dev/null 2>&1; then + echo "Test failed: the backend returned '$response', expected message '$EXPECTED'" + exit 1 +fi +echo "PASS: the backend returned '$EXPECTED'" + +echo "Test passed: $EXAMPLE serves the demo site and its backend responds" From c3b0fb3ba2bd665dfc20d032a5f30bfce2d8ba43 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 13 Oct 2026 11:24:43 +0530 Subject: [PATCH 2/4] Follow the site's URL logic for the backend check LocalStack writes the API Gateway URL into config.json with the region segment and without the port, while the stack output has the port and no region. Check both the stack output URL and the URL the site loads, including the site's own fallback. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- test.sh | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/test.sh b/test.sh index 067f0ea..5386e55 100755 --- a/test.sh +++ b/test.sh @@ -82,18 +82,37 @@ esac config=$(expect_status 200 "$SITE/config.json") configured=$(echo "$config" | jq -r ".$CONFIG_KEY") -if [ "$configured" = null ] || [[ "$URL" != "$configured"* ]]; then - echo "Test failed: config.json has $CONFIG_KEY '$configured', expected the stack output for $URL" +if [ "$configured" = null ] || [ -z "$configured" ]; then + echo "Test failed: config.json has no $CONFIG_KEY: $config" exit 1 fi -echo "PASS: config.json points the site at $configured" +echo "PASS: config.json has $CONFIG_KEY $configured" -echo "Backend: $URL" -response=$(expect_status 200 "$URL") -if ! echo "$response" | jq -e --arg m "$EXPECTED" '.message == $m' > /dev/null 2>&1; then - echo "Test failed: the backend returned '$response', expected message '$EXPECTED'" +# check_backend : the URL returns the expected message +check_backend() { + local response + response=$(expect_status 200 "$1") || return 1 + if ! echo "$response" | jq -e --arg m "$EXPECTED" '.message == $m' > /dev/null 2>&1; then + echo "Test failed: $1 returned '$response', expected message '$EXPECTED'" >&2 + return 1 + fi +} + +check_backend "$URL" +echo "PASS: the stack output URL $URL returned '$EXPECTED'" + +# The site calls the configured URL, and for API Gateway falls back to the URL +# without the region segment (see site_assets/main.js) +site_url=$configured +[ "$CONFIG_KEY" = api_url ] && site_url="${configured}hello" +if check_backend "$site_url"; then + echo "PASS: the URL the site loads from config.json returned the message" +elif [ "$CONFIG_KEY" = api_url ]; then + fallback="$(echo "$configured" | awk -F. 'BEGIN {OFS="."} {$3=""; sub(/\.\./, "."); print}')hello" + check_backend "$fallback" + echo "PASS: the site's fallback URL $fallback returned the message" +else exit 1 fi -echo "PASS: the backend returned '$EXPECTED'" echo "Test passed: $EXAMPLE serves the demo site and its backend responds" From b39917cee34eb4dffed9dfebe13b6bd01b874f55 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 13 Oct 2026 11:27:46 +0530 Subject: [PATCH 3/4] Report the site's URL fallback as a note The first URL the site tries is expected to fail on LocalStack, so do not print it as a test failure. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- test.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test.sh b/test.sh index 5386e55..cc1f487 100755 --- a/test.sh +++ b/test.sh @@ -105,14 +105,15 @@ echo "PASS: the stack output URL $URL returned '$EXPECTED'" # without the region segment (see site_assets/main.js) site_url=$configured [ "$CONFIG_KEY" = api_url ] && site_url="${configured}hello" -if check_backend "$site_url"; then +if check_backend "$site_url" 2> /dev/null; then echo "PASS: the URL the site loads from config.json returned the message" elif [ "$CONFIG_KEY" = api_url ]; then + echo "Note: $site_url did not respond, trying the site's fallback URL" fallback="$(echo "$configured" | awk -F. 'BEGIN {OFS="."} {$3=""; sub(/\.\./, "."); print}')hello" check_backend "$fallback" echo "PASS: the site's fallback URL $fallback returned the message" else - exit 1 + check_backend "$site_url" fi echo "Test passed: $EXAMPLE serves the demo site and its backend responds" From 8b408e27cbd5ec4f91b72a073d207dad62e868b9 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 13 Oct 2026 11:30:13 +0530 Subject: [PATCH 4/4] Document make deploy and make test Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index cd46800..ec129b0 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,17 @@ After the deploy is completed, you will see some outputs in the terminal. For ex * If you get an error during the bootstrap, this can commonly be caused by either LocalStack or Docker not running. Be sure that you are successfully started LocalStack and Docker before bootstrapping. * If your deployment fails to load the assets required to run the site (i.e. into S3 and Lambda), be sure that you have successfully added your auth token. When you start up LocalStack in the terminal, you should see a message reading something like `Successfully activated cached license`. +### Testing an Example + +With LocalStack running, you can deploy and test any example from the repository root: + +```bash +make deploy EXAMPLE=dynamodb +make test EXAMPLE=dynamodb +``` + +`EXAMPLE` is one of `s3`, `cf-s3`, `lambda`, `api-gateway` or `dynamodb`. The test checks the site is served and, for the examples with a backend, that the backend returns the message the site displays. + ### Deploying Remotely to AWS You will need to have the [AWS CLI](https://github.com/aws/aws-cli/tree/v2?tab=readme-ov-file#installation) installed and configured for your account (you will likely need to set up an IAM user and obtain an access key). Finally, you will need to install the [AWS CDK CLI](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html#getting_started_install).