rastack 0.0.41 → 0.0.43
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.
- package/CHANGELOG.md +15 -0
- package/dist/deploy/config.d.ts +23 -7
- package/dist/deploy/config.js +29 -12
- package/dist/deploy/io.d.ts +15 -0
- package/dist/deploy/io.js +32 -2
- package/dist/rastack-bootstrap.d.ts +2 -1
- package/dist/rastack-bootstrap.js +14 -6
- package/dist/rastack-init.d.ts +3 -2
- package/dist/rastack-init.js +33 -40
- package/dist/rastack-update.js +16 -8
- package/dist/templates/cloudformation/app.yaml +635 -0
- package/dist/templates/cloudformation/bootstrap.yaml +342 -0
- package/dist/templates/cloudformation/placeholder/bootstrap +28 -0
- package/dist/update.d.ts +11 -0
- package/dist/update.js +14 -0
- package/package.json +2 -2
- package/scripts/copy-templates.js +42 -0
- package/src/deploy/config.ts +35 -10
- package/src/deploy/io.ts +34 -2
- package/src/rastack-bootstrap.ts +17 -6
- package/src/rastack-init.ts +39 -56
- package/src/rastack-update.ts +22 -9
- package/src/update.ts +14 -0
- package/test/deploy.spec.ts +53 -0
- package/test/update.spec.ts +13 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
AWSTemplateFormatVersion: "2010-09-09"
|
|
2
|
+
Description: >-
|
|
3
|
+
React API Stack bootstrap (tier-0). Creates the account+region S3 buckets that
|
|
4
|
+
host the framework CloudFormation templates and the Lambda placeholder, plus a
|
|
5
|
+
least-privilege GitHub OIDC publisher role. Run once per account and region.
|
|
6
|
+
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
# Parameters — everything that varies between accounts / forks lives here, so
|
|
9
|
+
# the same template bootstraps any account without edits.
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
Parameters:
|
|
12
|
+
NamePrefix:
|
|
13
|
+
Type: String
|
|
14
|
+
Default: rastack
|
|
15
|
+
Description: Prefix for the bucket names and the publisher role.
|
|
16
|
+
# No hyphens, matching app.yaml (its NamePrefix feeds a Cognito name).
|
|
17
|
+
AllowedPattern: "^[a-z0-9]{1,20}$"
|
|
18
|
+
|
|
19
|
+
GitHubOwner:
|
|
20
|
+
Type: String
|
|
21
|
+
Default: theserverkid
|
|
22
|
+
Description: GitHub org/user that owns the framework repo allowed to publish.
|
|
23
|
+
|
|
24
|
+
PublisherRepo:
|
|
25
|
+
Type: String
|
|
26
|
+
Default: reactapistack
|
|
27
|
+
Description: >
|
|
28
|
+
The single repository whose CI (via OIDC) may publish templates/artifacts
|
|
29
|
+
into these buckets. Only this repo's workflows can assume the publisher
|
|
30
|
+
role — nothing else, and no long-lived keys.
|
|
31
|
+
|
|
32
|
+
PublisherBranch:
|
|
33
|
+
Type: String
|
|
34
|
+
Default: master
|
|
35
|
+
Description: Branch of PublisherRepo whose workflow may assume the publisher role.
|
|
36
|
+
|
|
37
|
+
CreateOIDCProvider:
|
|
38
|
+
Type: String
|
|
39
|
+
Default: "true"
|
|
40
|
+
AllowedValues: ["true", "false"]
|
|
41
|
+
Description: >
|
|
42
|
+
Create the GitHub Actions OIDC provider. Set "false" if the account
|
|
43
|
+
already has one (an account may have only a single provider per URL) and
|
|
44
|
+
pass its ARN in ExistingOIDCProviderArn.
|
|
45
|
+
|
|
46
|
+
ExistingOIDCProviderArn:
|
|
47
|
+
Type: String
|
|
48
|
+
Default: ""
|
|
49
|
+
Description: ARN of a pre-existing GitHub OIDC provider (when CreateOIDCProvider=false).
|
|
50
|
+
|
|
51
|
+
ReadableByAccount:
|
|
52
|
+
Type: String
|
|
53
|
+
Default: "true"
|
|
54
|
+
AllowedValues: ["true", "false"]
|
|
55
|
+
Description: >
|
|
56
|
+
Grant s3:GetObject on both buckets to the account root, so any IAM
|
|
57
|
+
principal in THIS account can launch a hosted template / read the
|
|
58
|
+
placeholder artifact from the Console without an extra identity policy.
|
|
59
|
+
Public (internet) access stays blocked regardless — account-root is not
|
|
60
|
+
public.
|
|
61
|
+
|
|
62
|
+
Conditions:
|
|
63
|
+
ShouldCreateOIDCProvider: !Equals [!Ref CreateOIDCProvider, "true"]
|
|
64
|
+
GrantAccountRead: !Equals [!Ref ReadableByAccount, "true"]
|
|
65
|
+
HasExistingOIDCArn: !Not [!Equals [!Ref ExistingOIDCProviderArn, ""]]
|
|
66
|
+
|
|
67
|
+
Resources:
|
|
68
|
+
|
|
69
|
+
# -- GitHub OIDC provider ----------------------------------------------------
|
|
70
|
+
# Short-lived OIDC tokens instead of stored AWS keys. Reused by the per-app
|
|
71
|
+
# deploy role (app.yaml) too; created here once so tier-0 owns the trust root.
|
|
72
|
+
GitHubOIDCProvider:
|
|
73
|
+
Type: AWS::IAM::OIDCProvider
|
|
74
|
+
Condition: ShouldCreateOIDCProvider
|
|
75
|
+
Properties:
|
|
76
|
+
Url: https://token.actions.githubusercontent.com
|
|
77
|
+
ClientIdList:
|
|
78
|
+
- sts.amazonaws.com
|
|
79
|
+
# AWS no longer validates these thumbprints for the GitHub host, but the
|
|
80
|
+
# property is still required; both historically-published values are listed.
|
|
81
|
+
ThumbprintList:
|
|
82
|
+
- 6938fd4d98bab03faadb97b34396831e3780aea1
|
|
83
|
+
- 1c58a3a8518e8759bf075b76b750d4f2df264fca
|
|
84
|
+
|
|
85
|
+
# -- Templates bucket --------------------------------------------------------
|
|
86
|
+
# Hosts the *.yaml CloudFormation templates (app.yaml + future ones). Private,
|
|
87
|
+
# versioned, encrypted, TLS-only, public access fully blocked. CloudFormation
|
|
88
|
+
# reads a template referenced by S3 URL using the *deployer's* credentials, so
|
|
89
|
+
# no public read is needed for a Console "Launch Stack" within the account.
|
|
90
|
+
TemplatesBucket:
|
|
91
|
+
Type: AWS::S3::Bucket
|
|
92
|
+
# The template store is durable and shared across every consumer stack —
|
|
93
|
+
# never let a `delete-stack` of the bootstrap take it (and its version
|
|
94
|
+
# history) down with it.
|
|
95
|
+
DeletionPolicy: Retain
|
|
96
|
+
UpdateReplacePolicy: Retain
|
|
97
|
+
Properties:
|
|
98
|
+
BucketName: !Sub "${NamePrefix}-cloudformation-${AWS::AccountId}-${AWS::Region}"
|
|
99
|
+
VersioningConfiguration:
|
|
100
|
+
Status: Enabled
|
|
101
|
+
BucketEncryption:
|
|
102
|
+
ServerSideEncryptionConfiguration:
|
|
103
|
+
- ServerSideEncryptionByDefault:
|
|
104
|
+
SSEAlgorithm: AES256
|
|
105
|
+
PublicAccessBlockConfiguration:
|
|
106
|
+
BlockPublicAcls: true
|
|
107
|
+
BlockPublicPolicy: true
|
|
108
|
+
IgnorePublicAcls: true
|
|
109
|
+
RestrictPublicBuckets: true
|
|
110
|
+
OwnershipControls:
|
|
111
|
+
Rules:
|
|
112
|
+
- ObjectOwnership: BucketOwnerEnforced
|
|
113
|
+
LifecycleConfiguration:
|
|
114
|
+
Rules:
|
|
115
|
+
- Id: expire-noncurrent-template-versions
|
|
116
|
+
Status: Enabled
|
|
117
|
+
NoncurrentVersionExpiration:
|
|
118
|
+
NoncurrentDays: 30
|
|
119
|
+
AbortIncompleteMultipartUpload:
|
|
120
|
+
DaysAfterInitiation: 7
|
|
121
|
+
|
|
122
|
+
TemplatesBucketPolicy:
|
|
123
|
+
Type: AWS::S3::BucketPolicy
|
|
124
|
+
Properties:
|
|
125
|
+
Bucket: !Ref TemplatesBucket
|
|
126
|
+
PolicyDocument:
|
|
127
|
+
Version: "2012-10-17"
|
|
128
|
+
Statement:
|
|
129
|
+
- Sid: DenyInsecureTransport
|
|
130
|
+
Effect: Deny
|
|
131
|
+
Principal: "*"
|
|
132
|
+
Action: "s3:*"
|
|
133
|
+
Resource:
|
|
134
|
+
- !GetAtt TemplatesBucket.Arn
|
|
135
|
+
- !Sub "${TemplatesBucket.Arn}/*"
|
|
136
|
+
Condition:
|
|
137
|
+
Bool:
|
|
138
|
+
aws:SecureTransport: "false"
|
|
139
|
+
- !If
|
|
140
|
+
- GrantAccountRead
|
|
141
|
+
- Sid: AccountReadTemplates
|
|
142
|
+
Effect: Allow
|
|
143
|
+
Principal:
|
|
144
|
+
AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
|
|
145
|
+
Action:
|
|
146
|
+
- s3:GetObject
|
|
147
|
+
- s3:ListBucket
|
|
148
|
+
Resource:
|
|
149
|
+
- !GetAtt TemplatesBucket.Arn
|
|
150
|
+
- !Sub "${TemplatesBucket.Arn}/*"
|
|
151
|
+
- !Ref "AWS::NoValue"
|
|
152
|
+
|
|
153
|
+
# -- Artifacts bucket --------------------------------------------------------
|
|
154
|
+
# Hosts the Lambda placeholder bundle (placeholders/bootstrap.zip) that the
|
|
155
|
+
# per-app template's function is created from, plus any build artifacts CI
|
|
156
|
+
# stages. Same hardening as the templates bucket.
|
|
157
|
+
ArtifactsBucket:
|
|
158
|
+
Type: AWS::S3::Bucket
|
|
159
|
+
DeletionPolicy: Retain
|
|
160
|
+
UpdateReplacePolicy: Retain
|
|
161
|
+
Properties:
|
|
162
|
+
BucketName: !Sub "${NamePrefix}-artifacts-${AWS::AccountId}-${AWS::Region}"
|
|
163
|
+
VersioningConfiguration:
|
|
164
|
+
Status: Enabled
|
|
165
|
+
BucketEncryption:
|
|
166
|
+
ServerSideEncryptionConfiguration:
|
|
167
|
+
- ServerSideEncryptionByDefault:
|
|
168
|
+
SSEAlgorithm: AES256
|
|
169
|
+
PublicAccessBlockConfiguration:
|
|
170
|
+
BlockPublicAcls: true
|
|
171
|
+
BlockPublicPolicy: true
|
|
172
|
+
IgnorePublicAcls: true
|
|
173
|
+
RestrictPublicBuckets: true
|
|
174
|
+
OwnershipControls:
|
|
175
|
+
Rules:
|
|
176
|
+
- ObjectOwnership: BucketOwnerEnforced
|
|
177
|
+
LifecycleConfiguration:
|
|
178
|
+
Rules:
|
|
179
|
+
- Id: expire-noncurrent-artifact-versions
|
|
180
|
+
Status: Enabled
|
|
181
|
+
NoncurrentVersionExpiration:
|
|
182
|
+
NoncurrentDays: 30
|
|
183
|
+
AbortIncompleteMultipartUpload:
|
|
184
|
+
DaysAfterInitiation: 7
|
|
185
|
+
|
|
186
|
+
ArtifactsBucketPolicy:
|
|
187
|
+
Type: AWS::S3::BucketPolicy
|
|
188
|
+
Properties:
|
|
189
|
+
Bucket: !Ref ArtifactsBucket
|
|
190
|
+
PolicyDocument:
|
|
191
|
+
Version: "2012-10-17"
|
|
192
|
+
Statement:
|
|
193
|
+
- Sid: DenyInsecureTransport
|
|
194
|
+
Effect: Deny
|
|
195
|
+
Principal: "*"
|
|
196
|
+
Action: "s3:*"
|
|
197
|
+
Resource:
|
|
198
|
+
- !GetAtt ArtifactsBucket.Arn
|
|
199
|
+
- !Sub "${ArtifactsBucket.Arn}/*"
|
|
200
|
+
Condition:
|
|
201
|
+
Bool:
|
|
202
|
+
aws:SecureTransport: "false"
|
|
203
|
+
- !If
|
|
204
|
+
- GrantAccountRead
|
|
205
|
+
- Sid: AccountReadArtifacts
|
|
206
|
+
Effect: Allow
|
|
207
|
+
Principal:
|
|
208
|
+
AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
|
|
209
|
+
Action:
|
|
210
|
+
- s3:GetObject
|
|
211
|
+
- s3:ListBucket
|
|
212
|
+
Resource:
|
|
213
|
+
- !GetAtt ArtifactsBucket.Arn
|
|
214
|
+
- !Sub "${ArtifactsBucket.Arn}/*"
|
|
215
|
+
- !Ref "AWS::NoValue"
|
|
216
|
+
|
|
217
|
+
# -- Publisher role (GitHub OIDC) --------------------------------------------
|
|
218
|
+
# The framework's CI assumes THIS role (no stored keys) to sync the templates
|
|
219
|
+
# and the placeholder artifact into the two buckets. Its trust is pinned to a
|
|
220
|
+
# single repo+branch, and its permission grant is scoped to exactly these two
|
|
221
|
+
# buckets — plus an explicit deny that stops it re-governing them or escalating.
|
|
222
|
+
TemplatePublisherRole:
|
|
223
|
+
Type: AWS::IAM::Role
|
|
224
|
+
Properties:
|
|
225
|
+
RoleName: !Sub "${NamePrefix}-template-publisher"
|
|
226
|
+
Description: CI role that publishes React API Stack templates + artifacts.
|
|
227
|
+
MaxSessionDuration: 3600
|
|
228
|
+
AssumeRolePolicyDocument:
|
|
229
|
+
Version: "2012-10-17"
|
|
230
|
+
Statement:
|
|
231
|
+
- Effect: Allow
|
|
232
|
+
Principal:
|
|
233
|
+
# When reusing an existing provider but no ARN was passed, derive
|
|
234
|
+
# the account's single standard GitHub provider ARN — never an
|
|
235
|
+
# empty principal (which IAM rejects as MalformedPolicyDocument).
|
|
236
|
+
Federated: !If
|
|
237
|
+
- ShouldCreateOIDCProvider
|
|
238
|
+
- !Ref GitHubOIDCProvider
|
|
239
|
+
- !If
|
|
240
|
+
- HasExistingOIDCArn
|
|
241
|
+
- !Ref ExistingOIDCProviderArn
|
|
242
|
+
- !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com"
|
|
243
|
+
Action: sts:AssumeRoleWithWebIdentity
|
|
244
|
+
Condition:
|
|
245
|
+
StringEquals:
|
|
246
|
+
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
247
|
+
StringLike:
|
|
248
|
+
token.actions.githubusercontent.com:sub: !Sub "repo:${GitHubOwner}/${PublisherRepo}:ref:refs/heads/${PublisherBranch}"
|
|
249
|
+
Policies:
|
|
250
|
+
- PolicyName: publish-to-template-buckets
|
|
251
|
+
PolicyDocument:
|
|
252
|
+
Version: "2012-10-17"
|
|
253
|
+
Statement:
|
|
254
|
+
- Sid: WriteObjects
|
|
255
|
+
Effect: Allow
|
|
256
|
+
Action:
|
|
257
|
+
- s3:PutObject
|
|
258
|
+
- s3:GetObject
|
|
259
|
+
- s3:DeleteObject
|
|
260
|
+
Resource:
|
|
261
|
+
- !Sub "${TemplatesBucket.Arn}/*"
|
|
262
|
+
- !Sub "${ArtifactsBucket.Arn}/*"
|
|
263
|
+
- Sid: ListBuckets
|
|
264
|
+
Effect: Allow
|
|
265
|
+
Action:
|
|
266
|
+
- s3:ListBucket
|
|
267
|
+
- s3:GetBucketLocation
|
|
268
|
+
Resource:
|
|
269
|
+
- !GetAtt TemplatesBucket.Arn
|
|
270
|
+
- !GetAtt ArtifactsBucket.Arn
|
|
271
|
+
- PolicyName: deny-privilege-escalation
|
|
272
|
+
PolicyDocument:
|
|
273
|
+
Version: "2012-10-17"
|
|
274
|
+
Statement:
|
|
275
|
+
# Defense-in-depth: a compromised publish workflow can only write
|
|
276
|
+
# template/artifact objects. It can never re-govern the buckets,
|
|
277
|
+
# touch identity, or grant itself anything more.
|
|
278
|
+
- Sid: DenyPrivilegeEscalation
|
|
279
|
+
Effect: Deny
|
|
280
|
+
Action:
|
|
281
|
+
- iam:*
|
|
282
|
+
- sts:AssumeRole
|
|
283
|
+
- organizations:*
|
|
284
|
+
- account:*
|
|
285
|
+
- s3:PutBucketPolicy
|
|
286
|
+
- s3:DeleteBucketPolicy
|
|
287
|
+
- s3:PutBucketPublicAccessBlock
|
|
288
|
+
- s3:PutBucketAcl
|
|
289
|
+
- s3:PutObjectAcl
|
|
290
|
+
- s3:DeleteBucket
|
|
291
|
+
Resource: "*"
|
|
292
|
+
|
|
293
|
+
Outputs:
|
|
294
|
+
TemplatesBucketName:
|
|
295
|
+
Description: S3 bucket hosting the CloudFormation templates.
|
|
296
|
+
Value: !Ref TemplatesBucket
|
|
297
|
+
Export:
|
|
298
|
+
Name: !Sub "${AWS::StackName}-TemplatesBucket"
|
|
299
|
+
|
|
300
|
+
TemplatesBaseUrl:
|
|
301
|
+
Description: Base HTTPS URL of the templates bucket (append the object key).
|
|
302
|
+
Value: !Sub "https://${TemplatesBucket}.s3.${AWS::Region}.amazonaws.com"
|
|
303
|
+
|
|
304
|
+
AppTemplateUrl:
|
|
305
|
+
Description: >
|
|
306
|
+
URL the per-app backend template will live at once published
|
|
307
|
+
(deploy/cloudformation/app.yaml → app.yaml). Consumer repos launch from this.
|
|
308
|
+
Value: !Sub "https://${TemplatesBucket}.s3.${AWS::Region}.amazonaws.com/app.yaml"
|
|
309
|
+
|
|
310
|
+
LaunchAppStackUrl:
|
|
311
|
+
Description: >
|
|
312
|
+
One-click Console "Launch Stack" (quick-create) link for the hosted app
|
|
313
|
+
template. Fill in GitHubRepo/AppName parameters on the review screen.
|
|
314
|
+
Value: !Sub "https://${AWS::Region}.console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/create/review?templateURL=https://${TemplatesBucket}.s3.${AWS::Region}.amazonaws.com/app.yaml&stackName=rastack-app"
|
|
315
|
+
|
|
316
|
+
ArtifactsBucketName:
|
|
317
|
+
Description: S3 bucket hosting the Lambda placeholder + build artifacts.
|
|
318
|
+
Value: !Ref ArtifactsBucket
|
|
319
|
+
Export:
|
|
320
|
+
Name: !Sub "${AWS::StackName}-ArtifactsBucket"
|
|
321
|
+
|
|
322
|
+
ArtifactsBaseUrl:
|
|
323
|
+
Description: Base HTTPS URL of the artifacts bucket.
|
|
324
|
+
Value: !Sub "https://${ArtifactsBucket}.s3.${AWS::Region}.amazonaws.com"
|
|
325
|
+
|
|
326
|
+
PublisherRoleArn:
|
|
327
|
+
Description: >
|
|
328
|
+
Role ARN the framework CI assumes via OIDC to publish templates/artifacts.
|
|
329
|
+
Wire as the AWS_PUBLISH_ROLE_ARN repo Variable in the framework repo.
|
|
330
|
+
Value: !GetAtt TemplatePublisherRole.Arn
|
|
331
|
+
Export:
|
|
332
|
+
Name: !Sub "${AWS::StackName}-PublisherRoleArn"
|
|
333
|
+
|
|
334
|
+
OIDCProviderArn:
|
|
335
|
+
Description: GitHub OIDC provider ARN (created or reused) — reuse in app.yaml.
|
|
336
|
+
Value: !If
|
|
337
|
+
- ShouldCreateOIDCProvider
|
|
338
|
+
- !Ref GitHubOIDCProvider
|
|
339
|
+
- !If
|
|
340
|
+
- HasExistingOIDCArn
|
|
341
|
+
- !Ref ExistingOIDCProviderArn
|
|
342
|
+
- !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Placeholder React API Stack Lambda bootstrap (provided.al2023 custom runtime).
|
|
3
|
+
#
|
|
4
|
+
# Its one guaranteed job: be a valid code bundle so app.yaml can CREATE the
|
|
5
|
+
# function with no Rust toolchain (Lambda does not execute this at create time).
|
|
6
|
+
# CI then replaces it out of band with the compiled rastack-server via
|
|
7
|
+
# `aws lambda update-function-code`; app.yaml pins a fixed Code.S3Key, so stack
|
|
8
|
+
# updates never revert what CI shipped.
|
|
9
|
+
#
|
|
10
|
+
# Best-effort until real code lands: if the (minimal) runtime has curl, every
|
|
11
|
+
# invocation returns a clear 503 rather than an opaque error. The placeholder is
|
|
12
|
+
# never meant to serve real traffic — deploy the backend before pointing at it.
|
|
13
|
+
set -eu
|
|
14
|
+
|
|
15
|
+
while true; do
|
|
16
|
+
HEADERS="$(mktemp)"
|
|
17
|
+
# Long-poll for the next invocation.
|
|
18
|
+
curl -sS -LD "$HEADERS" -o /dev/null \
|
|
19
|
+
"http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next"
|
|
20
|
+
REQUEST_ID="$(grep -Fi 'Lambda-Runtime-Aws-Request-Id' "$HEADERS" \
|
|
21
|
+
| tr -d '[:space:]' | cut -d: -f2)"
|
|
22
|
+
[ -n "${REQUEST_ID}" ] || continue
|
|
23
|
+
|
|
24
|
+
RESPONSE='{"statusCode":503,"headers":{"content-type":"application/json"},"body":"{\"detail\":\"React API Stack backend not yet deployed — CI has not shipped the rastack-server bundle to this function.\"}"}'
|
|
25
|
+
curl -sS -o /dev/null \
|
|
26
|
+
"http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/${REQUEST_ID}/response" \
|
|
27
|
+
-d "${RESPONSE}"
|
|
28
|
+
done
|
package/dist/update.d.ts
CHANGED
|
@@ -52,6 +52,17 @@ export declare function npmBinary(platform?: string): string;
|
|
|
52
52
|
* shell means no injection surface).
|
|
53
53
|
*/
|
|
54
54
|
export declare function npmSpawnShell(platform?: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The single command string used to launch npm on platforms that must go through
|
|
57
|
+
* a shell (Windows — see `npmSpawnShell`). Node 22+ deprecates passing an args
|
|
58
|
+
* *array* together with `shell: true` (DEP0190: the args are concatenated, not
|
|
59
|
+
* escaped), so the shell path builds one string and runs it via `execSync`
|
|
60
|
+
* instead. Joining with spaces is safe here because every token is a fixed
|
|
61
|
+
* literal except the version, which `sanitizeVersion` constrains to
|
|
62
|
+
* `[A-Za-z0-9._-]` (no shell metacharacters). POSIX never uses this — it keeps
|
|
63
|
+
* the shell-free, array-based `execFile`, which has no concatenation at all.
|
|
64
|
+
*/
|
|
65
|
+
export declare function npmShellCommand(binary: string, args: string[]): string;
|
|
55
66
|
/** The npm package spec to install for a target version. */
|
|
56
67
|
export declare function installSpec(version: string): string;
|
|
57
68
|
/** The `npm <args>` (after the `npm` binary) that installs the target globally. */
|
package/dist/update.js
CHANGED
|
@@ -15,6 +15,7 @@ exports.parseUpdateFlags = parseUpdateFlags;
|
|
|
15
15
|
exports.sanitizeVersion = sanitizeVersion;
|
|
16
16
|
exports.npmBinary = npmBinary;
|
|
17
17
|
exports.npmSpawnShell = npmSpawnShell;
|
|
18
|
+
exports.npmShellCommand = npmShellCommand;
|
|
18
19
|
exports.installSpec = installSpec;
|
|
19
20
|
exports.npmInstallArgs = npmInstallArgs;
|
|
20
21
|
exports.globalPrefixFromPackageDir = globalPrefixFromPackageDir;
|
|
@@ -88,6 +89,19 @@ function npmBinary(platform = process.platform) {
|
|
|
88
89
|
function npmSpawnShell(platform = process.platform) {
|
|
89
90
|
return platform === "win32";
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* The single command string used to launch npm on platforms that must go through
|
|
94
|
+
* a shell (Windows — see `npmSpawnShell`). Node 22+ deprecates passing an args
|
|
95
|
+
* *array* together with `shell: true` (DEP0190: the args are concatenated, not
|
|
96
|
+
* escaped), so the shell path builds one string and runs it via `execSync`
|
|
97
|
+
* instead. Joining with spaces is safe here because every token is a fixed
|
|
98
|
+
* literal except the version, which `sanitizeVersion` constrains to
|
|
99
|
+
* `[A-Za-z0-9._-]` (no shell metacharacters). POSIX never uses this — it keeps
|
|
100
|
+
* the shell-free, array-based `execFile`, which has no concatenation at all.
|
|
101
|
+
*/
|
|
102
|
+
function npmShellCommand(binary, args) {
|
|
103
|
+
return [binary, ...args].join(" ");
|
|
104
|
+
}
|
|
91
105
|
/** The npm package spec to install for a target version. */
|
|
92
106
|
function installSpec(version) {
|
|
93
107
|
return !version || version === "latest" ? "rastack" : `rastack@${version}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rastack",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.43",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "runtime.ts",
|
|
6
6
|
"types": "runtime.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
|
16
|
-
"build": "tsc && cp src/.prettierrc dist/.prettierrc",
|
|
16
|
+
"build": "tsc && cp src/.prettierrc dist/.prettierrc && node scripts/copy-templates.js",
|
|
17
17
|
"start": "node dist/index.js",
|
|
18
18
|
"test": "jest"
|
|
19
19
|
},
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copy the canonical CloudFormation templates (deploy/cloudformation/*.yaml + the
|
|
4
|
+
* Lambda placeholder bundle) into the built package so `rastack init` /
|
|
5
|
+
* `rastack bootstrap` can deploy them from ANY repo.
|
|
6
|
+
*
|
|
7
|
+
* A consumer repo (the demo, any app) deliberately does not carry a copy of the
|
|
8
|
+
* framework IaC (deploy/cloudformation/README.md), so the CLI ships the templates
|
|
9
|
+
* itself. `resolveDeployAsset` (src/deploy/io.ts) reads them from
|
|
10
|
+
* `dist/templates/cloudformation/`, which this script populates from the single
|
|
11
|
+
* source of truth at repo-root `deploy/cloudformation/`.
|
|
12
|
+
*/
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
|
|
16
|
+
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
17
|
+
const srcDir = path.join(repoRoot, "deploy", "cloudformation");
|
|
18
|
+
const destDir = path.join(__dirname, "..", "dist", "templates", "cloudformation");
|
|
19
|
+
|
|
20
|
+
const assets = [
|
|
21
|
+
"app.yaml",
|
|
22
|
+
"bootstrap.yaml",
|
|
23
|
+
path.join("placeholder", "bootstrap"),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
fs.mkdirSync(path.join(destDir, "placeholder"), { recursive: true });
|
|
27
|
+
|
|
28
|
+
for (const rel of assets) {
|
|
29
|
+
const src = path.join(srcDir, rel);
|
|
30
|
+
const dest = path.join(destDir, rel);
|
|
31
|
+
if (!fs.existsSync(src)) {
|
|
32
|
+
console.error(`copy-templates: missing source ${src}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
fs.copyFileSync(src, dest);
|
|
36
|
+
// Preserve the executable bit on the Lambda placeholder binary.
|
|
37
|
+
if (rel.endsWith("bootstrap")) fs.chmodSync(dest, 0o755);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(
|
|
41
|
+
`copy-templates: bundled ${assets.length} CloudFormation asset(s) into dist/templates/cloudformation`,
|
|
42
|
+
);
|
package/src/deploy/config.ts
CHANGED
|
@@ -314,7 +314,14 @@ export function stackParameters(cfg: DeployConfig): StackParameter[] {
|
|
|
314
314
|
return params;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
/**
|
|
317
|
+
/**
|
|
318
|
+
* The canonical repo-relative CloudFormation template paths. These are the paths
|
|
319
|
+
* in the *framework* repo (theserverkid/reactapistack) and the default the argv
|
|
320
|
+
* builders below embed. A **consumer** repo (the demo, any app) deliberately does
|
|
321
|
+
* NOT carry a copy of the framework IaC — the templates ship inside the `rastack`
|
|
322
|
+
* package instead — so `init`/`bootstrap` resolve the real on-disk path via
|
|
323
|
+
* `resolveDeployAsset` (io.ts) and pass it in as the `templateFile` override.
|
|
324
|
+
*/
|
|
318
325
|
export const APP_TEMPLATE_FILE = "deploy/cloudformation/app.yaml";
|
|
319
326
|
export const BOOTSTRAP_TEMPLATE_FILE = "deploy/cloudformation/bootstrap.yaml";
|
|
320
327
|
|
|
@@ -327,7 +334,8 @@ export const BOOTSTRAP_TEMPLATE_FILE = "deploy/cloudformation/bootstrap.yaml";
|
|
|
327
334
|
* accepts *only* a local template file. `--template-url` is exclusive to the
|
|
328
335
|
* low-level `create-stack`/`update-stack`/`create-change-set` commands, so
|
|
329
336
|
* pairing it with `deploy` fails with `the following arguments are required:
|
|
330
|
-
* --template-file`.
|
|
337
|
+
* --template-file`. The file is resolved from the `rastack` package (see
|
|
338
|
+
* `resolveDeployAsset`), so a consumer repo need not carry the framework IaC.
|
|
331
339
|
*/
|
|
332
340
|
function cfnDeployArgs(
|
|
333
341
|
templateFile: string,
|
|
@@ -362,10 +370,17 @@ function awsCommand(args: string[]): string {
|
|
|
362
370
|
return `aws ${args.join(" ")}`;
|
|
363
371
|
}
|
|
364
372
|
|
|
365
|
-
/**
|
|
366
|
-
|
|
373
|
+
/**
|
|
374
|
+
* The argv that creates the app's infrastructure (app.yaml). `templateFile`
|
|
375
|
+
* defaults to the canonical repo-relative path; `init` passes the resolved
|
|
376
|
+
* package-bundled path so it works from a consumer repo that carries no IaC.
|
|
377
|
+
*/
|
|
378
|
+
export function deployStackArgs(
|
|
379
|
+
cfg: DeployConfig,
|
|
380
|
+
templateFile: string = APP_TEMPLATE_FILE,
|
|
381
|
+
): string[] {
|
|
367
382
|
return cfnDeployArgs(
|
|
368
|
-
|
|
383
|
+
templateFile,
|
|
369
384
|
cfg.stackName,
|
|
370
385
|
cfg.region,
|
|
371
386
|
stackParameters(cfg),
|
|
@@ -377,8 +392,11 @@ export function deployStackArgs(cfg: DeployConfig): string[] {
|
|
|
377
392
|
* infrastructure (app.yaml), from the in-repo template. `rastack init` runs this
|
|
378
393
|
* for you; it is also printed by `--print` so it can be run by hand.
|
|
379
394
|
*/
|
|
380
|
-
export function deployStackCommand(
|
|
381
|
-
|
|
395
|
+
export function deployStackCommand(
|
|
396
|
+
cfg: DeployConfig,
|
|
397
|
+
templateFile: string = APP_TEMPLATE_FILE,
|
|
398
|
+
): string {
|
|
399
|
+
return awsCommand(deployStackArgs(cfg, templateFile));
|
|
382
400
|
}
|
|
383
401
|
|
|
384
402
|
/**
|
|
@@ -410,13 +428,19 @@ export function bootstrapStackParameters(
|
|
|
410
428
|
return params;
|
|
411
429
|
}
|
|
412
430
|
|
|
413
|
-
/**
|
|
431
|
+
/**
|
|
432
|
+
* The argv that creates the bootstrap stack (bootstrap.yaml). `templateFile`
|
|
433
|
+
* defaults to the canonical repo-relative path; `init`/`bootstrap` pass the
|
|
434
|
+
* resolved package-bundled path so it works from a consumer repo that carries no
|
|
435
|
+
* IaC.
|
|
436
|
+
*/
|
|
414
437
|
export function bootstrapStackArgs(
|
|
415
438
|
cfg: DeployConfig,
|
|
416
439
|
reuseOidc = false,
|
|
440
|
+
templateFile: string = BOOTSTRAP_TEMPLATE_FILE,
|
|
417
441
|
): string[] {
|
|
418
442
|
return cfnDeployArgs(
|
|
419
|
-
|
|
443
|
+
templateFile,
|
|
420
444
|
bootstrapStackName(cfg),
|
|
421
445
|
cfg.region,
|
|
422
446
|
bootstrapStackParameters(cfg, reuseOidc),
|
|
@@ -427,8 +451,9 @@ export function bootstrapStackArgs(
|
|
|
427
451
|
export function bootstrapStackCommand(
|
|
428
452
|
cfg: DeployConfig,
|
|
429
453
|
reuseOidc = false,
|
|
454
|
+
templateFile: string = BOOTSTRAP_TEMPLATE_FILE,
|
|
430
455
|
): string {
|
|
431
|
-
return awsCommand(bootstrapStackArgs(cfg, reuseOidc));
|
|
456
|
+
return awsCommand(bootstrapStackArgs(cfg, reuseOidc, templateFile));
|
|
432
457
|
}
|
|
433
458
|
|
|
434
459
|
/** Stack Output keys `init` reads back off the bootstrap stack. */
|
package/src/deploy/io.ts
CHANGED
|
@@ -18,6 +18,38 @@ export function configPath(cwd = process.cwd()): string {
|
|
|
18
18
|
return path.join(cwd, CONFIG_PATH);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a CloudFormation asset (`app.yaml`, `bootstrap.yaml`, or the
|
|
23
|
+
* `placeholder/bootstrap` Lambda bundle) to an absolute on-disk path.
|
|
24
|
+
*
|
|
25
|
+
* The templates are the **framework's** IaC, not the consumer's — the demo repo
|
|
26
|
+
* and every future app repo deliberately do not carry a copy (see
|
|
27
|
+
* deploy/cloudformation/README.md). So `init`/`bootstrap`, which run in those
|
|
28
|
+
* repos, cannot assume `deploy/cloudformation/<asset>` exists relative to the
|
|
29
|
+
* cwd — that is exactly the `Invalid template path deploy/cloudformation/…`
|
|
30
|
+
* failure. Instead the assets ship *inside* the `rastack` package (copied into
|
|
31
|
+
* `dist/templates/cloudformation/` at build time) and are resolved from the
|
|
32
|
+
* install location, with fallbacks so running from source or from the framework
|
|
33
|
+
* repo itself still works. Returns null when no candidate exists.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveDeployAsset(
|
|
36
|
+
relPath: string,
|
|
37
|
+
cwd = process.cwd(),
|
|
38
|
+
): string | null {
|
|
39
|
+
const candidates = [
|
|
40
|
+
// Installed package: dist/deploy/io.js → dist/templates/cloudformation/…
|
|
41
|
+
path.join(__dirname, "..", "templates", "cloudformation", relPath),
|
|
42
|
+
// Running from TS source: tools/src/deploy/io.ts → repo-root deploy/…
|
|
43
|
+
path.join(__dirname, "..", "..", "..", "deploy", "cloudformation", relPath),
|
|
44
|
+
// Invoked inside the framework repo itself (cwd is the repo root).
|
|
45
|
+
path.join(cwd, "deploy", "cloudformation", relPath),
|
|
46
|
+
];
|
|
47
|
+
for (const candidate of candidates) {
|
|
48
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
21
53
|
/** Read + normalize the deploy config, or null when it does not exist yet. */
|
|
22
54
|
export function loadConfig(cwd = process.cwd()): DeployConfig | null {
|
|
23
55
|
const file = configPath(cwd);
|
|
@@ -126,8 +158,8 @@ export function uploadPlaceholder(
|
|
|
126
158
|
region: string,
|
|
127
159
|
cwd = process.cwd(),
|
|
128
160
|
): boolean {
|
|
129
|
-
const src =
|
|
130
|
-
if (!
|
|
161
|
+
const src = resolveDeployAsset("placeholder/bootstrap", cwd);
|
|
162
|
+
if (!src) return false;
|
|
131
163
|
const zip = zipStore([
|
|
132
164
|
{ name: "bootstrap", data: fs.readFileSync(src), mode: 0o755 },
|
|
133
165
|
]);
|