Skip to content

Commit 5d6bb88

Browse files
committed
Fix file URL pathname handling
1 parent b07a315 commit 5d6bb88

File tree

4 files changed

+203
-34
lines changed

4 files changed

+203
-34
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
name: Integration Tests
2+
3+
on:
4+
push:
5+
branches: [main, develop]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
jobs:
10+
test-modes:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
test-case:
15+
- name: "create-and-execute"
16+
mode: "create-and-execute"
17+
- name: "create-only"
18+
mode: "create-only"
19+
- name: "drift-revert"
20+
mode: "create-only"
21+
deployment-mode: "REVERT_DRIFT"
22+
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- name: Configure AWS credentials
27+
uses: aws-actions/configure-aws-credentials@v4
28+
with:
29+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
30+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
31+
aws-region: us-east-1
32+
33+
- name: Test ${{ matrix.test-case.name }}
34+
id: deploy
35+
uses: ./
36+
with:
37+
mode: ${{ matrix.test-case.mode }}
38+
name: test-${{ matrix.test-case.name }}-${{ github.run_number }}
39+
template: |
40+
AWSTemplateFormatVersion: '2010-09-09'
41+
Parameters:
42+
BucketPrefix:
43+
Type: String
44+
Default: test
45+
Resources:
46+
TestBucket:
47+
Type: AWS::S3::Bucket
48+
Properties:
49+
BucketName: !Sub '${BucketPrefix}-bucket-${AWS::StackId}'
50+
Outputs:
51+
BucketName:
52+
Value: !Ref TestBucket
53+
parameter-overrides: "BucketPrefix=integration-test"
54+
deployment-mode: ${{ matrix.test-case.deployment-mode }}
55+
no-fail-on-empty-changeset: "1"
56+
57+
- name: Verify outputs
58+
run: |
59+
echo "Stack ID: ${{ steps.deploy.outputs.stack-id }}"
60+
echo "Bucket Name: ${{ steps.deploy.outputs.BucketName }}"
61+
if [ "${{ matrix.test-case.mode }}" = "create-only" ]; then
62+
echo "Change Set ID: ${{ steps.deploy.outputs.change-set-id }}"
63+
echo "Has Changes: ${{ steps.deploy.outputs.has-changes }}"
64+
echo "Changes Count: ${{ steps.deploy.outputs.changes-count }}"
65+
fi
66+
67+
test-execute-only:
68+
runs-on: ubuntu-latest
69+
steps:
70+
- uses: actions/checkout@v4
71+
72+
- name: Configure AWS credentials
73+
uses: aws-actions/configure-aws-credentials@v4
74+
with:
75+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
76+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
77+
aws-region: us-east-1
78+
79+
- name: Create change set
80+
id: create-cs
81+
uses: ./
82+
with:
83+
mode: "create-only"
84+
name: test-execute-${{ github.run_number }}
85+
template: |
86+
AWSTemplateFormatVersion: '2010-09-09'
87+
Resources:
88+
TestBucket:
89+
Type: AWS::S3::Bucket
90+
91+
- name: Execute change set
92+
uses: ./
93+
with:
94+
mode: "execute-only"
95+
name: test-execute-${{ github.run_number }}
96+
execute-change-set-id: ${{ steps.create-cs.outputs.change-set-id }}
97+
98+
test-advanced-features:
99+
runs-on: ubuntu-latest
100+
steps:
101+
- uses: actions/checkout@v4
102+
103+
- name: Configure AWS credentials
104+
uses: aws-actions/configure-aws-credentials@v4
105+
with:
106+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
107+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
108+
aws-region: us-east-1
109+
110+
- name: Test with all features
111+
uses: ./
112+
with:
113+
name: test-advanced-${{ github.run_number }}
114+
template: |
115+
AWSTemplateFormatVersion: '2010-09-09'
116+
Parameters:
117+
Environment:
118+
Type: String
119+
Resources:
120+
TestBucket:
121+
Type: AWS::S3::Bucket
122+
Properties:
123+
Tags:
124+
- Key: Environment
125+
Value: !Ref Environment
126+
parameter-overrides: "Environment=test"
127+
capabilities: "CAPABILITY_IAM,CAPABILITY_NAMED_IAM"
128+
tags: '[{"Key": "Project", "Value": "Integration-Test"}]'
129+
timeout-in-minutes: 10
130+
include-nested-stacks-change-set: "1"
131+
change-set-name: "custom-changeset-name"
132+
133+
cleanup:
134+
runs-on: ubuntu-latest
135+
needs: [test-modes, test-execute-only, test-advanced-features]
136+
if: always()
137+
steps:
138+
- name: Configure AWS credentials
139+
uses: aws-actions/configure-aws-credentials@v4
140+
with:
141+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
142+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
143+
aws-region: us-east-1
144+
145+
- name: Cleanup test stacks
146+
run: |
147+
aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE --query "StackSummaries[?contains(StackName, 'test-') && contains(StackName, '${{ github.run_number }}')].StackName" --output text | tr '\t' '\n' | while read stack; do
148+
if [ ! -z "$stack" ]; then
149+
echo "Deleting stack: $stack"
150+
aws cloudformation delete-stack --stack-name "$stack" || true
151+
fi
152+
done

