From cdb32ac49752efff466e497948c9287a5ae7f825 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 13 Oct 2026 19:11:19 +0530 Subject: [PATCH 1/2] Add CI that runs this repository through a CodePipeline test.sh builds the repository with its own buildspec.yml in a LocalStack CodePipeline and deploys cfn_stack.yaml with the CloudFormation action, then checks the jar and the queue. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- .github/workflows/main.yml | 83 +++++++++++++++++++++++ README.md | 19 ++++++ test.sh | 135 +++++++++++++++++++++++++++++++++++++ 3 files changed, 237 insertions(+) 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..ccaa150 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,83 @@ +name: Run the source through a pipeline + +on: + push: + paths-ignore: + - '**.md' + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + pipeline: + name: Build and deploy through CodePipeline + # The pipeline pulls the branch from this repository, so it cannot test branches in forks + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout + uses: actions/checkout@v4 + + # LocalStack stops tracking a build whose image is still downloading, + # so pull the CodeBuild images (about 17 GB) before starting the pipeline + - name: Pull the CodeBuild images + run: | + # Anonymous ECR Public pulls are rate limited per IP, which runners share + for image in public.ecr.aws/codebuild/amazonlinux-x86_64-standard:5.0 localstack/aws-codebuild-local:2; do + for attempt in 1 2 3 4 5; do + docker pull -q "$image" && break + [ "$attempt" = 5 ] && exit 1 + echo "Pull of $image failed, retrying in 60s" + sleep 60 + done + done + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: 11 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install lstk + run: | + npm install -g @localstack/lstk + lstk setup aws --force --non-interactive + + - name: Start LocalStack + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + # Forwarded as CODEPIPELINE_GH_TOKEN, used to download the source archive from GitHub + LOCALSTACK_CODEPIPELINE_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: lstk start + + - name: Run the pipeline + env: + REPOSITORY: ${{ github.repository }} + BRANCH: ${{ github.head_ref || github.ref_name }} + EXPECTED_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + MAX_WAIT_SECONDS: 1200 + run: ./test.sh + + - name: Show pipeline state and LocalStack logs + if: failure() + run: | + lstk aws codepipeline get-pipeline-state --name source-pipeline || true + lstk aws codepipeline list-action-executions --pipeline-name source-pipeline || true + builds=$(lstk aws codebuild list-builds --query 'ids' --output text || true) + [ -n "$builds" ] && lstk aws codebuild batch-get-builds --ids $builds \ + --query 'builds[].{id: id, status: buildStatus, phases: phases[].[phaseType, phaseStatus, contexts[0].message]}' || true + lstk aws cloudformation describe-stack-events --stack-name source-pipeline-stack || true + docker ps -a + lstk logs | grep -vE 'ListPipelineExecutions|ListActionExecutions' | tail -400 diff --git a/README.md b/README.md index 6197279..46ceec3 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,25 @@ mvn install This compiles `MessageUtil.java`, runs the JUnit tests, and produces `target/messageUtil-1.0.jar`. +## Testing + +`test.sh` runs this repository through a LocalStack CodePipeline: + +1. **Source:** a CodeStarSourceConnection action fetches `$REPOSITORY` at `$BRANCH` (default `localstack-samples/test-codepipeline-source` at `main`). +2. **Build:** a CodeBuild action runs `buildspec.yml`, so the Maven build and the JUnit tests run in CodeBuild. +3. **Deploy:** a CloudFormation action creates `cfn_stack.yaml` with a queue name override. + +It then checks that the build artifact holds `target/messageUtil-1.0.jar` with the compiled `MessageUtil`, and that the stack created the queue and output its URL. It needs `javap` (any JDK) and LocalStack started with a GitHub token for downloading the source: + +```bash +LOCALSTACK_CODEPIPELINE_GH_TOKEN= lstk start +./test.sh +``` + +Pull `public.ecr.aws/codebuild/amazonlinux-x86_64-standard:5.0` before the first run. LocalStack stops tracking a build if its image is still downloading a few seconds after the build starts. + +CI runs the same test on every pull request against the pull request branch. The ECS blue/green deployment (`appspec.yml`, `Dockerfile`) is not covered. + ## License Licensed under the Apache License 2.0. See [LICENSE](LICENSE). diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..1b3d8f0 --- /dev/null +++ b/test.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# Run this repository through a LocalStack CodePipeline, as the source of: +# 1. a CodeBuild action using buildspec.yml (Maven build and JUnit tests) +# 2. a CloudFormation action deploying cfn_stack.yaml +# Then check the build artifact holds the jar and the stack created the queue. +set -euo pipefail + +REPOSITORY=${REPOSITORY:-localstack-samples/test-codepipeline-source} +BRANCH=${BRANCH:-main} +PIPELINE=${PIPELINE:-source-pipeline} +PROJECT=${PROJECT:-message-util-build} +STACK=${STACK:-source-pipeline-stack} +QUEUE=${QUEUE:-source-pipeline-queue} +BUCKET=${BUCKET:-source-pipeline-artifacts} +ROLE_ARN=arn:aws:iam::000000000000:role/source-pipeline-role +MAX_WAIT_SECONDS=${MAX_WAIT_SECONDS:-1800} +WORK_DIR=$(mktemp -d) +trap 'rm -rf "$WORK_DIR"' EXIT + +echo "Source: $REPOSITORY@$BRANCH" + +lstk aws iam create-role --role-name source-pipeline-role --assume-role-policy-document '{ + "Version": "2012-10-17", + "Statement": [{"Effect": "Allow", "Action": "sts:AssumeRole", "Principal": {"Service": [ + "codepipeline.amazonaws.com", "codebuild.amazonaws.com", "cloudformation.amazonaws.com"]}}] +}' > /dev/null +lstk aws iam put-role-policy --role-name source-pipeline-role --policy-name source-pipeline \ + --policy-document '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": [ + "s3:*", "codebuild:*", "cloudformation:*", "sqs:*", "logs:*", "iam:PassRole"], "Resource": "*"}]}' + +CONNECTION_ARN=$(lstk aws codeconnections create-connection --connection-name source-pipeline \ + --provider-type GitHub --query ConnectionArn --output text) + +# No buildspec override, so CodeBuild uses buildspec.yml from the source +lstk aws codebuild create-project --name "$PROJECT" \ + --source type=CODEPIPELINE --artifacts type=CODEPIPELINE \ + --environment type=LINUX_CONTAINER,image=aws/codebuild/standard:5.0,computeType=BUILD_GENERAL1_SMALL \ + --service-role "$ROLE_ARN" > /dev/null + +lstk aws s3 mb "s3://$BUCKET" > /dev/null + +cat > "$WORK_DIR/pipeline.json" < /dev/null + +status="" +waited=0 +until [ "$status" = Succeeded ]; do + if [[ $status =~ ^(Failed|Stopped|Superseded)$ ]]; then + echo "Test failed: pipeline execution ended with status $status" + lstk aws codepipeline get-pipeline-state --name "$PIPELINE" + exit 1 + fi + if (( waited >= MAX_WAIT_SECONDS )); then + echo "Test failed: pipeline did not finish within ${MAX_WAIT_SECONDS}s, last status: $status" + exit 1 + fi + sleep 10 + waited=$((waited + 10)) + status=$(lstk aws codepipeline list-pipeline-executions --pipeline-name "$PIPELINE" \ + --query 'pipelineExecutionSummaries[0].status' --output text) +done +echo "PASS: pipeline execution succeeded" + +source_revision=$(lstk aws codepipeline list-action-executions --pipeline-name "$PIPELINE" \ + --query "actionExecutionDetails[?actionName=='Source'].output.outputVariables.CommitId | [0]" --output text) +if [ -n "${EXPECTED_COMMIT:-}" ] && [ "$source_revision" != "$EXPECTED_COMMIT" ]; then + echo "Test failed: the source action fetched $source_revision, expected $EXPECTED_COMMIT" + exit 1 +fi +echo "PASS: the source action fetched commit $source_revision" + +read -r out_bucket out_key < <(lstk aws codepipeline list-action-executions --pipeline-name "$PIPELINE" \ + --query "actionExecutionDetails[?actionName=='Build'].output.outputArtifacts[0].s3location | [0].[bucket, key]" \ + --output text) +lstk aws s3 cp "s3://$out_bucket/$out_key" "$WORK_DIR/build-output.zip" > /dev/null +unzip -q -o "$WORK_DIR/build-output.zip" -d "$WORK_DIR/build-output" +JAR="$WORK_DIR/build-output/target/messageUtil-1.0.jar" +if [ ! -f "$JAR" ]; then + echo "Test failed: the build artifact has no target/messageUtil-1.0.jar" + unzip -l "$WORK_DIR/build-output.zip" + exit 1 +fi +methods=$(javap -cp "$JAR" MessageUtil) +if [[ "$methods" != *"printMessage()"* || "$methods" != *"salutationMessage()"* ]]; then + echo "Test failed: MessageUtil in the jar is missing its methods: $methods" + exit 1 +fi +echo "PASS: the build artifact holds messageUtil-1.0.jar with the compiled MessageUtil" + +stack_status=$(lstk aws cloudformation describe-stacks --stack-name "$STACK" \ + --query 'Stacks[0].StackStatus' --output text) +queue_output=$(lstk aws cloudformation describe-stacks --stack-name "$STACK" \ + --query "Stacks[0].Outputs[?OutputKey=='QueueURL'].OutputValue | [0]" --output text) +queue_url=$(lstk aws sqs get-queue-url --queue-name "$QUEUE" --query QueueUrl --output text) +if [ "$stack_status" != CREATE_COMPLETE ] || [ "$queue_output" != "$queue_url" ]; then + echo "Test failed: stack $STACK is $stack_status with QueueURL '$queue_output', queue URL is '$queue_url'" + exit 1 +fi +echo "PASS: cfn_stack.yaml created queue $QUEUE and output its URL" + +echo "Test passed: the repository builds and deploys through CodePipeline" From f21772de113ff1f9e37cbdbfa7f2675f33198290 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 13 Oct 2026 19:41:54 +0530 Subject: [PATCH 2/2] Create a V1 pipeline and fail fast if it never starts Without pipelineType, LocalStack created the pipeline as V2, logged that V2 emulation is not available and never started an execution, so the test waited 20 minutes on status None. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- test.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test.sh b/test.sh index 1b3d8f0..18ca8d0 100755 --- a/test.sh +++ b/test.sh @@ -3,6 +3,7 @@ # 1. a CodeBuild action using buildspec.yml (Maven build and JUnit tests) # 2. a CloudFormation action deploying cfn_stack.yaml # Then check the build artifact holds the jar and the stack created the queue. +# LocalStack only runs V1 pipelines, so the pipeline type is set explicitly. set -euo pipefail REPOSITORY=${REPOSITORY:-localstack-samples/test-codepipeline-source} @@ -42,6 +43,7 @@ lstk aws s3 mb "s3://$BUCKET" > /dev/null cat > "$WORK_DIR/pipeline.json" <= 120 )); then + echo "Test failed: no pipeline execution started within 120s" + exit 1 + fi if (( waited >= MAX_WAIT_SECONDS )); then echo "Test failed: pipeline did not finish within ${MAX_WAIT_SECONDS}s, last status: $status" exit 1