testdriverai 6.1.4 → 6.1.5-canary.b2eb46e.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,98 @@
1
+ name: AWS
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+
7
+ jobs:
8
+ gather:
9
+ name: Gather Test Files
10
+ runs-on: ubuntu-latest
11
+ outputs:
12
+ test_files: ${{ steps.test_list.outputs.files }}
13
+ steps:
14
+ - name: Check out repository
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Find all test files
18
+ id: test_list
19
+ run: |
20
+ FILES=$(ls ./testdriver/acceptance/*.yaml)
21
+ FILENAMES=$(basename -a $FILES)
22
+ FILES_JSON=$(echo "$FILENAMES" | jq -R -s -c 'split("\n")[:-1]')
23
+ echo "files=$FILES_JSON" >> $GITHUB_OUTPUT
24
+
25
+ test:
26
+ needs: gather
27
+ runs-on: ubuntu-latest
28
+ strategy:
29
+ matrix:
30
+ test: ${{ fromJson(needs.gather.outputs.test_files) }}
31
+ fail-fast: false
32
+ steps:
33
+ - name: Checkout repository
34
+ uses: actions/checkout@v4
35
+ with:
36
+ fetch-depth: 0
37
+ # only needed for `act`
38
+ # - name: Install AWS CLI
39
+ # run: |
40
+ # apt-get update
41
+ # apt-get install curl unzip -y
42
+ # curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
43
+ # unzip awscliv2.zip
44
+ # ./aws/install
45
+ - name: Set up Node.js
46
+ uses: actions/setup-node@v4
47
+ with:
48
+ node-version: "20"
49
+ cache: "npm"
50
+ - name: Install dependencies
51
+ run: NODE_ENV=production npm ci
52
+ - name: Setup AWS Instance
53
+ id: aws-setup
54
+ run: |
55
+ OUTPUT=$(./setup/aws/spawn-runner.sh | tee /dev/stderr) # Capture and display output
56
+ echo "$OUTPUT"
57
+ PUBLIC_IP=$(echo "$OUTPUT" | grep "PUBLIC_IP=" | cut -d'=' -f2)
58
+ INSTANCE_ID=$(echo "$OUTPUT" | grep "INSTANCE_ID=" | cut -d'=' -f2)
59
+ AWS_REGION=$(echo "$OUTPUT" | grep "AWS_REGION=" | cut -d'=' -f2)
60
+ echo "public-ip=$PUBLIC_IP" >> $GITHUB_OUTPUT
61
+ echo "instance-id=$INSTANCE_ID" >> $GITHUB_OUTPUT
62
+ echo "aws-region=$AWS_REGION" >> $GITHUB_OUTPUT
63
+ env:
64
+ FORCE_COLOR: 3
65
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
66
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
67
+ AWS_REGION: us-east-2
68
+ AWS_LAUNCH_TEMPLATE_ID: lt-00d02f31cfc602f27
69
+ AMI_ID: ami-085f872ca0cd80fed
70
+ - name: Run TestDriver
71
+ run: node bin/testdriverai.js run testdriver/acceptance/${{ matrix.test }} --ip="${{ steps.aws-setup.outputs.public-ip }}" --junit=out.xml
72
+ env:
73
+ TD_API_KEY: ${{ secrets.TD_API_KEY }}
74
+ TD_WEBSITE: https://testdriver-sandbox.vercel.app
75
+ TD_THIS_FILE: ${{ matrix.test }}
76
+ - name: Upload TestDriver AI CLI logs
77
+ if: always()
78
+ uses: actions/upload-artifact@v4
79
+ with:
80
+ name: testdriverai-cli-logs-${{ matrix.test }}
81
+ path: /tmp/testdriverai-cli-*.log
82
+ if-no-files-found: warn
83
+ retention-days: 30
84
+ - name: Upload test results as artifact
85
+ if: always()
86
+ uses: actions/upload-artifact@v4
87
+ with:
88
+ name: test-results-${{ matrix.test }}
89
+ path: out.xml
90
+ retention-days: 30
91
+ - name: Shutdown AWS Instance
92
+ if: always()
93
+ run: aws ec2 terminate-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID"
94
+ env:
95
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
96
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
97
+ AWS_REGION: ${{ steps.aws-setup.outputs.aws-region }}
98
+ INSTANCE_ID: ${{ steps.aws-setup.outputs.instance-id }}
package/agent/index.js CHANGED
@@ -74,6 +74,7 @@ class TestDriverAgent extends EventEmitter2 {
74
74
  this.sandboxId = flags["sandbox-id"] || null;
75
75
  this.sandboxAmi = flags["sandbox-ami"] || null;
76
76
  this.sandboxInstance = flags["sandbox-instance"] || null;
77
+ this.ip = flags.ip || null;
77
78
  this.workingDir = flags.workingDir || process.cwd();
78
79
 
79
80
  // Resolve thisFile to absolute path with proper extension
@@ -1709,7 +1710,20 @@ ${regression}
1709
1710
  const recentId = createNew ? null : this.getRecentSandboxId();
1710
1711
 
1711
1712
  // Set sandbox ID for reconnection (only if not creating new and recent ID exists)
1712
- if (!createNew && recentId) {
1713
+ if (this.ip) {
1714
+ let instance = await this.sandbox.send({
1715
+ type: "direct",
1716
+ resolution: this.config.TD_RESOLUTION,
1717
+ ci: this.config.CI,
1718
+ ip: this.ip,
1719
+ });
1720
+
1721
+ await this.renderSandbox(instance.instance, headless);
1722
+ await this.newSession();
1723
+ await this.runLifecycle("provision");
1724
+
1725
+ return;
1726
+ } else if (!createNew && recentId) {
1713
1727
  this.emitter.emit(
1714
1728
  events.log.narration,
1715
1729
  theme.dim(`using recent sandbox: ${recentId}`),
@@ -1720,10 +1734,8 @@ ${regression}
1720
1734
  events.log.narration,
1721
1735
  theme.dim(`no recent sandbox found, creating a new one.`),
1722
1736
  );
1723
- }
1724
-
1725
- // Only attempt to connect to existing sandbox if not in CI mode and not creating new
1726
- if (this.sandboxId && !this.config.CI && !createNew) {
1737
+ } else if (this.sandboxId && !this.config.CI) {
1738
+ // Only attempt to connect to existing sandbox if not in CI mode and not creating new
1727
1739
  // Attempt to connect to known instance
1728
1740
  this.emitter.emit(
1729
1741
  events.log.narration,
@@ -55,6 +55,10 @@ function createCommandDefinitions(agent) {
55
55
  "sandbox-instance": Flags.string({
56
56
  description: "Specify EC2 instance type for sandbox (e.g., i3.metal)",
57
57
  }),
58
+ ip: Flags.string({
59
+ description:
60
+ "Connect directly to a sandbox at the specified IP address",
61
+ }),
58
62
  summary: Flags.string({
59
63
  description: "Specify output file for summarize results",
60
64
  }),
@@ -129,6 +133,10 @@ function createCommandDefinitions(agent) {
129
133
  "sandbox-instance": Flags.string({
130
134
  description: "Specify EC2 instance type for sandbox (e.g., i3.metal)",
131
135
  }),
136
+ ip: Flags.string({
137
+ description:
138
+ "Connect directly to a sandbox at the specified IP address",
139
+ }),
132
140
  summary: Flags.string({
133
141
  description: "Specify output file for summarize results",
134
142
  }),
package/docs/docs.json CHANGED
@@ -50,6 +50,7 @@
50
50
  }
51
51
  ]
52
52
  },
53
+ "/getting-started/self-hosting",
53
54
  "/getting-started/playwright",
54
55
  "/getting-started/vscode"
55
56
  ]
@@ -0,0 +1,303 @@
1
+ ---
2
+ title: "Self-Hosting TestDriver"
3
+ sidebarTitle: "Self-Hosting"
4
+ description: "Complete guide to self-hosting TestDriver instances on AWS"
5
+ icon: "server"
6
+ ---
7
+
8
+ ```mermaid
9
+ graph LR
10
+ A[CLI] <--> B[api.testdriver.ai]
11
+ B <--> C[Your AWS EC2 Instance]
12
+ ```
13
+
14
+ Self-hosting TestDriver allows you to run tests on your own infrastructure, giving you full control over the environment, security, and configurations. This guide walks you through setting up and managing self-hosted TestDriver instances using AWS.
15
+
16
+ ## Why self host?
17
+
18
+ - **Enhanced security**: Get complete control over ingress and egress rules.
19
+ - **Complete customization**: Modify the TestDriver Golden Image to include custom dependencies, software, and configurations at launch time.
20
+ - **Powerful Infrastructure**: Run tests on bare metal infrastructure that support emulators and simulators.
21
+
22
+ ## Quick Start (TL;DR)
23
+
24
+ 1. **Copy the workflow file**: Use [`.github/workflows/self-hosted.yml`](https://github.com/testdriverai/cli/tree/main/.github/workflows/self-hosted.yml) as your template
25
+ 2. **Run CloudFormation**: Deploy our [`setup/aws/cloudformation.yaml`](https://github.com/testdriverai/cli/tree/main/setup/aws/cloudformation.yaml) to provision infrastructure
26
+ 3. **Setup instances**: Use [`setup/aws/spawn-runner.sh`](https://github.com/testdriverai/cli/tree/main/setup/aws/spawn-runner.sh) with your launch template ID
27
+ 4. **Configure GitHub Actions**: Add AWS credentials to your repository secrets
28
+
29
+ ## Overview
30
+
31
+ Self-hosting TestDriver gives you complete control over your test execution environment. You'll provision EC2 instances on AWS using our pre-configured AMI and infrastructure templates.
32
+
33
+ ## Prerequisites
34
+
35
+ - AWS account with appropriate permissions
36
+ - AWS CLI installed locally
37
+ - Access to TestDriver's shared AMI. [Contact us for access](https://form.typeform.com/to/UECf9rDx?typeform-source=testdriver.ai).
38
+ - GitHub repository for your tests
39
+
40
+ ## Step 1: Set Up AWS Infrastructure
41
+
42
+ ### Deploy CloudFormation Stack
43
+
44
+ Our [`setup/aws/cloudformation.yaml`](https://github.com/testdriverai/cli/tree/main/setup/aws/cloudformation.yaml) template creates:
45
+
46
+ - Dedicated VPC with public subnet
47
+ - Security group with proper port access
48
+ - IAM roles and instance profiles
49
+ - EC2 launch template for programmatic instance creation
50
+
51
+ This is a one-time setup used to generate a template ID for launching instances.
52
+
53
+ ```bash
54
+ # Deploy the CloudFormation stack
55
+ aws cloudformation deploy \
56
+ --template-file setup/aws/cloudformation.yaml \
57
+ --stack-name testdriver-infrastructure-11 \
58
+ --parameter-overrides \
59
+ ProjectTag=testdriver \
60
+ AllowedIngressCidr=0.0.0.0/0 \
61
+ InstanceType=c5.xlarge \
62
+ CreateKeyPair=true \
63
+ --capabilities CAPABILITY_IAM
64
+ ```
65
+
66
+ <Danger>
67
+ **Security**: Replace `AllowedIngressCidr=0.0.0.0/0` with your specific IP ranges to lock down access to your VPC.
68
+ </Danger>
69
+
70
+ ### Get Launch Template ID
71
+
72
+ After CloudFormation completes, find the launch template ID in the stack outputs:
73
+
74
+ ```bash
75
+ aws cloudformation describe-stacks \
76
+ --stack-name testdriver-infrastructure-11 \
77
+ --query 'Stacks[0].Outputs[?OutputKey==`LaunchTemplateId`].OutputValue' \
78
+ --output text
79
+ ```
80
+
81
+ Save this ID - you'll need it for the next step.
82
+
83
+ ## Step 2: Spawn a New TestDriver Runner
84
+
85
+ ### Using aws-setup.sh
86
+
87
+ Our [`setup/aws/spawn-runner.sh`](https://github.com/testdriverai/cli/tree/main/setup/aws/spawn-runner.sh) spawns and initializes instances:
88
+
89
+ - Launches instances using your launch template
90
+ - Completes TestDriver handshake
91
+ - Returns instance details for CLI usage
92
+
93
+ ```bash
94
+ # Launch an instance
95
+ export AWS_REGION=us-east-2
96
+ export AMI_ID=ami-085f872ca0cd80fed # Your TestDriver AMI (contact us to get one)
97
+ export AWS_LAUNCH_TEMPLATE_ID=lt-00d02f31cfc602f27 # From CloudFormation output from step 1
98
+
99
+ ./aws-setup.sh
100
+ ```
101
+
102
+ The script outputs:
103
+
104
+ ```
105
+ PUBLIC_IP=1.2.3.4
106
+ INSTANCE_ID=i-1234567890abcdef0
107
+ AWS_REGION=us-east-2
108
+ ```
109
+
110
+ ### CLI Usage
111
+
112
+ Once you have an instance IP, run tests directly:
113
+
114
+ ```bash
115
+ # Basic test execution
116
+ npx testdriverai run test.yaml --ip=1.2.3.4
117
+ ```
118
+
119
+ You can use the `PUBLIC_IP` to target the instance you just spawned via `./setup/aws/spawn-runner.sh`:
120
+
121
+ ```sh
122
+ npx testdriverai@latest run testdriver/your-test.yaml \
123
+ --ip="$PUBLIC_IP" \
124
+ ```
125
+
126
+ Note that the instance will remain running until you terminate it. You can do this manually via the AWS console, or programmatically in your CI workflow:
127
+
128
+ ```bash
129
+ aws ec2 terminate-instances \
130
+ --region us-east-2 \
131
+ --instance-ids $INSTANCE_ID
132
+ ```
133
+
134
+ ## Step 3: GitHub Actions Integration
135
+
136
+ ### Example Workflow
137
+
138
+ Our [`.github/workflows/self-hosted.yml`](https://github.com/testdriverai/cli/tree/main/.github/workflows/self-hosted.yml) demonstrates the complete workflow:
139
+
140
+ ```yaml
141
+ name: TestDriver Self-Hosted
142
+
143
+ on:
144
+ workflow_dispatch:
145
+ push:
146
+
147
+ jobs:
148
+ test:
149
+ runs-on: ubuntu-latest
150
+ steps:
151
+ - name: Checkout repository
152
+ uses: actions/checkout@v4
153
+
154
+ - name: Setup AWS Instance
155
+ id: aws-setup
156
+ run: |
157
+ OUTPUT=$(./setup/aws/spawn-runner.sh | tee /dev/stderr)
158
+ PUBLIC_IP=$(echo "$OUTPUT" | grep "PUBLIC_IP=" | cut -d'=' -f2)
159
+ INSTANCE_ID=$(echo "$OUTPUT" | grep "INSTANCE_ID=" | cut -d'=' -f2)
160
+ echo "public-ip=$PUBLIC_IP" >> $GITHUB_OUTPUT
161
+ echo "instance-id=$INSTANCE_ID" >> $GITHUB_OUTPUT
162
+ env:
163
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
164
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
165
+ AWS_REGION: us-east-2
166
+ AWS_LAUNCH_TEMPLATE_ID: ${{ secrets.AWS_LAUNCH_TEMPLATE_ID }}
167
+ AMI_ID: ${{ secrets.AMI_ID }}
168
+
169
+ - name: Run TestDriver
170
+ run: |
171
+ npx testdriverai run your-test.yaml \
172
+ --ip="${{ steps.aws-setup.outputs.public-ip }}"
173
+ env:
174
+ TD_API_KEY: ${{ secrets.TD_API_KEY }}
175
+
176
+ - name: Shutdown AWS Instance
177
+ if: always()
178
+ run: |
179
+ aws ec2 terminate-instances \
180
+ --region us-east-2 \
181
+ --instance-ids ${{ steps.aws-setup.outputs.instance-id }}
182
+ env:
183
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
184
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
185
+ ```
186
+
187
+ ### Required Secrets
188
+
189
+ Configure these secrets in your GitHub repository:
190
+
191
+ | Secret | Description | Example |
192
+ | ------------------------ | ----------------------------------- | ------------------------------------------------------------ |
193
+ | `AWS_ACCESS_KEY_ID` | AWS access key | `AKIAIOSFODNN7EXAMPLE` |
194
+ | `AWS_SECRET_ACCESS_KEY` | AWS secret key | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` |
195
+ | `AWS_LAUNCH_TEMPLATE_ID` | Launch template from CloudFormation | `lt-07c53ce8349b958d1` |
196
+ | `AMI_ID` | TestDriver AMI ID | `ami-085f872ca0cd80fed` |
197
+ | `TD_API_KEY` | TestDriver API key | Your API key from [the dashboard](https://app.testdriver.ai) |
198
+
199
+ ## AMI Customization
200
+
201
+ ### Using the Base AMI
202
+
203
+ Our AMI comes pre-configured with:
204
+
205
+ - Windows Server with desktop environment
206
+ - Required TestDriver dependencies
207
+ - Optimized settings for test execution
208
+
209
+ ### Modifying the AMI
210
+
211
+ You can customize the AMI for your specific needs:
212
+
213
+ 1. **Launch an instance** from our base AMI
214
+ 2. **Make your changes** (install software, configure settings)
215
+ 3. **Create a new AMI** from your modified instance
216
+ 4. **Update your workflow** to use the new AMI ID
217
+
218
+ ### Amazon Image Builder
219
+
220
+ For automated AMI builds, use [Amazon EC2 Image Builder](https://aws.amazon.com/image-builder/):
221
+
222
+ ```yaml
223
+ # Example Image Builder pipeline
224
+ Components:
225
+ - Name: testdriver-base
226
+ Version: 1.0.0
227
+ Platform: Windows
228
+ Type: BUILD
229
+ Data: |
230
+ name: TestDriver Custom Setup
231
+ description: Custom TestDriver AMI with additional software
232
+ schemaVersion: 1.0
233
+ phases:
234
+ - name: build
235
+ steps:
236
+ - name: InstallSoftware
237
+ action: ExecutePowerShell
238
+ inputs:
239
+ commands:
240
+ - "# Your custom installation commands here"
241
+ ```
242
+
243
+ ## Security Considerations
244
+
245
+ ### Network Security
246
+
247
+ 1. **Restrict CIDR blocks**: Only allow access from your known IP ranges
248
+ 2. **Use VPC endpoints**: For private communication with AWS services
249
+ 3. **Enable VPC Flow Logs**: For network monitoring and debugging
250
+
251
+ ### AWS Authentication
252
+
253
+ Use [OIDC for GitHub Actions](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) instead of long-term credentials:
254
+
255
+ ```yaml
256
+ permissions:
257
+ id-token: write
258
+ contents: read
259
+
260
+ steps:
261
+ - name: Configure AWS credentials
262
+ uses: aws-actions/configure-aws-credentials@v4
263
+ with:
264
+ role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
265
+ aws-region: us-east-2
266
+ ```
267
+
268
+ ### Instance Security
269
+
270
+ - **Terminate instances** immediately after use
271
+ - **Monitor costs** with AWS billing alerts
272
+ - **Use least-privilege IAM roles** for instance profiles
273
+ - **Enable CloudTrail** for audit logging
274
+
275
+ ## Troubleshooting
276
+
277
+ ### Common Issues
278
+
279
+ **Instance not responding:**
280
+
281
+ - Check security group rules allow necessary ports
282
+ - Verify instance has passed all status checks
283
+ - Ensure AMI is compatible with selected instance type
284
+
285
+ **Connection timeouts:**
286
+
287
+ - Verify network connectivity from runner to instance
288
+ - Check VPC routing and internet gateway configuration
289
+ - Confirm instance is in correct subnet
290
+
291
+ **AWS CLI errors:**
292
+
293
+ - Validate AWS credentials and permissions
294
+ - Check AWS service quotas and limits
295
+ - Verify region consistency across all resources
296
+
297
+ ### Getting Help
298
+
299
+ For enterprise customers:
300
+
301
+ - Contact your account manager for AMI access issues
302
+ - Use support channels for infrastructure questions
303
+ - Check the TestDriver documentation for CLI usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "6.1.4",
3
+ "version": "6.1.5-canary.b2eb46e.0",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,463 @@
1
+ AWSTemplateFormatVersion: "2010-09-09"
2
+ Description: >-
3
+ Baseline artifacts (NO EC2 instance): Creates a dedicated VPC with public subnet, Security Group,
4
+ IAM Role/Profile, optional KeyPair, and an EC2 Launch Template so you can spawn many instances
5
+ programmatically. Writes handy IDs into SSM Parameter Store for easy lookup in scripts or automation.
6
+
7
+ Metadata:
8
+ AWS::CloudFormation::Interface:
9
+ ParameterGroups:
10
+ - Label:
11
+ default: "Notification Configuration"
12
+ Parameters:
13
+ - NotificationEmail
14
+ - Label:
15
+ default: "Project Configuration"
16
+ Parameters:
17
+ - ProjectTag
18
+ - Label:
19
+ default: "Network Configuration"
20
+ Parameters:
21
+ - AllowedIngressCidr
22
+ - Label:
23
+ default: "Instance Configuration"
24
+ Parameters:
25
+ - InstanceType
26
+ - Label:
27
+ default: "Key Pair Configuration"
28
+ Parameters:
29
+ - CreateKeyPair
30
+ - ExistingKeyName
31
+ ParameterLabels:
32
+ ProjectTag:
33
+ default: "Project Tag"
34
+ InstanceType:
35
+ default: "Instance Type"
36
+ AllowedIngressCidr:
37
+ default: "Allowed Ingress CIDR"
38
+ CreateKeyPair:
39
+ default: "Create New Key Pair"
40
+ ExistingKeyName:
41
+ default: "Existing Key Name (only required if 'Create New Key Pair' is 'no')"
42
+ NotificationEmail:
43
+ default: "Notification Email (optional)"
44
+
45
+ Rules:
46
+ ValidateKeyPairConfiguration:
47
+ RuleCondition: !Equals [!Ref CreateKeyPair, "no"]
48
+ Assertions:
49
+ - Assert: !Not [!Equals [!Ref ExistingKeyName, ""]]
50
+ AssertDescription: "ExistingKeyName must be provided when CreateKeyPair is 'no'"
51
+
52
+ Parameters:
53
+ NotificationEmail:
54
+ Type: String
55
+ Default: ""
56
+ Description: Email address to receive deployment completion notifications (optional)
57
+
58
+ ProjectTag:
59
+ Type: String
60
+ Default: testdriver
61
+
62
+ InstanceType:
63
+ Type: String
64
+ Default: c5.xlarge
65
+ AllowedValues:
66
+ - c5.xlarge
67
+ - c5.2xlarge
68
+ - c5.4xlarge
69
+ - c5.9xlarge
70
+ - c5.12xlarge
71
+ - c5.18xlarge
72
+ - c5.24xlarge
73
+ - c5.metal
74
+ - i3.metal
75
+ Description: Instance type - only c5.xlarge or larger, plus c5.metal and i3.metal allowed
76
+
77
+ AllowedIngressCidr:
78
+ Type: String
79
+ Default: 0.0.0.0/0
80
+ Description: CIDR allowed to access inbound ports (0.0.0.0/0 means "anyone", we recommend tightening this in production).
81
+
82
+ CreateKeyPair:
83
+ Type: String
84
+ AllowedValues: [yes, no]
85
+ Default: yes
86
+ Description: Create a new key pair for instance access? (If 'no', you must provide an existing key name)
87
+ ExistingKeyName:
88
+ Type: String
89
+ Default: ""
90
+ Description: Name of existing EC2 Key Pair (only required when CreateKeyPair is 'no')
91
+
92
+ Conditions:
93
+ UseExistingKeyProvided: !Not [!Equals [!Ref ExistingKeyName, ""]]
94
+ CreateKey: !Equals [!Ref CreateKeyPair, "yes"]
95
+ SendNotification: !Not [!Equals [!Ref NotificationEmail, ""]]
96
+
97
+ Resources:
98
+ # VPC for TestDriver
99
+ TestDriverVpc:
100
+ Type: AWS::EC2::VPC
101
+ Properties:
102
+ CidrBlock: 10.0.0.0/16
103
+ EnableDnsHostnames: true
104
+ EnableDnsSupport: true
105
+ Tags:
106
+ - { Key: Name, Value: !Sub "${AWS::StackName}-vpc" }
107
+ - { Key: Project, Value: !Ref ProjectTag }
108
+
109
+ # Public subnet for EC2 instances
110
+ PublicSubnet:
111
+ Type: AWS::EC2::Subnet
112
+ Properties:
113
+ VpcId: !Ref TestDriverVpc
114
+ CidrBlock: 10.0.1.0/24
115
+ AvailabilityZone: !Select [0, !GetAZs ""]
116
+ MapPublicIpOnLaunch: true
117
+ Tags:
118
+ - { Key: Name, Value: !Sub "${AWS::StackName}-public-subnet" }
119
+ - { Key: Project, Value: !Ref ProjectTag }
120
+
121
+ # Internet Gateway
122
+ InternetGateway:
123
+ Type: AWS::EC2::InternetGateway
124
+ Properties:
125
+ Tags:
126
+ - { Key: Name, Value: !Sub "${AWS::StackName}-igw" }
127
+ - { Key: Project, Value: !Ref ProjectTag }
128
+
129
+ # Attach Internet Gateway to VPC
130
+ AttachGateway:
131
+ Type: AWS::EC2::VPCGatewayAttachment
132
+ Properties:
133
+ VpcId: !Ref TestDriverVpc
134
+ InternetGatewayId: !Ref InternetGateway
135
+
136
+ # Route table for public subnet
137
+ PublicRouteTable:
138
+ Type: AWS::EC2::RouteTable
139
+ Properties:
140
+ VpcId: !Ref TestDriverVpc
141
+ Tags:
142
+ - { Key: Name, Value: !Sub "${AWS::StackName}-public-rt" }
143
+ - { Key: Project, Value: !Ref ProjectTag }
144
+
145
+ # Route to Internet Gateway
146
+ PublicRoute:
147
+ Type: AWS::EC2::Route
148
+ DependsOn: AttachGateway
149
+ Properties:
150
+ RouteTableId: !Ref PublicRouteTable
151
+ DestinationCidrBlock: 0.0.0.0/0
152
+ GatewayId: !Ref InternetGateway
153
+
154
+ # Associate route table with public subnet
155
+ SubnetRouteTableAssociation:
156
+ Type: AWS::EC2::SubnetRouteTableAssociation
157
+ Properties:
158
+ SubnetId: !Ref PublicSubnet
159
+ RouteTableId: !Ref PublicRouteTable
160
+
161
+ SecurityGroup:
162
+ Type: AWS::EC2::SecurityGroup
163
+ Properties:
164
+ GroupDescription: SG for QA desktop testing (RDP/HTTPS/NGINX/pyautogui + VNC)
165
+ VpcId: !Ref TestDriverVpc
166
+ SecurityGroupIngress:
167
+ - {
168
+ IpProtocol: tcp,
169
+ FromPort: 8765,
170
+ ToPort: 8765,
171
+ CidrIp: !Ref AllowedIngressCidr,
172
+ Description: "pyautogui-cli WebSockets",
173
+ }
174
+ - {
175
+ IpProtocol: tcp,
176
+ FromPort: 8443,
177
+ ToPort: 8443,
178
+ CidrIp: !Ref AllowedIngressCidr,
179
+ Description: "Custom 8443",
180
+ }
181
+ - {
182
+ IpProtocol: tcp,
183
+ FromPort: 8080,
184
+ ToPort: 8080,
185
+ CidrIp: !Ref AllowedIngressCidr,
186
+ Description: "NGINX 8080",
187
+ }
188
+ - {
189
+ IpProtocol: tcp,
190
+ FromPort: 80,
191
+ ToPort: 80,
192
+ CidrIp: !Ref AllowedIngressCidr,
193
+ Description: "HTTP 80",
194
+ }
195
+ - {
196
+ IpProtocol: tcp,
197
+ FromPort: 443,
198
+ ToPort: 443,
199
+ CidrIp: !Ref AllowedIngressCidr,
200
+ Description: "HTTPS 443",
201
+ }
202
+ - {
203
+ IpProtocol: tcp,
204
+ FromPort: 3389,
205
+ ToPort: 3389,
206
+ CidrIp: !Ref AllowedIngressCidr,
207
+ Description: "RDP 3389",
208
+ }
209
+ - {
210
+ IpProtocol: tcp,
211
+ FromPort: 5900,
212
+ ToPort: 5900,
213
+ CidrIp: !Ref AllowedIngressCidr,
214
+ Description: "TightVNC 5900",
215
+ }
216
+ - {
217
+ IpProtocol: tcp,
218
+ FromPort: 5901,
219
+ ToPort: 5901,
220
+ CidrIp: !Ref AllowedIngressCidr,
221
+ Description: "noVNC Websockify 5901",
222
+ }
223
+ - {
224
+ IpProtocol: tcp,
225
+ FromPort: 6080,
226
+ ToPort: 6080,
227
+ CidrIp: !Ref AllowedIngressCidr,
228
+ Description: "noVNC HTTP 6080",
229
+ }
230
+ SecurityGroupEgress:
231
+ - IpProtocol: -1
232
+ CidrIp: 0.0.0.0/0
233
+ Description: Allow all outbound
234
+ Tags:
235
+ - { Key: Project, Value: !Ref ProjectTag }
236
+
237
+ InstanceRole:
238
+ Type: AWS::IAM::Role
239
+ Properties:
240
+ AssumeRolePolicyDocument:
241
+ Version: "2012-10-17"
242
+ Statement:
243
+ - Effect: Allow
244
+ Principal: { Service: ec2.amazonaws.com }
245
+ Action: sts:AssumeRole
246
+ ManagedPolicyArns:
247
+ - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
248
+ Tags:
249
+ - { Key: Project, Value: !Ref ProjectTag }
250
+
251
+ InstanceProfile:
252
+ Type: AWS::IAM::InstanceProfile
253
+ Properties:
254
+ Roles: [!Ref InstanceRole]
255
+
256
+ KeyPair:
257
+ Type: AWS::EC2::KeyPair
258
+ Condition: CreateKey
259
+ Properties:
260
+ KeyName: !Sub "${AWS::StackName}-key"
261
+ KeyType: rsa
262
+
263
+ LaunchTemplate:
264
+ Type: AWS::EC2::LaunchTemplate
265
+ Properties:
266
+ LaunchTemplateName: !Sub "${AWS::StackName}-lt"
267
+ LaunchTemplateData:
268
+ InstanceType: !Ref InstanceType
269
+ IamInstanceProfile:
270
+ Name: !Ref InstanceProfile
271
+ # Lock SG + Subnet to the same VPC
272
+ NetworkInterfaces:
273
+ - DeviceIndex: 0
274
+ SubnetId: !Ref PublicSubnet
275
+ Groups: [!Ref SecurityGroup]
276
+ AssociatePublicIpAddress: true
277
+ KeyName: !If
278
+ - CreateKey
279
+ - !Ref KeyPair
280
+ - !If
281
+ - UseExistingKeyProvided
282
+ - !Ref ExistingKeyName
283
+ - !Ref AWS::NoValue
284
+ TagSpecifications:
285
+ - ResourceType: instance
286
+ Tags: [{ Key: Project, Value: !Ref ProjectTag }]
287
+ - ResourceType: volume
288
+ Tags: [{ Key: Project, Value: !Ref ProjectTag }]
289
+
290
+ # SNS Topic for deployment notifications
291
+ DeploymentNotificationTopic:
292
+ Type: AWS::SNS::Topic
293
+ Condition: SendNotification
294
+ Properties:
295
+ TopicName: !Sub "${AWS::StackName}-deployment-notifications"
296
+ DisplayName: !Sub "${AWS::StackName} Deployment Notifications"
297
+ Tags:
298
+ - { Key: Project, Value: !Ref ProjectTag }
299
+
300
+ # SNS Subscription for email notifications
301
+ EmailSubscription:
302
+ Type: AWS::SNS::Subscription
303
+ Condition: SendNotification
304
+ Properties:
305
+ Protocol: email
306
+ TopicArn: !Ref DeploymentNotificationTopic
307
+ Endpoint: !Ref NotificationEmail
308
+
309
+ # Custom resource to send completion notification
310
+ DeploymentCompleteNotification:
311
+ Type: AWS::CloudFormation::CustomResource
312
+ Condition: SendNotification
313
+ Properties:
314
+ ServiceToken: !GetAtt NotificationLambda.Arn
315
+ StackName: !Ref AWS::StackName
316
+ TopicArn: !Ref DeploymentNotificationTopic
317
+ DependsOn:
318
+ - SsmParamSg
319
+ - SsmParamIp
320
+ - SsmParamLt
321
+ - SsmParamLtLatest
322
+ - SsmParamVpc
323
+ - SsmParamSubnet
324
+
325
+ # Lambda function to send the notification
326
+ NotificationLambda:
327
+ Type: AWS::Lambda::Function
328
+ Condition: SendNotification
329
+ Properties:
330
+ FunctionName: !Sub "${AWS::StackName}-deployment-notifier"
331
+ Runtime: python3.11
332
+ Handler: index.lambda_handler
333
+ Role: !GetAtt NotificationLambdaRole.Arn
334
+ Code:
335
+ ZipFile: |
336
+ import boto3
337
+ import json
338
+ import cfnresponse
339
+
340
+ def lambda_handler(event, context):
341
+ try:
342
+ if event['RequestType'] == 'Create':
343
+ sns = boto3.client('sns')
344
+ stack_name = event['ResourceProperties']['StackName']
345
+ topic_arn = event['ResourceProperties']['TopicArn']
346
+
347
+ message = f"""
348
+ TestDriver Infrastructure Deployment Complete!
349
+
350
+ Stack Name: {stack_name}
351
+ Status: CREATE_COMPLETE
352
+
353
+ Your TestDriver infrastructure is now ready to use.
354
+ Check the CloudFormation outputs for resource IDs and configuration details.
355
+ """
356
+
357
+ sns.publish(
358
+ TopicArn=topic_arn,
359
+ Subject=f'TestDriver Stack {stack_name} - Deployment Complete',
360
+ Message=message
361
+ )
362
+
363
+ cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
364
+ except Exception as e:
365
+ print(f"Error: {e}")
366
+ cfnresponse.send(event, context, cfnresponse.FAILED, {})
367
+ Tags:
368
+ - { Key: Project, Value: !Ref ProjectTag }
369
+
370
+ # IAM Role for the notification Lambda
371
+ NotificationLambdaRole:
372
+ Type: AWS::IAM::Role
373
+ Condition: SendNotification
374
+ Properties:
375
+ AssumeRolePolicyDocument:
376
+ Version: "2012-10-17"
377
+ Statement:
378
+ - Effect: Allow
379
+ Principal:
380
+ Service: lambda.amazonaws.com
381
+ Action: sts:AssumeRole
382
+ ManagedPolicyArns:
383
+ - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
384
+ Policies:
385
+ - PolicyName: SNSPublishPolicy
386
+ PolicyDocument:
387
+ Version: "2012-10-17"
388
+ Statement:
389
+ - Effect: Allow
390
+ Action:
391
+ - sns:Publish
392
+ Resource: !Ref DeploymentNotificationTopic
393
+ Tags:
394
+ - { Key: Project, Value: !Ref ProjectTag }
395
+
396
+ SsmParamSg:
397
+ Type: AWS::SSM::Parameter
398
+ Properties:
399
+ Name: !Sub "/testdriver/infra/${AWS::StackName}/security-group-id"
400
+ Type: String
401
+ Value: !Ref SecurityGroup
402
+
403
+ SsmParamIp:
404
+ Type: AWS::SSM::Parameter
405
+ Properties:
406
+ Name: !Sub "/testdriver/infra/${AWS::StackName}/instance-profile-name"
407
+ Type: String
408
+ Value: !Ref InstanceProfile
409
+
410
+ SsmParamLt:
411
+ Type: AWS::SSM::Parameter
412
+ Properties:
413
+ Name: !Sub "/testdriver/infra/${AWS::StackName}/launch-template-id"
414
+ Type: String
415
+ Value: !Ref LaunchTemplate
416
+
417
+ SsmParamLtLatest:
418
+ Type: AWS::SSM::Parameter
419
+ Properties:
420
+ Name: !Sub "/testdriver/infra/${AWS::StackName}/launch-template-latest-version"
421
+ Type: String
422
+ Value: !GetAtt LaunchTemplate.LatestVersionNumber
423
+
424
+ SsmParamVpc:
425
+ Type: AWS::SSM::Parameter
426
+ Properties:
427
+ Name: !Sub "/testdriver/infra/${AWS::StackName}/testdriver-vpc-id"
428
+ Type: String
429
+ Value: !Ref TestDriverVpc
430
+
431
+ SsmParamSubnet:
432
+ Type: AWS::SSM::Parameter
433
+ Properties:
434
+ Name: !Sub "/testdriver/infra/${AWS::StackName}/testdriver-public-subnet-id"
435
+ Type: String
436
+ Value: !Ref PublicSubnet
437
+
438
+ Outputs:
439
+ VpcId:
440
+ Value: !Ref TestDriverVpc
441
+ Description: VPC ID created for TestDriver
442
+ SubnetId:
443
+ Value: !Ref PublicSubnet
444
+ Description: Public subnet ID for TestDriver instances
445
+ SecurityGroupId:
446
+ Value: !Ref SecurityGroup
447
+ Description: Security Group for QA desktop testing
448
+ InstanceProfileName:
449
+ Value: !Ref InstanceProfile
450
+ Description: Instance Profile to attach to instances
451
+ LaunchTemplateId:
452
+ Value: !Ref LaunchTemplate
453
+ Description: EC2 Launch Template ID
454
+ LaunchTemplateLatestVersion:
455
+ Value: !GetAtt LaunchTemplate.LatestVersionNumber
456
+ Description: Latest Launch Template version
457
+ KeyPairSsmParam:
458
+ Condition: CreateKey
459
+ Value: !Sub "/ec2/keypair/${KeyPair.KeyPairId}"
460
+ Description: SSM parameter that stores the generated private key
461
+ SsmNamespaceUsed:
462
+ Value: !Sub "/testdriver/infra/${AWS::StackName}"
463
+ Description: Prefix where IDs are stored for discovery
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # --- Config (reads from env) ---
5
+ : "${AWS_REGION:?Set AWS_REGION}"
6
+ : "${AMI_ID:?Set AMI_ID (TestDriver Ami)}"
7
+ : "${AWS_LAUNCH_TEMPLATE_ID:?Set AWS_LAUNCH_TEMPLATE_ID}"
8
+ : "${AWS_LAUNCH_TEMPLATE_VERSION:=\$Latest}"
9
+ : "${AWS_TAG_PREFIX:=td}"
10
+ : "${RUNNER_CLASS_ID:=default}"
11
+
12
+ TAG_NAME="${AWS_TAG_PREFIX}-"$(date +%s)
13
+ WS_CONFIG_PATH='C:\Windows\Temp\pyautogui-ws.json'
14
+
15
+ echo "Launching AWS Instance..."
16
+
17
+ # --- 1) Launch instance ---
18
+ RUN_JSON=$(aws ec2 run-instances \
19
+ --region "$AWS_REGION" \
20
+ --image-id "$AMI_ID" \
21
+ --launch-template "LaunchTemplateId=$AWS_LAUNCH_TEMPLATE_ID,Version=$AWS_LAUNCH_TEMPLATE_VERSION" \
22
+ --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=${TAG_NAME}},{Key=Class,Value=${RUNNER_CLASS_ID}}]" \
23
+ --output json)
24
+
25
+ INSTANCE_ID=$(jq -r '.Instances[0].InstanceId' <<<"$RUN_JSON")
26
+
27
+ echo "Launched: $INSTANCE_ID"
28
+ echo "Instance details:"
29
+ echo " Region: $AWS_REGION"
30
+ echo " AMI ID: $AMI_ID"
31
+ echo " Launch Template ID: $AWS_LAUNCH_TEMPLATE_ID"
32
+ echo " Launch Template Version: $AWS_LAUNCH_TEMPLATE_VERSION"
33
+
34
+ echo "Waiting for instance to be running..."
35
+
36
+ # --- 2) Wait for running + status checks ---
37
+ aws ec2 wait instance-running --region "$AWS_REGION" --instance-ids "$INSTANCE_ID"
38
+ echo "✓ Instance is now running"
39
+
40
+ echo "Waiting for instance to pass status checks..."
41
+
42
+ aws ec2 wait instance-status-ok --region "$AWS_REGION" --instance-ids "$INSTANCE_ID"
43
+ echo "✓ Instance passed all status checks"
44
+
45
+ # Additional validation - check instance state details
46
+ echo "Validating instance readiness..."
47
+ INSTANCE_STATE=$(aws ec2 describe-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" \
48
+ --query 'Reservations[0].Instances[0].{State:State.Name,StatusChecks:StateTransitionReason}' \
49
+ --output json)
50
+ echo "Instance state details: $INSTANCE_STATE"
51
+
52
+ # --- 3) Ensure SSM connectivity ---
53
+ echo "Waiting for SSM connectivity..."
54
+ echo "This can take several minutes for the SSM agent to be fully ready..."
55
+
56
+ # First, check if the instance is registered with SSM
57
+ echo "Checking SSM instance registration..."
58
+ TRIES=0; MAX_TRIES=60
59
+ while :; do
60
+ echo "Attempt $((TRIES+1))/$MAX_TRIES: Checking if instance is registered with SSM..."
61
+
62
+ # Check if instance appears in SSM managed instances
63
+ if aws ssm describe-instance-information \
64
+ --region "$AWS_REGION" \
65
+ --filters "Key=InstanceIds,Values=$INSTANCE_ID" \
66
+ --query 'InstanceInformationList[0].InstanceId' \
67
+ --output text 2>/dev/null | grep -q "$INSTANCE_ID"; then
68
+ echo "✓ Instance is registered with SSM"
69
+ break
70
+ fi
71
+
72
+ TRIES=$((TRIES+1))
73
+ if [ $TRIES -ge $MAX_TRIES ]; then
74
+ echo "❌ SSM registration timeout - instance may not have proper IAM role or SSM agent"
75
+ echo "Checking instance details for debugging..."
76
+ aws ec2 describe-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" \
77
+ --query 'Reservations[0].Instances[0].{State:State.Name,IAMProfile:IamInstanceProfile.Arn,SecurityGroups:SecurityGroups[].GroupId}' \
78
+ --output table
79
+ exit 2
80
+ fi
81
+ echo "Instance not yet registered with SSM, waiting..."
82
+ sleep 10
83
+ done
84
+
85
+ # Now test SSM command execution
86
+ echo "Testing SSM command execution..."
87
+ TRIES=0; MAX_TRIES=30
88
+ while :; do
89
+ echo "Attempt $((TRIES+1))/$MAX_TRIES: Sending test SSM command..."
90
+
91
+ if CMD_JSON=$(aws ssm send-command \
92
+ --region "$AWS_REGION" \
93
+ --targets "Key=instanceIds,Values=$INSTANCE_ID" \
94
+ --document-name "AWS-RunPowerShellScript" \
95
+ --parameters 'commands=["echo SSM connectivity test successful"]' \
96
+ --output json 2>/dev/null); then
97
+
98
+ COMMAND_ID=$(jq -r '.Command.CommandId' <<<"$CMD_JSON")
99
+ echo "✓ SSM command sent successfully (Command ID: $COMMAND_ID)"
100
+
101
+ # Wait for command to complete and check status
102
+ echo "Waiting for command execution..."
103
+ if aws ssm wait command-executed --region "$AWS_REGION" --command-id "$COMMAND_ID" --instance-id "$INSTANCE_ID" 2>/dev/null; then
104
+ echo "✓ SSM connectivity confirmed"
105
+ break
106
+ else
107
+ echo "⚠ Command execution may have failed, checking status..."
108
+ CMD_STATUS=$(aws ssm get-command-invocation \
109
+ --region "$AWS_REGION" \
110
+ --command-id "$COMMAND_ID" \
111
+ --instance-id "$INSTANCE_ID" \
112
+ --query 'Status' \
113
+ --output text 2>/dev/null || echo "Unknown")
114
+ echo "Command status: $CMD_STATUS"
115
+
116
+ if [ "$CMD_STATUS" = "Success" ]; then
117
+ echo "✓ Command actually succeeded"
118
+ break
119
+ fi
120
+ fi
121
+ else
122
+ echo "⚠ Failed to send SSM command"
123
+ fi
124
+
125
+ TRIES=$((TRIES+1))
126
+ if [ $TRIES -ge $MAX_TRIES ]; then
127
+ echo "❌ SSM command execution timeout"
128
+ echo "Final debugging information:"
129
+
130
+ # Get SSM agent status
131
+ echo "SSM Agent status on instance:"
132
+ aws ssm describe-instance-information \
133
+ --region "$AWS_REGION" \
134
+ --filters "Key=InstanceIds,Values=$INSTANCE_ID" \
135
+ --query 'InstanceInformationList[0].{PingStatus:PingStatus,LastPingDateTime:LastPingDateTime,AgentVersion:AgentVersion}' \
136
+ --output table 2>/dev/null || echo "Could not retrieve SSM status"
137
+
138
+ exit 2
139
+ fi
140
+ echo "Retrying in 20 seconds..."
141
+ sleep 20
142
+ done
143
+
144
+ echo "Getting Public IP..."
145
+
146
+ # # --- 4) Get instance Public IP ---
147
+ DESC_JSON=$(aws ec2 describe-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" --output json)
148
+ PUBLIC_IP=$(jq -r '.Reservations[0].Instances[0].PublicIpAddress // empty' <<<"$DESC_JSON")
149
+ [ -n "$PUBLIC_IP" ] || PUBLIC_IP="No public IP assigned"
150
+
151
+ # echo "Getting Websocket Port..."
152
+
153
+
154
+ # --- 5) Read WebSocket config JSON ---
155
+ echo "Reading WebSocket configuration from: $WS_CONFIG_PATH"
156
+ READ_JSON=$(aws ssm send-command \
157
+ --region "$AWS_REGION" \
158
+ --instance-ids "$INSTANCE_ID" \
159
+ --document-name "AWS-RunPowerShellScript" \
160
+ --parameters "commands=[\"if (Test-Path '${WS_CONFIG_PATH}') { Get-Content -Raw '${WS_CONFIG_PATH}' } else { Write-Output 'Config file not found at ${WS_CONFIG_PATH}' }\"]" \
161
+ --output json)
162
+
163
+ READ_CMD_ID=$(jq -r '.Command.CommandId' <<<"$READ_JSON")
164
+ echo "WebSocket config read command ID: $READ_CMD_ID"
165
+
166
+ echo "Waiting for WebSocket config command to complete..."
167
+ aws ssm wait command-executed --region "$AWS_REGION" --command-id "$READ_CMD_ID" --instance-id "$INSTANCE_ID"
168
+
169
+ INVOC=$(aws ssm get-command-invocation \
170
+ --region "$AWS_REGION" \
171
+ --command-id "$READ_CMD_ID" \
172
+ --instance-id "$INSTANCE_ID" \
173
+ --output json)
174
+
175
+ STDOUT=$(jq -r '.StandardOutputContent // ""' <<<"$INVOC")
176
+ STDERR=$(jq -r '.StandardErrorContent // ""' <<<"$INVOC")
177
+ CMD_STATUS=$(jq -r '.Status // ""' <<<"$INVOC")
178
+
179
+ echo "WebSocket config command status: $CMD_STATUS"
180
+ if [ -n "$STDERR" ] && [ "$STDERR" != "null" ]; then
181
+ echo "WebSocket config stderr: $STDERR"
182
+ fi
183
+ echo "WebSocket config raw output: $STDOUT"
184
+
185
+ # --- 6) Output results ---
186
+ echo "Setup complete!"
187
+ echo "PUBLIC_IP=$PUBLIC_IP"
188
+ echo "INSTANCE_ID=$INSTANCE_ID"
189
+ echo "AWS_REGION=$AWS_REGION"