thinkwork-cli 0.12.8 → 0.12.10
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/README.md +21 -16
- package/dist/{api-client-MC4NOZ4L.js → api-client-ASJWIG3Z.js} +1 -1
- package/dist/{chunk-34NMB5AK.js → chunk-JEHV35EU.js} +31 -0
- package/dist/cli.js +14041 -12816
- package/dist/commands/enterprise/templates/deploy-repo/.github/workflows/deploy.yml +38 -12
- package/dist/commands/enterprise/templates/deploy-repo/docs/runbook.md +85 -78
- package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +1 -0
- package/dist/terraform/examples/greenfield/main.tf +37 -16
- package/dist/terraform/modules/app/static-site/main.tf +34 -3
- package/dist/terraform/modules/app/www-dns/main.tf +34 -7
- package/dist/terraform/modules/app/www-dns/variables.tf +14 -2
- package/dist/terraform/modules/thinkwork/main.tf +66 -15
- package/dist/terraform/modules/thinkwork/outputs.tf +32 -6
- package/dist/terraform/modules/thinkwork/variables.tf +14 -2
- package/package.json +1 -1
|
@@ -4,6 +4,14 @@ name: Deploy ThinkWork
|
|
|
4
4
|
on:
|
|
5
5
|
workflow_dispatch:
|
|
6
6
|
inputs:
|
|
7
|
+
operation:
|
|
8
|
+
description: "Deploy or destroy the stage stack"
|
|
9
|
+
type: choice
|
|
10
|
+
required: true
|
|
11
|
+
default: deploy
|
|
12
|
+
options:
|
|
13
|
+
- deploy
|
|
14
|
+
- destroy
|
|
7
15
|
stage:
|
|
8
16
|
description: "Deployment stage"
|
|
9
17
|
type: string
|
|
@@ -36,10 +44,11 @@ concurrency:
|
|
|
36
44
|
|
|
37
45
|
jobs:
|
|
38
46
|
deploy:
|
|
39
|
-
name: Deploy ${{ github.event.inputs.stage }}
|
|
47
|
+
name: ${{ github.event.inputs.operation == 'destroy' && 'Destroy' || 'Deploy' }} ${{ github.event.inputs.stage }}
|
|
40
48
|
runs-on: ubuntu-latest
|
|
41
49
|
environment: ${{ github.event.inputs.stage }}
|
|
42
50
|
env:
|
|
51
|
+
OPERATION: ${{ github.event.inputs.operation }}
|
|
43
52
|
STAGE: ${{ github.event.inputs.stage }}
|
|
44
53
|
COMPONENT: ${{ github.event.inputs.component }}
|
|
45
54
|
RUN_SMOKES: ${{ github.event.inputs.run_smokes }}
|
|
@@ -96,7 +105,7 @@ jobs:
|
|
|
96
105
|
terraform_wrapper: false
|
|
97
106
|
|
|
98
107
|
- name: Prepare release artifacts
|
|
99
|
-
if: ${{ github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' || github.event.inputs.component == 'artifacts' }}
|
|
108
|
+
if: ${{ github.event.inputs.operation == 'deploy' && (github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' || github.event.inputs.component == 'artifacts') }}
|
|
100
109
|
shell: bash
|
|
101
110
|
run: |
|
|
102
111
|
set -euo pipefail
|
|
@@ -107,25 +116,25 @@ jobs:
|
|
|
107
116
|
--lambda-prefix "${{ steps.lock.outputs.lambda_prefix }}"
|
|
108
117
|
|
|
109
118
|
- name: Terraform init
|
|
110
|
-
if: ${{ github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' || github.event.inputs.component == 'artifacts' || github.event.inputs.component == 'smokes' }}
|
|
119
|
+
if: ${{ github.event.inputs.operation == 'destroy' || github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' || github.event.inputs.component == 'artifacts' || github.event.inputs.component == 'smokes' }}
|
|
111
120
|
working-directory: terraform
|
|
112
121
|
shell: bash
|
|
113
122
|
run: terraform init -backend-config="backend-${STAGE}.hcl"
|
|
114
123
|
|
|
115
124
|
- name: Select Terraform workspace
|
|
116
|
-
if: ${{ github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' || github.event.inputs.component == 'artifacts' || github.event.inputs.component == 'smokes' }}
|
|
125
|
+
if: ${{ github.event.inputs.operation == 'destroy' || github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' || github.event.inputs.component == 'artifacts' || github.event.inputs.component == 'smokes' }}
|
|
117
126
|
working-directory: terraform
|
|
118
127
|
shell: bash
|
|
119
128
|
run: |
|
|
120
129
|
set -euo pipefail
|
|
121
|
-
if [ "$COMPONENT" = "all" ] || [ "$COMPONENT" = "foundation" ]; then
|
|
130
|
+
if [ "$OPERATION" = "deploy" ] && { [ "$COMPONENT" = "all" ] || [ "$COMPONENT" = "foundation" ]; }; then
|
|
122
131
|
terraform workspace select "$STAGE" || terraform workspace new "$STAGE"
|
|
123
132
|
else
|
|
124
133
|
terraform workspace select "$STAGE"
|
|
125
134
|
fi
|
|
126
135
|
|
|
127
136
|
- name: Terraform apply
|
|
128
|
-
if: ${{ github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' }}
|
|
137
|
+
if: ${{ github.event.inputs.operation == 'deploy' && (github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation') }}
|
|
129
138
|
working-directory: terraform
|
|
130
139
|
shell: bash
|
|
131
140
|
env:
|
|
@@ -139,8 +148,23 @@ jobs:
|
|
|
139
148
|
-var="lambda_artifact_prefix=${{ steps.lock.outputs.lambda_prefix }}" \
|
|
140
149
|
-auto-approve
|
|
141
150
|
|
|
151
|
+
- name: Terraform destroy
|
|
152
|
+
if: ${{ github.event.inputs.operation == 'destroy' }}
|
|
153
|
+
working-directory: terraform
|
|
154
|
+
shell: bash
|
|
155
|
+
env:
|
|
156
|
+
TF_VAR_db_password: ${{ secrets.TF_VAR_DB_PASSWORD }}
|
|
157
|
+
TF_VAR_api_auth_secret: ${{ secrets.TF_VAR_API_AUTH_SECRET }}
|
|
158
|
+
run: |
|
|
159
|
+
set -euo pipefail
|
|
160
|
+
terraform destroy \
|
|
161
|
+
-var-file="stages/${STAGE}.tfvars" \
|
|
162
|
+
-var="lambda_artifact_bucket=${{ steps.lock.outputs.artifact_bucket }}" \
|
|
163
|
+
-var="lambda_artifact_prefix=${{ steps.lock.outputs.lambda_prefix }}" \
|
|
164
|
+
-auto-approve
|
|
165
|
+
|
|
142
166
|
- name: Copy runtime images into customer ECR
|
|
143
|
-
if: ${{ github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' || github.event.inputs.component == 'artifacts' }}
|
|
167
|
+
if: ${{ github.event.inputs.operation == 'deploy' && (github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' || github.event.inputs.component == 'artifacts') }}
|
|
144
168
|
shell: bash
|
|
145
169
|
run: |
|
|
146
170
|
set -euo pipefail
|
|
@@ -153,7 +177,7 @@ jobs:
|
|
|
153
177
|
--ecr-repository-url "$ECR_REPOSITORY_URL"
|
|
154
178
|
|
|
155
179
|
- name: Update AgentCore runtimes
|
|
156
|
-
if: ${{ github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' || github.event.inputs.component == 'artifacts' }}
|
|
180
|
+
if: ${{ github.event.inputs.operation == 'deploy' && (github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' || github.event.inputs.component == 'artifacts') }}
|
|
157
181
|
shell: bash
|
|
158
182
|
run: |
|
|
159
183
|
set -euo pipefail
|
|
@@ -163,7 +187,7 @@ jobs:
|
|
|
163
187
|
--region "${{ vars.AWS_REGION }}"
|
|
164
188
|
|
|
165
189
|
- name: Sync static site bundles
|
|
166
|
-
if: ${{ github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' || github.event.inputs.component == 'artifacts' }}
|
|
190
|
+
if: ${{ github.event.inputs.operation == 'deploy' && (github.event.inputs.component == 'all' || github.event.inputs.component == 'foundation' || github.event.inputs.component == 'artifacts') }}
|
|
167
191
|
shell: bash
|
|
168
192
|
run: |
|
|
169
193
|
set -euo pipefail
|
|
@@ -173,7 +197,7 @@ jobs:
|
|
|
173
197
|
--terraform-dir terraform
|
|
174
198
|
|
|
175
199
|
- name: Apply customer overlay contract
|
|
176
|
-
if: ${{ github.event.inputs.component == 'all' || github.event.inputs.component == 'overlays' }}
|
|
200
|
+
if: ${{ github.event.inputs.operation == 'deploy' && (github.event.inputs.component == 'all' || github.event.inputs.component == 'overlays') }}
|
|
177
201
|
shell: bash
|
|
178
202
|
run: |
|
|
179
203
|
set -euo pipefail
|
|
@@ -187,7 +211,7 @@ jobs:
|
|
|
187
211
|
> "$RELEASE_DIR/overlay-report.json"
|
|
188
212
|
|
|
189
213
|
- name: Run smoke checks
|
|
190
|
-
if: ${{ github.event.inputs.run_smokes == 'true' && (github.event.inputs.component == 'all' || github.event.inputs.component == 'smokes') }}
|
|
214
|
+
if: ${{ github.event.inputs.operation == 'deploy' && github.event.inputs.run_smokes == 'true' && (github.event.inputs.component == 'all' || github.event.inputs.component == 'smokes') }}
|
|
191
215
|
shell: bash
|
|
192
216
|
run: |
|
|
193
217
|
set -euo pipefail
|
|
@@ -206,6 +230,7 @@ jobs:
|
|
|
206
230
|
--work-dir "$RELEASE_DIR" \
|
|
207
231
|
--terraform-dir terraform \
|
|
208
232
|
--stage "$STAGE" \
|
|
233
|
+
--operation "$OPERATION" \
|
|
209
234
|
--component "$COMPONENT" \
|
|
210
235
|
--output "$DEPLOY_SUMMARY_JSON"
|
|
211
236
|
cat "$DEPLOY_SUMMARY_JSON"
|
|
@@ -214,6 +239,7 @@ jobs:
|
|
|
214
239
|
echo
|
|
215
240
|
jq -r '
|
|
216
241
|
"- Stage: " + .stage,
|
|
242
|
+
"- Operation: " + .operation,
|
|
217
243
|
"- Release: " + .release.version,
|
|
218
244
|
"- Component: " + .component,
|
|
219
245
|
"- API: " + (.outputs.api_endpoint // "n/a"),
|
|
@@ -228,7 +254,7 @@ jobs:
|
|
|
228
254
|
- uses: actions/upload-artifact@v4
|
|
229
255
|
if: always()
|
|
230
256
|
with:
|
|
231
|
-
name: thinkwork
|
|
257
|
+
name: thinkwork-${{ github.event.inputs.operation }}-${{ github.event.inputs.stage }}-${{ github.run_id }}
|
|
232
258
|
path: |
|
|
233
259
|
/tmp/thinkwork-release/*.json
|
|
234
260
|
/tmp/thinkwork-release/runtime-images.json
|
|
@@ -16,21 +16,67 @@ bootstrap -> workflow dispatch -> CI deploy -> overlay apply -> smoke summary
|
|
|
16
16
|
Run these commands from an operator machine with temporary customer AWS admin
|
|
17
17
|
access and GitHub admin access to this repository.
|
|
18
18
|
|
|
19
|
-
1.
|
|
19
|
+
1. Log in and verify readiness:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
23
|
-
aws sts get-caller-identity
|
|
22
|
+
thinkwork login
|
|
24
23
|
```
|
|
25
24
|
|
|
26
|
-
2.
|
|
25
|
+
2. Run the top-level bootstrap:
|
|
27
26
|
|
|
28
27
|
```bash
|
|
29
|
-
|
|
30
|
-
thinkwork --version
|
|
28
|
+
thinkwork deploy --bootstrap
|
|
31
29
|
```
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
The CLI prompts for the customer slug, stage, GitHub deployment repo,
|
|
32
|
+
whether to create the repo if it does not exist, and required secret values.
|
|
33
|
+
It creates or reuses the deployment repo checkout, bootstraps dev and prod
|
|
34
|
+
deployment authority, sets GitHub Environment secrets through `gh`, commits
|
|
35
|
+
and pushes managed files, dispatches the dev workflow, waits for the run, and
|
|
36
|
+
prints deploy evidence plus discovered URLs.
|
|
37
|
+
|
|
38
|
+
3. Deploy again after repo or overlay changes:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
thinkwork deploy
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Run this from inside the generated deployment repo. From another directory,
|
|
45
|
+
pass `--customer {{CUSTOMER_SLUG}} --stage dev`.
|
|
46
|
+
|
|
47
|
+
4. Log in to the deployed stack:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
thinkwork login --stage dev --region {{REGION}}
|
|
51
|
+
thinkwork me --stage dev --region {{REGION}}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
5. Remove a stage:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
thinkwork destroy
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Run this from inside the generated deployment repo. The CLI prompts for the
|
|
61
|
+
stage, confirms the destructive action, dispatches the customer repo workflow
|
|
62
|
+
with `operation=destroy`, waits for GitHub Actions, and reports the run URL.
|
|
63
|
+
It removes the selected stage stack while preserving customer-wide bootstrap
|
|
64
|
+
resources such as the deployment repo, Terraform state bucket, artifact
|
|
65
|
+
bucket, OIDC provider, and GitHub Environment configuration.
|
|
66
|
+
|
|
67
|
+
## Manual Fallback
|
|
68
|
+
|
|
69
|
+
Use these commands only when troubleshooting or when you need to split the
|
|
70
|
+
one-line flow into explicit steps.
|
|
71
|
+
|
|
72
|
+
1. Confirm access:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
gh auth status
|
|
76
|
+
aws sts get-caller-identity
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
2. Choose the release and checksum:
|
|
34
80
|
|
|
35
81
|
```bash
|
|
36
82
|
VERSION="$(thinkwork --version)"
|
|
@@ -39,7 +85,7 @@ access and GitHub admin access to this repository.
|
|
|
39
85
|
MANIFEST_SHA256="$(curl -fsSL "$MANIFEST_URL" | shasum -a 256 | awk '{print $1}')"
|
|
40
86
|
```
|
|
41
87
|
|
|
42
|
-
|
|
88
|
+
3. Bootstrap AWS trust, GitHub Environments, and managed repo files:
|
|
43
89
|
|
|
44
90
|
```bash
|
|
45
91
|
thinkwork enterprise bootstrap . \
|
|
@@ -54,91 +100,56 @@ access and GitHub admin access to this repository.
|
|
|
54
100
|
--yes
|
|
55
101
|
```
|
|
56
102
|
|
|
57
|
-
|
|
103
|
+
4. Commit, push, and dispatch manually:
|
|
58
104
|
|
|
59
105
|
```bash
|
|
60
106
|
git add .
|
|
61
107
|
git commit -m "chore: bootstrap ThinkWork deployment repo"
|
|
62
108
|
git push
|
|
109
|
+
|
|
110
|
+
gh workflow run deploy.yml \
|
|
111
|
+
--repo <github-owner>/<github-repo> \
|
|
112
|
+
-f stage=dev \
|
|
113
|
+
-f component=all \
|
|
114
|
+
-f run_smokes=true
|
|
63
115
|
```
|
|
64
116
|
|
|
65
|
-
|
|
117
|
+
5. Watch the run and download evidence:
|
|
66
118
|
|
|
67
119
|
```bash
|
|
68
|
-
gh
|
|
69
|
-
|
|
70
|
-
--env dev \
|
|
71
|
-
--body "$DEV_DB_PASSWORD"
|
|
120
|
+
RUN_ID="$(gh run list --repo <github-owner>/<github-repo> --workflow deploy.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
|
|
121
|
+
gh run watch "$RUN_ID" --repo <github-owner>/<github-repo> --exit-status
|
|
72
122
|
|
|
73
|
-
|
|
123
|
+
mkdir -p "deploy-artifacts/dev-${RUN_ID}"
|
|
124
|
+
gh run download "$RUN_ID" \
|
|
74
125
|
--repo <github-owner>/<github-repo> \
|
|
75
|
-
--
|
|
76
|
-
--
|
|
77
|
-
```
|
|
126
|
+
--name "thinkwork-deploy-dev-${RUN_ID}" \
|
|
127
|
+
--dir "deploy-artifacts/dev-${RUN_ID}"
|
|
78
128
|
|
|
79
|
-
|
|
129
|
+
jq . "deploy-artifacts/dev-${RUN_ID}/deploy-summary.json"
|
|
130
|
+
jq . "deploy-artifacts/dev-${RUN_ID}/smoke-summary.json"
|
|
131
|
+
```
|
|
80
132
|
|
|
81
|
-
|
|
133
|
+
6. Deploy production after the `prod` GitHub Environment has required
|
|
134
|
+
approvals and secrets:
|
|
82
135
|
|
|
83
136
|
```bash
|
|
84
|
-
|
|
85
|
-
gh variable list --repo <github-owner>/<github-repo> --env prod
|
|
137
|
+
thinkwork deploy --customer {{CUSTOMER_SLUG}} --stage prod --component all
|
|
86
138
|
```
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
`THINKWORK_ARTIFACT_BUCKET`.
|
|
90
|
-
|
|
91
|
-
8. Review stage config:
|
|
92
|
-
- `terraform/stages/dev.tfvars`
|
|
93
|
-
- `terraform/stages/prod.tfvars`
|
|
94
|
-
- `customer/deployment.json`
|
|
95
|
-
|
|
96
|
-
Do not commit plaintext secrets to `terraform/stages/*.tfvars`,
|
|
97
|
-
`customer/`, `.env`, or this runbook.
|
|
98
|
-
|
|
99
|
-
9. Dispatch the first deploy:
|
|
140
|
+
7. Destroy a stage manually:
|
|
100
141
|
|
|
101
142
|
```bash
|
|
102
143
|
gh workflow run deploy.yml \
|
|
103
144
|
--repo <github-owner>/<github-repo> \
|
|
145
|
+
-f operation=destroy \
|
|
104
146
|
-f stage=dev \
|
|
105
147
|
-f component=all \
|
|
106
|
-
-f run_smokes=
|
|
148
|
+
-f run_smokes=false
|
|
107
149
|
```
|
|
108
150
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
RUN_ID="$(gh run list --repo <github-owner>/<github-repo> --workflow deploy.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
|
|
113
|
-
gh run watch "$RUN_ID" --repo <github-owner>/<github-repo> --exit-status
|
|
114
|
-
|
|
115
|
-
mkdir -p "deploy-artifacts/dev-${RUN_ID}"
|
|
116
|
-
gh run download "$RUN_ID" \
|
|
117
|
-
--repo <github-owner>/<github-repo> \
|
|
118
|
-
--name "thinkwork-deploy-dev-${RUN_ID}" \
|
|
119
|
-
--dir "deploy-artifacts/dev-${RUN_ID}"
|
|
120
|
-
|
|
121
|
-
jq . "deploy-artifacts/dev-${RUN_ID}/deploy-summary.json"
|
|
122
|
-
jq . "deploy-artifacts/dev-${RUN_ID}/smoke-summary.json"
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
11. Log in to the deployed stack:
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
thinkwork login --stage dev --region {{REGION}}
|
|
129
|
-
thinkwork me --stage dev --region {{REGION}}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
12. Deploy production after the `prod` GitHub Environment has required
|
|
133
|
-
approvals and secrets:
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
gh workflow run deploy.yml \
|
|
137
|
-
--repo <github-owner>/<github-repo> \
|
|
138
|
-
-f stage=prod \
|
|
139
|
-
-f component=all \
|
|
140
|
-
-f run_smokes=true
|
|
141
|
-
```
|
|
151
|
+
Prefer `thinkwork destroy`; use direct `gh workflow run` only as a
|
|
152
|
+
troubleshooting fallback.
|
|
142
153
|
|
|
143
154
|
## Workflow Components
|
|
144
155
|
|
|
@@ -150,6 +161,10 @@ access and GitHub admin access to this repository.
|
|
|
150
161
|
| `overlays` | Apply only `customer/` evals, skills, workspace defaults, seeds, and branding records. |
|
|
151
162
|
| `smokes` | Run smoke checks against an already-deployed stage. |
|
|
152
163
|
|
|
164
|
+
Set workflow `operation=destroy` to remove the selected stage stack. Destroy
|
|
165
|
+
always uses Terraform for the whole stage; component-specific destroy is not
|
|
166
|
+
supported.
|
|
167
|
+
|
|
153
168
|
## Overlay-Only Changes
|
|
154
169
|
|
|
155
170
|
1. Edit files under `customer/`.
|
|
@@ -170,11 +185,7 @@ access and GitHub admin access to this repository.
|
|
|
170
185
|
4. Dispatch:
|
|
171
186
|
|
|
172
187
|
```bash
|
|
173
|
-
|
|
174
|
-
--repo <github-owner>/<github-repo> \
|
|
175
|
-
-f stage=dev \
|
|
176
|
-
-f component=overlays \
|
|
177
|
-
-f run_smokes=false
|
|
188
|
+
thinkwork deploy --customer {{CUSTOMER_SLUG}} --stage dev --component overlays --no-run-smokes
|
|
178
189
|
```
|
|
179
190
|
|
|
180
191
|
## Release Upgrades
|
|
@@ -209,11 +220,7 @@ access and GitHub admin access to this repository.
|
|
|
209
220
|
git commit -m "chore: bump ThinkWork to ${RELEASE}"
|
|
210
221
|
git push
|
|
211
222
|
|
|
212
|
-
|
|
213
|
-
--repo <github-owner>/<github-repo> \
|
|
214
|
-
-f stage=dev \
|
|
215
|
-
-f component=all \
|
|
216
|
-
-f run_smokes=true
|
|
223
|
+
thinkwork deploy --customer {{CUSTOMER_SLUG}} --stage dev --component all
|
|
217
224
|
```
|
|
218
225
|
|
|
219
226
|
3. Deploy to production after dev passes.
|
|
@@ -460,6 +460,7 @@ async function writeSummary() {
|
|
|
460
460
|
const terraformDir = required("--terraform-dir");
|
|
461
461
|
const summary = {
|
|
462
462
|
stage: required("--stage"),
|
|
463
|
+
operation: optional("--operation") ?? "deploy",
|
|
463
464
|
component: required("--component"),
|
|
464
465
|
release: manifest.release,
|
|
465
466
|
artifacts: await readJson(join(workDir, "prepared-artifacts.json"), null),
|
|
@@ -332,6 +332,7 @@ locals {
|
|
|
332
332
|
www_dns_enabled = var.www_domain != "" && var.cloudflare_zone_id != ""
|
|
333
333
|
docs_domain = var.www_domain != "" ? "docs.${var.www_domain}" : ""
|
|
334
334
|
admin_domain = var.www_domain != "" ? "admin.${var.www_domain}" : ""
|
|
335
|
+
app_domain = var.www_domain != "" ? "app.${var.www_domain}" : ""
|
|
335
336
|
computer_domain = var.www_domain != "" ? "computer.${var.www_domain}" : ""
|
|
336
337
|
sandbox_domain = var.www_domain != "" ? "sandbox.${var.www_domain}" : ""
|
|
337
338
|
api_domain = var.www_domain != "" ? "api.${var.www_domain}" : ""
|
|
@@ -417,8 +418,14 @@ module "thinkwork" {
|
|
|
417
418
|
admin_domain = local.www_dns_enabled ? local.admin_domain : ""
|
|
418
419
|
admin_certificate_arn = local.www_dns_enabled ? module.www_dns[0].certificate_arn : ""
|
|
419
420
|
|
|
420
|
-
#
|
|
421
|
-
# Same ACM cert covers it via the
|
|
421
|
+
# End-user app custom domain (derived from www_domain — app.<apex>).
|
|
422
|
+
# Same ACM cert covers it via the include_app SAN gate on www_dns.
|
|
423
|
+
app_domain = local.www_dns_enabled ? local.app_domain : ""
|
|
424
|
+
app_certificate_arn = local.www_dns_enabled ? module.www_dns[0].certificate_arn : ""
|
|
425
|
+
|
|
426
|
+
# Deprecated compatibility host (derived from www_domain — computer.<apex>).
|
|
427
|
+
# www-dns redirects this host to app.<apex> while old bookmarks/OAuth
|
|
428
|
+
# callbacks are still in circulation.
|
|
422
429
|
computer_domain = local.www_dns_enabled ? local.computer_domain : ""
|
|
423
430
|
computer_certificate_arn = local.www_dns_enabled ? module.www_dns[0].certificate_arn : ""
|
|
424
431
|
|
|
@@ -427,7 +434,7 @@ module "thinkwork" {
|
|
|
427
434
|
# the shared apex/www/docs/admin/computer/api certificate.
|
|
428
435
|
computer_sandbox_domain = local.www_dns_enabled ? local.sandbox_domain : ""
|
|
429
436
|
computer_sandbox_certificate_arn = local.www_dns_enabled ? aws_acm_certificate_validation.computer_sandbox[0].certificate_arn : ""
|
|
430
|
-
computer_sandbox_allowed_parent_origins = local.www_dns_enabled ? "https://${local.
|
|
437
|
+
computer_sandbox_allowed_parent_origins = local.www_dns_enabled ? "https://${local.app_domain},https://${local.admin_domain}" : ""
|
|
431
438
|
|
|
432
439
|
# SES inbound email subdomain (delegated Route53 subzone).
|
|
433
440
|
ses_inbound_domain = var.ses_inbound_domain
|
|
@@ -489,15 +496,14 @@ module "www_dns" {
|
|
|
489
496
|
include_admin = true
|
|
490
497
|
admin_cloudfront_domain_name = module.thinkwork.admin_distribution_domain
|
|
491
498
|
|
|
492
|
-
#
|
|
493
|
-
#
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
#
|
|
498
|
-
#
|
|
499
|
-
include_computer
|
|
500
|
-
computer_cloudfront_domain_name = module.thinkwork.computer_distribution_domain
|
|
499
|
+
# End-user app: canonical app.<apex> host. The underlying source package is
|
|
500
|
+
# still apps/computer for compatibility.
|
|
501
|
+
include_app = true
|
|
502
|
+
app_cloudfront_domain_name = module.thinkwork.app_distribution_domain
|
|
503
|
+
|
|
504
|
+
# Compatibility host: computer.<apex> redirects to app.<apex> while old
|
|
505
|
+
# bookmarks and OAuth callbacks are still in circulation.
|
|
506
|
+
include_computer = true
|
|
501
507
|
|
|
502
508
|
# Computer iframe sandbox: same cycle-avoidance pattern as computer.
|
|
503
509
|
include_computer_sandbox = true
|
|
@@ -652,18 +658,33 @@ output "admin_bucket_name" {
|
|
|
652
658
|
value = module.thinkwork.admin_bucket_name
|
|
653
659
|
}
|
|
654
660
|
|
|
661
|
+
output "app_url" {
|
|
662
|
+
description = "End-user app URL"
|
|
663
|
+
value = local.www_dns_enabled ? "https://${local.app_domain}" : "https://${module.thinkwork.app_distribution_domain}"
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
output "app_distribution_id" {
|
|
667
|
+
description = "CloudFront distribution ID for app (for cache invalidation)"
|
|
668
|
+
value = module.thinkwork.app_distribution_id
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
output "app_bucket_name" {
|
|
672
|
+
description = "S3 bucket for app assets"
|
|
673
|
+
value = module.thinkwork.app_bucket_name
|
|
674
|
+
}
|
|
675
|
+
|
|
655
676
|
output "computer_url" {
|
|
656
|
-
description = "
|
|
657
|
-
value = local.www_dns_enabled ? "https://${local.
|
|
677
|
+
description = "Deprecated alias for app_url"
|
|
678
|
+
value = local.www_dns_enabled ? "https://${local.app_domain}" : "https://${module.thinkwork.app_distribution_domain}"
|
|
658
679
|
}
|
|
659
680
|
|
|
660
681
|
output "computer_distribution_id" {
|
|
661
|
-
description = "
|
|
682
|
+
description = "Deprecated alias for app_distribution_id"
|
|
662
683
|
value = module.thinkwork.computer_distribution_id
|
|
663
684
|
}
|
|
664
685
|
|
|
665
686
|
output "computer_bucket_name" {
|
|
666
|
-
description = "
|
|
687
|
+
description = "Deprecated alias for app_bucket_name"
|
|
667
688
|
value = module.thinkwork.computer_bucket_name
|
|
668
689
|
}
|
|
669
690
|
|
|
@@ -27,6 +27,12 @@ variable "custom_domain" {
|
|
|
27
27
|
default = ""
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
variable "custom_domain_aliases" {
|
|
31
|
+
description = "Additional custom domains attached to the same CloudFront distribution. Useful for compatibility hosts that redirect at viewer-request time."
|
|
32
|
+
type = list(string)
|
|
33
|
+
default = []
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
variable "certificate_arn" {
|
|
31
37
|
description = "ACM certificate ARN for the custom domain (us-east-1, required for CloudFront)"
|
|
32
38
|
type = string
|
|
@@ -39,6 +45,12 @@ variable "is_spa" {
|
|
|
39
45
|
default = false
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
variable "viewer_request_function_code" {
|
|
49
|
+
description = "Optional CloudFront Function code to run on viewer-request. When set, it replaces the default non-SPA directory rewrite association."
|
|
50
|
+
type = string
|
|
51
|
+
default = ""
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
# ---------------------------------------------------------------------------
|
|
43
55
|
# Response-headers policy (optional, plan-012 U3)
|
|
44
56
|
#
|
|
@@ -111,6 +123,17 @@ locals {
|
|
|
111
123
|
: ""
|
|
112
124
|
)
|
|
113
125
|
)
|
|
126
|
+
|
|
127
|
+
viewer_request_function_enabled = var.viewer_request_function_code != ""
|
|
128
|
+
viewer_request_function_arn = (
|
|
129
|
+
local.viewer_request_function_enabled
|
|
130
|
+
? aws_cloudfront_function.viewer_request[0].arn
|
|
131
|
+
: (!var.is_spa ? aws_cloudfront_function.rewrite.arn : "")
|
|
132
|
+
)
|
|
133
|
+
custom_domain_aliases = distinct(compact(concat(
|
|
134
|
+
var.custom_domain != "" ? [var.custom_domain] : [],
|
|
135
|
+
var.custom_domain_aliases,
|
|
136
|
+
)))
|
|
114
137
|
}
|
|
115
138
|
|
|
116
139
|
check "policy_inputs_are_mutually_exclusive" {
|
|
@@ -240,6 +263,14 @@ resource "aws_cloudfront_function" "rewrite" {
|
|
|
240
263
|
EOF
|
|
241
264
|
}
|
|
242
265
|
|
|
266
|
+
resource "aws_cloudfront_function" "viewer_request" {
|
|
267
|
+
count = local.viewer_request_function_enabled ? 1 : 0
|
|
268
|
+
name = "thinkwork-${var.stage}-${var.site_name}-viewer-request"
|
|
269
|
+
runtime = "cloudfront-js-2.0"
|
|
270
|
+
publish = true
|
|
271
|
+
code = var.viewer_request_function_code
|
|
272
|
+
}
|
|
273
|
+
|
|
243
274
|
locals {
|
|
244
275
|
# For SPAs, 403/404 from S3 means "not a real asset" — serve index.html with
|
|
245
276
|
# a 200 so the client router can resolve the route. For directory-style
|
|
@@ -260,7 +291,7 @@ locals {
|
|
|
260
291
|
resource "aws_cloudfront_distribution" "site" {
|
|
261
292
|
enabled = true
|
|
262
293
|
default_root_object = "index.html"
|
|
263
|
-
aliases =
|
|
294
|
+
aliases = local.custom_domain_aliases
|
|
264
295
|
price_class = "PriceClass_100"
|
|
265
296
|
|
|
266
297
|
origin {
|
|
@@ -278,10 +309,10 @@ resource "aws_cloudfront_distribution" "site" {
|
|
|
278
309
|
response_headers_policy_id = local.effective_response_headers_policy_id != "" ? local.effective_response_headers_policy_id : null
|
|
279
310
|
|
|
280
311
|
dynamic "function_association" {
|
|
281
|
-
for_each =
|
|
312
|
+
for_each = local.viewer_request_function_arn != "" ? [1] : []
|
|
282
313
|
content {
|
|
283
314
|
event_type = "viewer-request"
|
|
284
|
-
function_arn =
|
|
315
|
+
function_arn = local.viewer_request_function_arn
|
|
285
316
|
}
|
|
286
317
|
}
|
|
287
318
|
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Responsibilities:
|
|
5
5
|
# 1. ACM certificate in us-east-1 covering apex + www
|
|
6
|
-
# (+ optional docs + optional admin).
|
|
6
|
+
# (+ optional docs + optional admin + optional app + optional computer).
|
|
7
7
|
# 2. Cloudflare DNS records for ACM DNS validation.
|
|
8
8
|
# 3. Apex CNAME in Cloudflare → primary CloudFront distribution (DNS-only).
|
|
9
9
|
# 4. Second CloudFront distribution fronting an S3 website-redirect bucket
|
|
10
10
|
# that 301s www.<domain> → https://<domain>, plus its Cloudflare CNAME.
|
|
11
11
|
# 5. Optional docs.<domain> CNAME → docs CloudFront distribution.
|
|
12
12
|
# 6. Optional admin.<domain> CNAME → admin CloudFront distribution.
|
|
13
|
+
# 7. Optional app.<domain> CNAME → end-user app CloudFront distribution.
|
|
14
|
+
# 8. Optional computer.<domain> → app.<domain> 301 compatibility redirect.
|
|
13
15
|
#
|
|
14
16
|
# Cloudflare records MUST be DNS-only (grey cloud). CloudFront terminates TLS
|
|
15
17
|
# with the ACM cert and needs the real Host header.
|
|
@@ -33,6 +35,7 @@ locals {
|
|
|
33
35
|
www = "www.${var.domain}"
|
|
34
36
|
docs = "docs.${var.domain}"
|
|
35
37
|
admin = "admin.${var.domain}"
|
|
38
|
+
app = "app.${var.domain}"
|
|
36
39
|
computer = "computer.${var.domain}"
|
|
37
40
|
sandbox = "sandbox.${var.domain}"
|
|
38
41
|
api = "api.${var.domain}"
|
|
@@ -49,17 +52,20 @@ locals {
|
|
|
49
52
|
[local.www],
|
|
50
53
|
var.include_docs ? [local.docs] : [],
|
|
51
54
|
var.include_admin ? [local.admin] : [],
|
|
55
|
+
var.include_app ? [local.app] : [],
|
|
52
56
|
var.include_computer ? [local.computer] : [],
|
|
53
57
|
var.include_api ? [local.api] : [],
|
|
54
58
|
)
|
|
55
59
|
|
|
56
60
|
# Existing CNAME records stay gated on non-empty targets because their target
|
|
57
61
|
# outputs are already known in the deployed stack. Newly bootstrapped records
|
|
58
|
-
# such as
|
|
59
|
-
# can plan the resource count before the new CloudFront
|
|
62
|
+
# such as app/computer compatibility/sandbox CNAMEs must gate only on explicit
|
|
63
|
+
# booleans so Terraform can plan the resource count before the new CloudFront
|
|
64
|
+
# distribution exists.
|
|
60
65
|
create_docs_record = var.include_docs && var.docs_cloudfront_domain_name != ""
|
|
61
66
|
create_admin_record = var.include_admin && var.admin_cloudfront_domain_name != ""
|
|
62
|
-
|
|
67
|
+
create_app_record = var.include_app
|
|
68
|
+
create_computer_record = var.include_computer
|
|
63
69
|
create_api_record = var.include_api && var.api_gateway_id != ""
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -267,7 +273,28 @@ resource "cloudflare_record" "admin" {
|
|
|
267
273
|
}
|
|
268
274
|
|
|
269
275
|
################################################################################
|
|
270
|
-
#
|
|
276
|
+
# app.<domain> → end-user app CloudFront distribution (optional)
|
|
277
|
+
################################################################################
|
|
278
|
+
|
|
279
|
+
resource "cloudflare_record" "app" {
|
|
280
|
+
count = local.create_app_record ? 1 : 0
|
|
281
|
+
|
|
282
|
+
zone_id = var.cloudflare_zone_id
|
|
283
|
+
name = local.app
|
|
284
|
+
content = var.app_cloudfront_domain_name
|
|
285
|
+
type = "CNAME"
|
|
286
|
+
ttl = 300
|
|
287
|
+
proxied = false
|
|
288
|
+
comment = "thinkwork-${var.stage} app → CloudFront"
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
################################################################################
|
|
292
|
+
# computer.<domain> → compatibility target (optional)
|
|
293
|
+
#
|
|
294
|
+
# With include_app=true, this alias points at the same app distribution as
|
|
295
|
+
# app.<domain>; that distribution's viewer-request function performs the 301
|
|
296
|
+
# to app.<domain>. Legacy deployments without include_app keep the old direct
|
|
297
|
+
# CNAME behavior.
|
|
271
298
|
################################################################################
|
|
272
299
|
|
|
273
300
|
resource "cloudflare_record" "computer" {
|
|
@@ -275,11 +302,11 @@ resource "cloudflare_record" "computer" {
|
|
|
275
302
|
|
|
276
303
|
zone_id = var.cloudflare_zone_id
|
|
277
304
|
name = local.computer
|
|
278
|
-
content = var.computer_cloudfront_domain_name
|
|
305
|
+
content = var.include_app ? var.app_cloudfront_domain_name : var.computer_cloudfront_domain_name
|
|
279
306
|
type = "CNAME"
|
|
280
307
|
ttl = 300
|
|
281
308
|
proxied = false
|
|
282
|
-
comment = "thinkwork-${var.stage} computer → CloudFront"
|
|
309
|
+
comment = var.include_app ? "thinkwork-${var.stage} computer → app compatibility alias" : "thinkwork-${var.stage} computer → CloudFront"
|
|
283
310
|
}
|
|
284
311
|
|
|
285
312
|
################################################################################
|