dist/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50602,7 +50602,7 @@ function parseNumber(s) {
5060250602
function parseParameters(parameterOverrides) {
5060350603
try {
5060450604
const path = new URL(parameterOverrides);
50605-
const rawParameters = fs.readFileSync(path, 'utf-8');
50605+
const rawParameters = fs.readFileSync(path.pathname, 'utf-8');
5060650606
return JSON.parse(rawParameters);
5060750607
}
5060850608
catch (err) {

src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function parseNumber(s: string): number | undefined {
4040
export function parseParameters(parameterOverrides: string): Parameter[] {
4141
try {
4242
const path = new URL(parameterOverrides)
43-
const rawParameters = fs.readFileSync(path, 'utf-8')
43+
const rawParameters = fs.readFileSync(path.pathname, 'utf-8')
4444

4545
return JSON.parse(rawParameters)
4646
} catch (err) {

src/validation.ts

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { z } from 'zod'
22
import * as fs from 'fs'
33

44
// Helper transformers
5-
const parseBoolean = (val?: string) => val ? !!+val : false
6-
const parseNumber = (val?: string) => val ? parseInt(val) || undefined : undefined
7-
const parseARNs = (val?: string) => val?.length ? val.split(',') : undefined
5+
const parseBoolean = (val?: string) => (val ? !!+val : false)
6+
const parseNumber = (val?: string) =>
7+
val ? parseInt(val) || undefined : undefined
8+
const parseARNs = (val?: string) => (val?.length ? val.split(',') : undefined)
89
const parseTags = (val?: string) => {
910
if (!val) return undefined
1011
try {
@@ -15,7 +16,7 @@ const parseTags = (val?: string) => {
1516
}
1617
const parseParameters = (val?: string) => {
1718
if (!val) return undefined
18-
19+
1920
try {
2021
const path = new URL(val)
2122
const rawParameters = fs.readFileSync(path, 'utf-8')
@@ -28,21 +29,20 @@ const parseParameters = (val?: string) => {
2829
}
2930

3031
const parameters = new Map<string, string>()
31-
val.split(/,(?=(?:(?:[^"']*["|']){2})*[^"']*$)/g)
32-
.forEach(parameter => {
33-
const values = parameter.trim().split('=')
34-
const key = values[0]
35-
const value = values.slice(1).join('=')
36-
let param = parameters.get(key)
37-
param = !param ? value : [param, value].join(',')
38-
if (
39-
(param.startsWith("'") && param.endsWith("'")) ||
40-
(param.startsWith('"') && param.endsWith('"'))
41-
) {
42-
param = param.substring(1, param.length - 1)
43-
}
44-
parameters.set(key, param)
45-
})
32+
val.split(/,(?=(?:(?:[^"']*["|']){2})*[^"']*$)/g).forEach(parameter => {
33+
const values = parameter.trim().split('=')
34+
const key = values[0]
35+
const value = values.slice(1).join('=')
36+
let param = parameters.get(key)
37+
param = !param ? value : [param, value].join(',')
38+
if (
39+
(param.startsWith("'") && param.endsWith("'")) ||
40+
(param.startsWith('"') && param.endsWith('"'))
41+
) {
42+
param = param.substring(1, param.length - 1)
43+
}
44+
parameters.set(key, param)
45+
})
4646

4747
return [...parameters.keys()].map(key => ({
4848
ParameterKey: key,
@@ -51,17 +51,22 @@ const parseParameters = (val?: string) => {
5151
}
5252

5353
const baseSchema = z.object({
54-
mode: z.enum(['create-and-execute', 'create-only', 'execute-only']).default('create-and-execute'),
54+
mode: z
55+
.enum(['create-and-execute', 'create-only', 'execute-only'])
56+
.default('create-and-execute'),
5557
name: z.string().min(1, 'Stack name is required'),
5658
'http-proxy': z.string().optional()
5759
})
5860

5961
const createSchema = baseSchema.extend({
6062
mode: z.enum(['create-and-execute', 'create-only']),
6163
template: z.string().min(1, 'Template is required for create modes'),
62-
capabilities: z.string().optional().transform(val =>
63-
val ? val.split(',').map(cap => cap.trim()) : ['CAPABILITY_IAM']
64-
),
64+
capabilities: z
65+
.string()
66+
.optional()
67+
.transform(val =>
68+
val ? val.split(',').map(cap => cap.trim()) : ['CAPABILITY_IAM']
69+
),
6570
'parameter-overrides': z.string().optional().transform(parseParameters),
6671
'no-fail-on-empty-changeset': z.string().optional().transform(parseBoolean),
6772
'no-execute-changeset': z.string().optional().transform(parseBoolean),
@@ -73,18 +78,28 @@ const createSchema = baseSchema.extend({
7378
tags: z.string().optional().transform(parseTags),
7479
'termination-protection': z.string().optional().transform(parseBoolean),
7580
'change-set-name': z.string().optional(),
76-
'include-nested-stacks-change-set': z.string().optional().transform(parseBoolean),
77-
'deployment-mode': z.string().optional().transform(val => {
78-
if (!val) return undefined
79-
if (val === 'REVERT_DRIFT') return val
80-
throw new Error(`Invalid deployment-mode: ${val}. Only 'REVERT_DRIFT' is supported.`)
81-
}),
81+
'include-nested-stacks-change-set': z
82+
.string()
83+
.optional()
84+
.transform(parseBoolean),
85+
'deployment-mode': z
86+
.string()
87+
.optional()
88+
.transform(val => {
89+
if (!val) return undefined
90+
if (val === 'REVERT_DRIFT') return val
91+
throw new Error(
92+
`Invalid deployment-mode: ${val}. Only 'REVERT_DRIFT' is supported.`
93+
)
94+
}),
8295
'execute-change-set-id': z.undefined()
8396
})
8497

8598
const executeSchema = baseSchema.extend({
8699
mode: z.literal('execute-only'),
87-
'execute-change-set-id': z.string().min(1, 'Change set ID is required for execute-only mode'),
100+
'execute-change-set-id': z
101+
.string()
102+
.min(1, 'Change set ID is required for execute-only mode'),
88103
template: z.undefined(),
89104
'parameter-overrides': z.undefined(),
90105
'deployment-mode': z.undefined(),
@@ -102,9 +117,11 @@ const executeSchema = baseSchema.extend({
102117
'include-nested-stacks-change-set': z.undefined()
103118
})
104119

105-
export function validateAndParseInputs(inputs: Record<string, string | undefined>) {
120+
export function validateAndParseInputs(
121+
inputs: Record<string, string | undefined>
122+
) {
106123
const mode = inputs.mode || 'create-and-execute'
107-
124+
108125
if (mode === 'execute-only') {
109126
return executeSchema.parse(inputs)
110127
} else {

0 commit comments

Comments
 (0)