dwe-core 0.1.0a1__tar.gz
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.
- dwe_core-0.1.0a1/PKG-INFO +729 -0
- dwe_core-0.1.0a1/README.md +708 -0
- dwe_core-0.1.0a1/dwe/__init__.py +0 -0
- dwe_core-0.1.0a1/dwe/cli.py +323 -0
- dwe_core-0.1.0a1/dwe/git_ops.py +68 -0
- dwe_core-0.1.0a1/dwe/registry.py +20 -0
- dwe_core-0.1.0a1/dwe/secrets.py +64 -0
- dwe_core-0.1.0a1/dwe/state.py +40 -0
- dwe_core-0.1.0a1/pyproject.toml +23 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dwe-core
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: DWE CLI - Data Warehouse Ecosystem Orchestrator
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Dist: PyGithub (>=2.1.1)
|
|
13
|
+
Requires-Dist: copier (>=9.0.0)
|
|
14
|
+
Requires-Dist: gitpython (>=3.1.40)
|
|
15
|
+
Requires-Dist: jinja2 (>=3.1.2)
|
|
16
|
+
Requires-Dist: python-gitlab (>=4.4.0)
|
|
17
|
+
Requires-Dist: rich (>=13.7.0)
|
|
18
|
+
Requires-Dist: typer[all] (>=0.9.0)
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# dwe-core
|
|
22
|
+
|
|
23
|
+
The **DWE CLI** (`dwe`) is the orchestration brain of the Data Warehouse Ecosystem. It takes a blank or existing client Git repository and injects a fully working **Adapter** — infrastructure, application config, CI/CD pipelines, and local dev commands — in a single command.
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
dwe create-service test_adapter --git-repo https://github.com/client/repo --envs dev --envs prod
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Internally this does:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
1. Clone GitPython clones the client repo to a temp directory
|
|
35
|
+
2. Hydrate Copier renders the adapter template into the clone
|
|
36
|
+
3. State CLI writes dwe-state.json
|
|
37
|
+
4. CI/CD CLI renders per-environment GitHub Actions / GitLab CI files
|
|
38
|
+
5. Branch initial-commit branch is created and committed
|
|
39
|
+
6. Env branches dev, prod branches are created from initial-commit
|
|
40
|
+
7. Push All branches are pushed to the remote
|
|
41
|
+
8. Secrets GitHub/GitLab API uploads secrets to the repository settings
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The result is a client repo that already has working infrastructure code, a `justfile` with `just up` / `just deploy-prod`, and CI/CD that deploys to the right environment when you push to its branch.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install poetry # if not already installed
|
|
52
|
+
poetry install # from dwe-core source (creates venv, installs deps)
|
|
53
|
+
# or once published:
|
|
54
|
+
pip install dwe-core
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Verify:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
dwe --help
|
|
61
|
+
dwe list-adapters
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Commands
|
|
67
|
+
|
|
68
|
+
### `dwe create-service`
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
dwe create-service <adapter_name> \
|
|
72
|
+
--git-repo <url> \
|
|
73
|
+
[--envs <name>]... \ # default: development, main
|
|
74
|
+
[--secrets <json>] \ # e.g. '{"AWS_KEY":"abc"}'
|
|
75
|
+
[--tag <version>] \ # adapter git tag, e.g. v1.2.0
|
|
76
|
+
[--token <api-token>] \ # or set GITHUB_TOKEN / GITLAB_TOKEN
|
|
77
|
+
[--aws-region <region>] \
|
|
78
|
+
[--instance-type <type>] \
|
|
79
|
+
[--clone-dir <path>] # default: temp dir
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Example — full run:**
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
export GITHUB_TOKEN=ghp_xxxx
|
|
86
|
+
|
|
87
|
+
dwe create-service test_adapter \
|
|
88
|
+
--git-repo https://github.com/acme/data-platform \
|
|
89
|
+
--envs development \
|
|
90
|
+
--envs staging \
|
|
91
|
+
--envs main \
|
|
92
|
+
--secrets '{"PULUMI_ACCESS_TOKEN":"pul-xxx","AWS_ACCESS_KEY_ID":"AKI...","AWS_SECRET_ACCESS_KEY":"..."}' \
|
|
93
|
+
--tag v1.0.0 \
|
|
94
|
+
--aws-region eu-west-1 \
|
|
95
|
+
--instance-type t3.small
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
After this runs, the `data-platform` repo has:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
.github/workflows/
|
|
102
|
+
deploy-development.yaml
|
|
103
|
+
deploy-staging.yaml
|
|
104
|
+
deploy-main.yaml
|
|
105
|
+
blueprint/
|
|
106
|
+
html/index.html
|
|
107
|
+
instance-setup.sh
|
|
108
|
+
docker-compose.yml
|
|
109
|
+
docker-compose.prod.yml
|
|
110
|
+
.env.example
|
|
111
|
+
justfile
|
|
112
|
+
infrastructure/
|
|
113
|
+
__main__.py <- project_name, instance_type already substituted
|
|
114
|
+
Pulumi.yaml
|
|
115
|
+
requirements.txt
|
|
116
|
+
dwe-state.json
|
|
117
|
+
.copier-answers.yml <- Copier's internal state (enables future updates)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `dwe update-service`
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
dwe update-service <adapter_name> <local_path> [--tag <version>]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Example:**
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
dwe update-service test_adapter ./data-platform --tag v1.2.0
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Internally:
|
|
133
|
+
1. Reads `dwe-state.json` and validates the adapter name matches
|
|
134
|
+
2. Creates a branch `dwe-update-20260322-1.2.0`
|
|
135
|
+
3. Runs `copier.run_update()` — **smart merge** that preserves your customisations
|
|
136
|
+
4. Updates `dwe-state.json` with the new version
|
|
137
|
+
|
|
138
|
+
Review the diff on the branch, then merge into your environment branches to trigger deployments.
|
|
139
|
+
|
|
140
|
+
### `dwe list-adapters`
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
dwe list-adapters
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Shows all adapters registered in `adapters.json`.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Adapter Registry (`adapters.json`)
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"test_adapter": {
|
|
155
|
+
"path": "/absolute/path/to/dwe_test_adapter",
|
|
156
|
+
"type": "local",
|
|
157
|
+
"description": "Test adapter: AWS EC2 instance via Pulumi"
|
|
158
|
+
},
|
|
159
|
+
"superset_adapter": {
|
|
160
|
+
"url": "https://github.com/hipposys/dwe-superset-adapter",
|
|
161
|
+
"type": "git",
|
|
162
|
+
"description": "Apache Superset on ECS"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## How to Define a New Adapter
|
|
170
|
+
|
|
171
|
+
An adapter is a **real, runnable project** that also serves as a Copier template. The guiding principle:
|
|
172
|
+
|
|
173
|
+
> **The adapter must work locally as-is.** A developer should be able to `git clone` the adapter, run `just up`, and have a working service — without running the DWE CLI at all.
|
|
174
|
+
|
|
175
|
+
### Step 1: Create the adapter repository
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
mkdir my_adapter && cd my_adapter
|
|
179
|
+
git init
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Step 2: Build a working application first
|
|
183
|
+
|
|
184
|
+
Build your service as a real project before adding any template variables. For example, if you're building a Superset adapter:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Make it work locally first
|
|
188
|
+
docker compose up # verify it runs
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Only once everything works locally do you introduce `{{ variables }}`.
|
|
192
|
+
|
|
193
|
+
### Step 3: Directory structure
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
my_adapter/
|
|
197
|
+
├── copier.yml # Copier config + question definitions
|
|
198
|
+
│
|
|
199
|
+
├── docker-compose.yml # Real, runnable. Uses ${ENV_VAR:-default} for runtime values.
|
|
200
|
+
├── docker-compose.prod.yml # Production overrides (restart policy, logging)
|
|
201
|
+
├── .env.example # Template for secrets — committed; .env is git-ignored
|
|
202
|
+
├── .gitignore
|
|
203
|
+
│
|
|
204
|
+
├── justfile # Dev commands (just up, just deploy-prod, just infra-up)
|
|
205
|
+
│
|
|
206
|
+
├── blueprint/ # Application-level config files
|
|
207
|
+
│ ├── html/ # or nginx.conf, superset_config.py, etc.
|
|
208
|
+
│ └── instance-setup.sh # EC2 user-data bootstrap script
|
|
209
|
+
│
|
|
210
|
+
├── infrastructure/ # Pulumi IaC — only files here use .jinja
|
|
211
|
+
│ ├── __main__.py.jinja # <- .jinja because it embeds {{ project_name }}
|
|
212
|
+
│ ├── Pulumi.yaml.jinja # <- .jinja because it embeds {{ project_name }}
|
|
213
|
+
│ └── requirements.txt
|
|
214
|
+
│
|
|
215
|
+
└── ci-templates/ # Jinja2 templates rendered by the CLI (not Copier)
|
|
216
|
+
└── deploy.yaml # Uses {{ ENV_NAME }}, {{ AWS_REGION }}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Step 4: Write `copier.yml`
|
|
220
|
+
|
|
221
|
+
`copier.yml` controls how Copier processes the adapter. Key settings:
|
|
222
|
+
|
|
223
|
+
```yaml
|
|
224
|
+
_templates_suffix: .jinja # ONLY files ending in .jinja are treated as templates
|
|
225
|
+
# Everything else is copied verbatim
|
|
226
|
+
|
|
227
|
+
_exclude:
|
|
228
|
+
- copier.yml # Don't copy Copier's own config
|
|
229
|
+
- ci-templates # CLI handles this separately
|
|
230
|
+
- README.md # Adapter's README is not for client repos
|
|
231
|
+
- .git
|
|
232
|
+
- .env # Never copy actual secrets
|
|
233
|
+
- __pycache__
|
|
234
|
+
- "*.pyc"
|
|
235
|
+
|
|
236
|
+
_skip_if_exists:
|
|
237
|
+
- .env.example # Preserve user customisations on updates
|
|
238
|
+
|
|
239
|
+
# Questions (answered non-interactively by the dwe CLI):
|
|
240
|
+
project_name:
|
|
241
|
+
type: str
|
|
242
|
+
help: "Client project name (used for cloud resource naming)"
|
|
243
|
+
|
|
244
|
+
adapter_name:
|
|
245
|
+
type: str
|
|
246
|
+
default: "my_adapter"
|
|
247
|
+
when: false # always set programmatically
|
|
248
|
+
|
|
249
|
+
adapter_version:
|
|
250
|
+
type: str
|
|
251
|
+
default: "v1.0.0"
|
|
252
|
+
when: false # always set programmatically
|
|
253
|
+
|
|
254
|
+
environments:
|
|
255
|
+
type: yaml
|
|
256
|
+
default: "[development, main]"
|
|
257
|
+
|
|
258
|
+
aws_region:
|
|
259
|
+
type: str
|
|
260
|
+
default: "us-east-1"
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Step 5: Decide what needs Jinja2
|
|
264
|
+
|
|
265
|
+
Apply this rule: **if the value changes per client, use `{{ variable }}`. If it changes per deployment environment, use a `.env` variable.**
|
|
266
|
+
|
|
267
|
+
| File | Approach | Reason |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| `docker-compose.yml` | `.env` interpolation (`${VAR:-default}`) | Works locally without any substitution; runtime config |
|
|
270
|
+
| `infrastructure/__main__.py` | Jinja2 (`.jinja` extension) | Cloud resource names must be unique per client at provision time |
|
|
271
|
+
| `infrastructure/Pulumi.yaml` | Jinja2 (`.jinja` extension) | Stack name must be unique per client |
|
|
272
|
+
| `justfile` | Verbatim copy (no `.jinja`) | Commands are identical across clients |
|
|
273
|
+
| `blueprint/instance-setup.sh` | Verbatim copy | Generic bootstrap, no client-specific values |
|
|
274
|
+
| `.env.example` | Verbatim copy | Users fill in real values after cloning |
|
|
275
|
+
|
|
276
|
+
**Jinja2 syntax in `.jinja` files:**
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
# infrastructure/__main__.py.jinja
|
|
280
|
+
instance = aws.ec2.Instance(
|
|
281
|
+
"{{ project_name }}-instance", # <- substituted by Copier
|
|
282
|
+
instance_type="{{ instance_type }}",
|
|
283
|
+
...
|
|
284
|
+
)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
After `dwe create-service` this becomes:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
instance = aws.ec2.Instance(
|
|
291
|
+
"acme-data-platform-instance",
|
|
292
|
+
instance_type="t3.small",
|
|
293
|
+
...
|
|
294
|
+
)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Step 6: Write `ci-templates/deploy.yaml`
|
|
298
|
+
|
|
299
|
+
This is a Jinja2 file rendered by the `dwe` CLI (not by Copier) to generate one workflow file per environment. The CLI uses `{@ @}` as variable delimiters (not `{{ }}`), so GitHub Actions `${{ secrets.X }}` syntax passes through **untouched** — no escaping needed.
|
|
300
|
+
|
|
301
|
+
```yaml
|
|
302
|
+
name: Deploy to {@ ENV_NAME @}
|
|
303
|
+
|
|
304
|
+
on:
|
|
305
|
+
push:
|
|
306
|
+
branches:
|
|
307
|
+
- {@ ENV_NAME @}
|
|
308
|
+
pull_request:
|
|
309
|
+
branches:
|
|
310
|
+
- {@ ENV_NAME @}
|
|
311
|
+
|
|
312
|
+
jobs:
|
|
313
|
+
deploy:
|
|
314
|
+
runs-on: ubuntu-latest
|
|
315
|
+
environment: {@ ENV_NAME @}
|
|
316
|
+
steps:
|
|
317
|
+
- uses: actions/checkout@v4
|
|
318
|
+
- name: Deploy
|
|
319
|
+
run: just deploy-prod
|
|
320
|
+
env:
|
|
321
|
+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} # passes through unchanged
|
|
322
|
+
AWS_REGION: {@ AWS_REGION @} # substituted by dwe CLI
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Available variables: `{@ ENV_NAME @}`, `{@ AWS_REGION @}`.
|
|
326
|
+
|
|
327
|
+
### Step 7: Register the adapter
|
|
328
|
+
|
|
329
|
+
Add an entry to `dwe-core/adapters.json`:
|
|
330
|
+
|
|
331
|
+
**Local (development):**
|
|
332
|
+
```json
|
|
333
|
+
{
|
|
334
|
+
"my_adapter": {
|
|
335
|
+
"path": "/absolute/path/to/my_adapter",
|
|
336
|
+
"type": "local",
|
|
337
|
+
"description": "My adapter description"
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Remote Git (production):**
|
|
343
|
+
```json
|
|
344
|
+
{
|
|
345
|
+
"my_adapter": {
|
|
346
|
+
"url": "https://github.com/your-org/my-adapter",
|
|
347
|
+
"type": "git",
|
|
348
|
+
"description": "My adapter description"
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Step 8: Test the adapter
|
|
354
|
+
|
|
355
|
+
**Test locally first (without DWE CLI):**
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
cd my_adapter
|
|
359
|
+
cp .env.example .env
|
|
360
|
+
just up # docker compose up — must work here
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Test Copier rendering in isolation:**
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
pip install copier
|
|
367
|
+
copier copy /path/to/my_adapter /tmp/test-output \
|
|
368
|
+
--data project_name=testproject \
|
|
369
|
+
--data aws_region=us-east-1 \
|
|
370
|
+
--defaults --overwrite --trust
|
|
371
|
+
|
|
372
|
+
# Inspect the output
|
|
373
|
+
ls /tmp/test-output
|
|
374
|
+
cat /tmp/test-output/infrastructure/Pulumi.yaml # should have project_name substituted
|
|
375
|
+
cat /tmp/test-output/docker-compose.yml # should be identical to source
|
|
376
|
+
cd /tmp/test-output && docker compose up # should still work
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Test via dwe CLI:**
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
dwe create-service my_adapter \
|
|
383
|
+
--git-repo https://github.com/test-org/empty-repo \
|
|
384
|
+
--envs development \
|
|
385
|
+
--envs main
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Adapter Versioning and Updates
|
|
391
|
+
|
|
392
|
+
Tag your adapter repository with semantic version tags. The DWE CLI and Copier use these tags for `update-service`:
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
cd my_adapter
|
|
396
|
+
git add -A && git commit -m "feat: add postgres service"
|
|
397
|
+
git tag v1.1.0
|
|
398
|
+
git push origin v1.1.0
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
When a client wants to update:
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
dwe update-service my_adapter ./client-repo --tag v1.1.0
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Copier reads the source URL from `.copier-answers.yml` in the client repo, checks out `v1.1.0`, and runs a 3-way merge. Files the user has customised are preserved where possible; conflicts surface as standard git merge conflicts.
|
|
408
|
+
|
|
409
|
+
**What gets updated:**
|
|
410
|
+
- `infrastructure/` — Pulumi code (Jinja2 re-rendered with new template)
|
|
411
|
+
- `blueprint/` — Application config files
|
|
412
|
+
- `justfile` — Dev commands
|
|
413
|
+
|
|
414
|
+
**What is NOT updated (protected):**
|
|
415
|
+
- `.env.example` — skipped if it already exists (`_skip_if_exists` in `copier.yml`)
|
|
416
|
+
- `.copier-answers.yml` — managed by Copier internally
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## State Files
|
|
421
|
+
|
|
422
|
+
### `dwe-state.json` (DWE-managed)
|
|
423
|
+
|
|
424
|
+
Written by the `dwe` CLI after `copier.run_copy()`. Tracks DWE-specific metadata:
|
|
425
|
+
|
|
426
|
+
```json
|
|
427
|
+
{
|
|
428
|
+
"dwe_version": "1.0.0",
|
|
429
|
+
"adapter": {
|
|
430
|
+
"name": "test_adapter",
|
|
431
|
+
"version": "v1.0.0",
|
|
432
|
+
"last_update": "2026-03-22"
|
|
433
|
+
},
|
|
434
|
+
"environments": ["development", "main"],
|
|
435
|
+
"infrastructure": "pulumi"
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### `.copier-answers.yml` (Copier-managed)
|
|
440
|
+
|
|
441
|
+
Written by Copier. Tracks the template source, version, and question answers. **Do not edit manually.** This is what enables `copier.run_update()` to know where the template came from.
|
|
442
|
+
|
|
443
|
+
```yaml
|
|
444
|
+
# Changes here will be overwritten by copier
|
|
445
|
+
_commit: v1.0.0
|
|
446
|
+
_src_path: /path/to/my_adapter
|
|
447
|
+
project_name: acme-data-platform
|
|
448
|
+
aws_region: eu-west-1
|
|
449
|
+
instance_type: t3.small
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Both files coexist. `dwe-state.json` is for DWE tooling; `.copier-answers.yml` is for Copier's update machinery.
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Developer Workflow After `create-service`
|
|
457
|
+
|
|
458
|
+
Once the client repo is hydrated, the full developer loop is:
|
|
459
|
+
|
|
460
|
+
**1. Local development (laptop):**
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
git clone https://github.com/client/data-platform
|
|
464
|
+
cd data-platform
|
|
465
|
+
cp .env.example .env # fill in local values (no real AWS keys needed)
|
|
466
|
+
just up # docker compose up — app is running at localhost:8080
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**2. Provision cloud infrastructure (once):**
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
# Fill in real AWS keys in .env
|
|
473
|
+
just install-infra # pip install pulumi pulumi-aws
|
|
474
|
+
just infra-preview # see what Pulumi will create
|
|
475
|
+
just infra-up # provision the EC2 instance
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
**3. Deploy to EC2 (SSH into the instance, then):**
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
git clone https://github.com/client/data-platform /srv/app
|
|
482
|
+
cd /srv/app
|
|
483
|
+
cp .env.example .env # fill in production values
|
|
484
|
+
just deploy-prod # docker compose -f ... up -d
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**4. CI/CD (automatic after push):**
|
|
488
|
+
|
|
489
|
+
Pushing to `development` or `main` triggers the corresponding GitHub Actions workflow. See the [CI/CD Workflow Design](#cicd-workflow-design) section below for the full two-path logic.
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## CI/CD Workflow Design
|
|
494
|
+
|
|
495
|
+
The generated CI/CD workflow (`.github/workflows/deploy-{env}.yaml`) implements a **two-path** logic inspired by the Superset production setup. The key insight: infrastructure changes and application changes require completely different responses.
|
|
496
|
+
|
|
497
|
+
### The Two Paths
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
Push to branch
|
|
501
|
+
│
|
|
502
|
+
▼
|
|
503
|
+
Detect changes
|
|
504
|
+
(dorny/paths-filter)
|
|
505
|
+
│
|
|
506
|
+
├─── infrastructure/** changed?
|
|
507
|
+
│ │
|
|
508
|
+
│ ├─ Pull Request → pulumi preview (validate, no apply)
|
|
509
|
+
│ └─ Push → pulumi up --yes (apply infra changes)
|
|
510
|
+
│
|
|
511
|
+
└─── docker-compose / blueprint changed?
|
|
512
|
+
AND infrastructure NOT changed?
|
|
513
|
+
│
|
|
514
|
+
└─ Push → SSM: git pull + just deploy-prod
|
|
515
|
+
(redeploy app on the live EC2 instance)
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
**Why skip deploy when infra also changed?** The `pulumi up` step re-provisions the EC2 instance itself, which already pulls the latest code via its user-data script. Running the app deploy on top of that would be redundant and potentially racy.
|
|
519
|
+
|
|
520
|
+
### Job Summary
|
|
521
|
+
|
|
522
|
+
| Job | Trigger | What it does |
|
|
523
|
+
|---|---|---|
|
|
524
|
+
| `pulumi-preview` | PR, `infrastructure/**` changed | Runs `pulumi preview` — shows what *would* change, no side effects |
|
|
525
|
+
| `pulumi-apply` | Push, `infrastructure/**` changed | Runs `pulumi up --yes` — applies infra changes |
|
|
526
|
+
| `deploy-app` | Push, app files changed, infra NOT changed | AWS SSM command: `git pull && just deploy-prod` on live EC2 |
|
|
527
|
+
|
|
528
|
+
### Required Secrets
|
|
529
|
+
|
|
530
|
+
Set these via `dwe create-service --secrets '{...}'` or manually in GitHub repository settings:
|
|
531
|
+
|
|
532
|
+
| Secret | Description |
|
|
533
|
+
|---|---|
|
|
534
|
+
| `AWS_ACCESS_KEY_ID` | AWS credentials for Pulumi and SSM |
|
|
535
|
+
| `AWS_SECRET_ACCESS_KEY` | AWS credentials |
|
|
536
|
+
| `PULUMI_ACCESS_TOKEN` | Pulumi Cloud token |
|
|
537
|
+
| `PULUMI_CONFIG_PASSPHRASE` | Pulumi stack encryption passphrase |
|
|
538
|
+
| `PULUMI_STACK` | Pulumi stack reference, e.g. `myorg/myproject/development` |
|
|
539
|
+
| `EC2_INSTANCE_ID` | Instance ID from `pulumi stack output instance_id`, e.g. `i-0abc1234` |
|
|
540
|
+
|
|
541
|
+
### SSM Prerequisites
|
|
542
|
+
|
|
543
|
+
The `deploy-app` job uses **AWS Systems Manager (SSM)** instead of SSH — no port 22, no SSH key stored as a secret.
|
|
544
|
+
|
|
545
|
+
To enable SSM on the EC2 instance:
|
|
546
|
+
|
|
547
|
+
**1. IAM instance profile** — attach a role with these policies to the EC2:
|
|
548
|
+
```json
|
|
549
|
+
{
|
|
550
|
+
"Effect": "Allow",
|
|
551
|
+
"Action": [
|
|
552
|
+
"ssm:UpdateInstanceInformation",
|
|
553
|
+
"ssmmessages:CreateControlChannel",
|
|
554
|
+
"ssmmessages:OpenControlChannel",
|
|
555
|
+
"ec2messages:GetMessages",
|
|
556
|
+
"ec2messages:SendReply"
|
|
557
|
+
],
|
|
558
|
+
"Resource": "*"
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Or simply attach the AWS managed policy `AmazonSSMManagedInstanceCore`.
|
|
563
|
+
|
|
564
|
+
**2. SSM agent** — Amazon Linux 2023 ships with it pre-installed. The `blueprint/instance-setup.sh` bootstrap script ensures it's running:
|
|
565
|
+
```bash
|
|
566
|
+
systemctl enable amazon-ssm-agent
|
|
567
|
+
systemctl start amazon-ssm-agent
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**3. Store the instance ID** — after running `just infra-up`, get the instance ID and store it as a secret:
|
|
571
|
+
```bash
|
|
572
|
+
cd infrastructure && pulumi stack output instance_id
|
|
573
|
+
# → i-0abc1234567890def
|
|
574
|
+
# Add this to GitHub repository secrets as EC2_INSTANCE_ID
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Example: What Happens on a Typical Push
|
|
578
|
+
|
|
579
|
+
**Scenario 1 — you edited `blueprint/html/index.html`:**
|
|
580
|
+
|
|
581
|
+
```
|
|
582
|
+
Push to development branch
|
|
583
|
+
↓
|
|
584
|
+
detect-changes: infrastructure=false, app=true
|
|
585
|
+
↓
|
|
586
|
+
deploy-app runs:
|
|
587
|
+
aws ssm send-command "git pull && just deploy-prod"
|
|
588
|
+
polls every 10s until success
|
|
589
|
+
prints stdout from EC2 instance
|
|
590
|
+
↓
|
|
591
|
+
New HTML is live ~30 seconds after push
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**Scenario 2 — you changed `infrastructure/__main__.py.jinja` (e.g. bigger instance type):**
|
|
595
|
+
|
|
596
|
+
```
|
|
597
|
+
Push to development branch
|
|
598
|
+
↓
|
|
599
|
+
detect-changes: infrastructure=true, app=false
|
|
600
|
+
↓
|
|
601
|
+
pulumi-apply runs:
|
|
602
|
+
pulumi up --yes
|
|
603
|
+
Pulumi modifies the EC2 instance type in-place (or replaces it)
|
|
604
|
+
↓
|
|
605
|
+
Infrastructure updated. New instance pulls latest code via user-data.
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
**Scenario 3 — you opened a PR with Pulumi changes:**
|
|
609
|
+
|
|
610
|
+
```
|
|
611
|
+
Pull Request to development
|
|
612
|
+
↓
|
|
613
|
+
detect-changes: infrastructure=true
|
|
614
|
+
↓
|
|
615
|
+
pulumi-preview runs:
|
|
616
|
+
pulumi preview
|
|
617
|
+
Output shown in CI logs — no changes applied
|
|
618
|
+
↓
|
|
619
|
+
Reviewer can see exactly what Pulumi will do before merging.
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Adapting for Other Platforms
|
|
623
|
+
|
|
624
|
+
The same two-path logic works for GitLab CI. The superset's `.gitlab-ci.yml` uses:
|
|
625
|
+
|
|
626
|
+
```yaml
|
|
627
|
+
# Skip deploy if terraform changed
|
|
628
|
+
- if: $CI_COMMIT_BRANCH == "main"
|
|
629
|
+
changes:
|
|
630
|
+
- terraform_scalling/**/*
|
|
631
|
+
when: never
|
|
632
|
+
# Only deploy if docker/compose changed
|
|
633
|
+
- if: $CI_COMMIT_BRANCH == "main"
|
|
634
|
+
changes:
|
|
635
|
+
- docker/**/*
|
|
636
|
+
- docker-compose.yml
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
For your adapter's GitLab template, mirror this pattern with `pulumi` instead of `terraform` and `infrastructure/**` instead of `terraform_scalling/**`.
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
## Adding a New Environment Later
|
|
644
|
+
|
|
645
|
+
Environments are set up at `create-service` time. To add one later:
|
|
646
|
+
|
|
647
|
+
```bash
|
|
648
|
+
# Create the branch
|
|
649
|
+
git checkout initial-commit
|
|
650
|
+
git checkout -b staging
|
|
651
|
+
git push origin staging
|
|
652
|
+
|
|
653
|
+
# Generate the workflow file
|
|
654
|
+
cp .github/workflows/deploy-development.yaml .github/workflows/deploy-staging.yaml
|
|
655
|
+
# Edit deploy-staging.yaml: change all occurrences of "development" to "staging"
|
|
656
|
+
git add .github/workflows/deploy-staging.yaml
|
|
657
|
+
git commit -m "chore: add staging environment"
|
|
658
|
+
git push
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## Releasing to PyPI
|
|
664
|
+
|
|
665
|
+
Two workflows handle the full release lifecycle:
|
|
666
|
+
|
|
667
|
+
```
|
|
668
|
+
bump version in pyproject.toml → merge to main
|
|
669
|
+
│
|
|
670
|
+
▼
|
|
671
|
+
tag-version.yml triggers on: push to main, pyproject.toml changed
|
|
672
|
+
reads Poetry version creates git tag vX.Y.Z automatically
|
|
673
|
+
│
|
|
674
|
+
▼
|
|
675
|
+
(go to GitHub → Releases → Draft a new release → publish it)
|
|
676
|
+
│
|
|
677
|
+
▼
|
|
678
|
+
pypi-publish.yml triggers on: release published
|
|
679
|
+
poetry build + publish pushes to PyPI via PYPI_TOKEN
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### One-time setup
|
|
683
|
+
|
|
684
|
+
Add `PYPI_TOKEN` to the repository secrets (`Settings → Secrets → Actions`):
|
|
685
|
+
|
|
686
|
+
1. Go to **https://pypi.org/manage/account/token/** and create an API token scoped to `dwe-core`
|
|
687
|
+
2. In GitHub: `Settings → Secrets and variables → Actions → New repository secret`
|
|
688
|
+
- Name: `PYPI_TOKEN`
|
|
689
|
+
- Value: the token from PyPI (starts with `pypi-`)
|
|
690
|
+
|
|
691
|
+
### Release flow
|
|
692
|
+
|
|
693
|
+
**Step 1 — bump the version and merge to `main`:**
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
poetry version patch # 1.0.0 → 1.0.1
|
|
697
|
+
poetry version minor # 1.0.0 → 1.1.0
|
|
698
|
+
poetry version major # 1.0.0 → 2.0.0
|
|
699
|
+
poetry version prerelease # 1.0.0 → 1.0.1a1
|
|
700
|
+
poetry version 1.2.0 # set explicit version
|
|
701
|
+
|
|
702
|
+
git add pyproject.toml
|
|
703
|
+
git commit -m "chore: bump version to $(poetry version -s)"
|
|
704
|
+
git push origin main
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
`tag-version.yml` fires on the push, reads the version from `pyproject.toml`, and pushes tag `vX.Y.Z`. No manual tagging needed, and it only runs on `main`.
|
|
708
|
+
|
|
709
|
+
**Step 2 — publish the GitHub Release:**
|
|
710
|
+
|
|
711
|
+
Go to `github.com/<org>/dwe-core/releases`, click **Draft a new release**, select the tag just created, and click **Publish release**.
|
|
712
|
+
|
|
713
|
+
`pypi-publish.yml` fires on the publish event: runs `poetry install`, `poetry build`, then `poetry publish -u __token__ -p $PYPI_TOKEN`.
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
## Technical Stack
|
|
718
|
+
|
|
719
|
+
| Concern | Library |
|
|
720
|
+
|---|---|
|
|
721
|
+
| CLI framework | [Typer](https://typer.tiangolo.com/) |
|
|
722
|
+
| Template engine | [Copier](https://copier.readthedocs.io/) |
|
|
723
|
+
| Git operations | [GitPython](https://gitpython.readthedocs.io/) |
|
|
724
|
+
| GitHub secrets | [PyGithub](https://pygithub.readthedocs.io/) |
|
|
725
|
+
| GitLab variables | [python-gitlab](https://python-gitlab.readthedocs.io/) |
|
|
726
|
+
| Runtime templating | [Jinja2](https://jinja.palletsprojects.com/) (for CI templates) |
|
|
727
|
+
| Infrastructure | [Pulumi](https://www.pulumi.com/) |
|
|
728
|
+
| Task runner | [Just](https://just.systems/) |
|
|
729
|
+
|