Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<github-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).
141 changes: 141 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#!/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.
# LocalStack only runs V1 pipelines, so the pipeline type is set explicitly.
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" <<JSON
{
"name": "$PIPELINE",
"pipelineType": "V1",
"roleArn": "$ROLE_ARN",
"artifactStore": {"type": "S3", "location": "$BUCKET"},
"stages": [
{"name": "Source", "actions": [{
"name": "Source",
"actionTypeId": {"category": "Source", "owner": "AWS", "provider": "CodeStarSourceConnection", "version": "1"},
"outputArtifacts": [{"name": "SourceCode"}],
"configuration": {"ConnectionArn": "$CONNECTION_ARN", "FullRepositoryId": "$REPOSITORY", "BranchName": "$BRANCH"}
}]},
{"name": "Build", "actions": [{
"name": "Build",
"actionTypeId": {"category": "Build", "owner": "AWS", "provider": "CodeBuild", "version": "1"},
"inputArtifacts": [{"name": "SourceCode"}],
"outputArtifacts": [{"name": "BuildOutput"}],
"configuration": {"ProjectName": "$PROJECT"}
}]},
{"name": "Deploy", "actions": [{
"name": "CreateQueue",
"actionTypeId": {"category": "Deploy", "owner": "AWS", "provider": "CloudFormation", "version": "1"},
"roleArn": "$ROLE_ARN",
"inputArtifacts": [{"name": "SourceCode"}],
"configuration": {
"ActionMode": "CREATE_UPDATE",
"StackName": "$STACK",
"TemplatePath": "SourceCode::cfn_stack.yaml",
"RoleArn": "$ROLE_ARN",
"ParameterOverrides": "{\"QueueName\": \"$QUEUE\"}"
}
}]}
]
}
JSON
lstk aws codepipeline create-pipeline --pipeline "file://$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 [ "$status" = None ] && (( waited >= 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
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"
Loading