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
40 changes: 40 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Integration Tests

on:
pull_request:
paths-ignore:
- "**/*.md"

jobs:
integration:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- name: Start LocalStack
run: docker compose up -d
- name: Install dependencies
run: |
npm install --legacy-peer-deps
npm install --legacy-peer-deps --prefix fixtures/
- name: Wait for LocalStack
run: |
echo "Waiting for LocalStack to be ready..."
for i in $(seq 1 30); do
HEALTH=$(curl -s http://localhost:4566/_localstack/health)
if echo "$HEALTH" | grep -q '"cloudformation": "available"' && \
echo "$HEALTH" | grep -q '"s3": "available"' && \
echo "$HEALTH" | grep -q '"stepfunctions": "available"'; then
echo "LocalStack is ready"
exit 0
fi
sleep 2
done
echo "LocalStack did not become ready in time"
exit 1
- name: Deploy fixtures
run: npm run test:integration
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ admin.env
.env
tmp
.coveralls.yml
tmpdirs-serverless
tmpdirs-serverless
.serverless
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fixtures/
.worktrees/
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1579,6 +1579,56 @@ stepFunctions:

As a result, `hellostepfunc1` will only have the tag of `score: 42`, and _not_ the tags at the provider level

## Integration Tests

Integration tests deploy real CloudFormation stacks to [LocalStack](https://localstack.cloud) to catch issues that unit tests cannot (e.g. circular resource dependencies, invalid ARN references).

### Prerequisites

- Docker

### Running all fixtures

Install dependencies, start LocalStack, then run the integration test script:

```bash
npm install --legacy-peer-deps
npm install --legacy-peer-deps --prefix fixtures/
docker compose up -d
npm run test:integration
```

### Running a single fixture

Use the `fixture:command` syntax directly with `osls`:

```bash
osls basic-state-machine:deploy --stage test
```

### Adding a fixture

Create a new directory under `fixtures/` containing a `serverless.yml`. It will be picked up automatically by `fixtures/serverless-compose.js`. No `package.json` is needed — all fixtures share `fixtures/package.json`.

Use `fixtures/base.yml` for all shared configuration (`provider`, `plugins`, `package`, `custom`):

```yaml
service: integration-my-fixture

provider: ${file(../base.yml):provider}
plugins: ${file(../base.yml):plugins}
package: ${file(../base.yml):package}
custom: ${file(../base.yml):custom}

stepFunctions:
stateMachines:
...
```

The `package` section in `base.yml` sets `excludeDevDependencies: false`, which is required to prevent OOM during deployment. Serverless Framework v3 defaults this to `true`, causing it to scan all files in `node_modules` (~25k files per fixture) to identify production dependencies before packaging — that analysis exhausts the JS heap.

> **Note:** `serverless-compose.js` at the repo root is intentionally a one-line re-export of `fixtures/serverless-compose.js`. It must exist at the root because `@osls/compose` resolves the config via `lstat`, which does not follow symlinks. Do not delete it.

## Commands

### deploy
Expand Down
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
localstack:
image: localstack/localstack:4
network_mode: host
environment:
SERVICES: cloudformation,stepfunctions,iam,lambda,s3,logs,sns,cloudwatch,sqs,events,scheduler,apigateway
volumes:
- /var/run/docker.sock:/var/run/docker.sock
21 changes: 21 additions & 0 deletions fixtures/activities/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
service: integration-activities

provider: ${file(../base.yml):provider}
plugins: ${file(../base.yml):plugins}
package: ${file(../base.yml):package}
custom: ${file(../base.yml):custom}

stepFunctions:
stateMachines:
activityMachine:
name: integration-activities-${opt:stage, 'test'}
definition:
StartAt: WaitForActivity
States:
WaitForActivity:
Type: Task
Resource:
Ref: MyActivityTaskStepFunctionsActivity
End: true
activities:
- MyActivityTask
21 changes: 21 additions & 0 deletions fixtures/api-gateway/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
service: integration-api-gateway

provider: ${file(../base.yml):provider}
plugins: ${file(../base.yml):plugins}
package: ${file(../base.yml):package}
custom: ${file(../base.yml):custom}

stepFunctions:
stateMachines:
apiMachine:
name: integration-api-gateway-${opt:stage, 'test'}
events:
- http:
path: execute
method: POST
definition:
StartAt: PassThrough
States:
PassThrough:
Type: Pass
End: true
23 changes: 23 additions & 0 deletions fixtures/base.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
provider:
name: aws
runtime: nodejs22.x
region: us-east-1
stage: test

plugins:
- serverless-localstack
- serverless-step-functions

# excludeDevDependencies must be false to prevent Serverless Framework v3 from
# scanning node_modules (~25k files) to identify production dependencies before
# packaging — that analysis exhausts the JS heap. The !node_modules/** pattern
# then excludes node_modules from the deployment package.
package:
excludeDevDependencies: false
patterns:
- '!node_modules/**'

custom:
localstack:
stages:
- test
3 changes: 3 additions & 0 deletions fixtures/basic-state-machine/handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports.hello = async () => ({ statusCode: 200 });
26 changes: 26 additions & 0 deletions fixtures/basic-state-machine/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
service: integration-basic-state-machine

provider: ${file(../base.yml):provider}
plugins: ${file(../base.yml):plugins}
package: ${file(../base.yml):package}
custom: ${file(../base.yml):custom}

functions:
hello:
handler: handler.hello

stepFunctions:
stateMachines:
basicMachine:
name: integration-basic-${opt:stage, 'test'}
tags:
integration: 'true'
fixture: basic-state-machine
definition:
StartAt: InvokeHello
States:
InvokeHello:
Type: Task
Resource:
Fn::GetAtt: [HelloLambdaFunction, Arn]
End: true
3 changes: 3 additions & 0 deletions fixtures/cloudwatch-alarms/handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports.hello = async () => ({ statusCode: 200 });
43 changes: 43 additions & 0 deletions fixtures/cloudwatch-alarms/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
service: integration-cloudwatch-alarms

provider: ${file(../base.yml):provider}
plugins: ${file(../base.yml):plugins}
package: ${file(../base.yml):package}
custom: ${file(../base.yml):custom}

resources:
Resources:
AlarmTopic:
Type: AWS::SNS::Topic

functions:
hello:
handler: handler.hello

stepFunctions:
stateMachines:
alarmMachine:
name: integration-cloudwatch-alarms-${opt:stage, 'test'}
definition:
StartAt: InvokeHello
States:
InvokeHello:
Type: Task
Resource:
Fn::GetAtt: [HelloLambdaFunction, Arn]
End: true
alarms:
topics:
ok:
Ref: AlarmTopic
alarm:
Ref: AlarmTopic
insufficientData:
Ref: AlarmTopic
metrics:
- executionsTimedOut
- executionsFailed
- executionsAborted
- executionThrottled
- executionsSucceeded
treatMissingData: ignore
27 changes: 27 additions & 0 deletions fixtures/cloudwatch-event/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
service: integration-cloudwatch-event

provider: ${file(../base.yml):provider}
plugins: ${file(../base.yml):plugins}
package: ${file(../base.yml):package}
custom: ${file(../base.yml):custom}

stepFunctions:
stateMachines:
eventMachine:
name: integration-cloudwatch-event-${opt:stage, 'test'}
events:
- cloudwatchEvent:
event:
source:
- aws.ec2
detail-type:
- EC2 Instance State-change Notification
detail:
state:
- running
definition:
StartAt: PassThrough
States:
PassThrough:
Type: Pass
End: true
3 changes: 3 additions & 0 deletions fixtures/express-workflow/handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports.hello = async () => ({ statusCode: 200 });
39 changes: 39 additions & 0 deletions fixtures/express-workflow/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
service: integration-express-workflow

provider: ${file(../base.yml):provider}
plugins: ${file(../base.yml):plugins}
package: ${file(../base.yml):package}
custom: ${file(../base.yml):custom}

resources:
Resources:
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /aws/states/integration-express-workflow-${opt:stage, 'test'}
RetentionInDays: 1

functions:
hello:
handler: handler.hello

stepFunctions:
stateMachines:
expressMachine:
name: integration-express-workflow-${opt:stage, 'test'}
type: EXPRESS
loggingConfig:
level: ALL
includeExecutionData: true
destinations:
- Fn::GetAtt: [LogGroup, Arn]
tracingConfig:
enabled: true
definition:
StartAt: InvokeHello
States:
InvokeHello:
Type: Task
Resource:
Fn::GetAtt: [HelloLambdaFunction, Arn]
End: true
20 changes: 20 additions & 0 deletions fixtures/no-output/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
service: integration-no-output

provider: ${file(../base.yml):provider}
plugins: ${file(../base.yml):plugins}
package: ${file(../base.yml):package}
custom: ${file(../base.yml):custom}

stepFunctions:
# noOutput suppresses all CloudFormation Outputs for state machine ARNs.
# This fixture intentionally produces no Stack Outputs.
noOutput: true
stateMachines:
noOutputMachine:
name: integration-no-output-${opt:stage, 'test'}
definition:
StartAt: PassThrough
States:
PassThrough:
Type: Pass
End: true
31 changes: 31 additions & 0 deletions fixtures/notifications/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
service: integration-notifications

provider: ${file(../base.yml):provider}
plugins: ${file(../base.yml):plugins}
package: ${file(../base.yml):package}
custom: ${file(../base.yml):custom}

resources:
Resources:
NotificationTopic:
Type: AWS::SNS::Topic
NotificationQueue:
Type: AWS::SQS::Queue

stepFunctions:
stateMachines:
notificationMachine:
name: integration-notifications-${opt:stage, 'test'}
definition:
StartAt: PassThrough
States:
PassThrough:
Type: Pass
End: true
notifications:
FAILED:
- sns:
Ref: NotificationTopic
SUCCEEDED:
- sqs:
Fn::GetAtt: [NotificationQueue, Arn]
Loading
Loading