pdt-cli 0.1.1__tar.gz → 0.1.2__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.
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/PKG-INFO +20 -5
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/README.md +18 -4
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/ci/tests/test_claude_task.py +3 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/ci/tests/test_score_task.py +109 -102
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/pyproject.toml +3 -2
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/cli.py +47 -17
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/completion.py +0 -26
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/config.py +56 -9
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/console.py +13 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/deploy.py +31 -12
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/deploy_aws.py +50 -28
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/deploy_aws_fargate.py +37 -9
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/deploy_azure.py +206 -51
- pdt_cli-0.1.2/src/pdt/deploy_azure_container_apps.py +676 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/deploy_common.py +155 -4
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/deploy_google_cloud.py +64 -34
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/deploy_windows.py +5 -2
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/gcloud_sdk.py +5 -13
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/utils/email_auth.py +4 -14
- pdt_cli-0.1.2/src/pdt/utils/env_secret.py +192 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/conftest.py +7 -0
- pdt_cli-0.1.2/tests/test_aws_inventory.py +38 -0
- pdt_cli-0.1.2/tests/test_aws_names.py +9 -0
- pdt_cli-0.1.2/tests/test_aws_not_found.py +19 -0
- pdt_cli-0.1.2/tests/test_aws_profile.py +16 -0
- pdt_cli-0.1.2/tests/test_azure_build.py +62 -0
- pdt_cli-0.1.2/tests/test_azure_deleted_vault.py +54 -0
- pdt_cli-0.1.2/tests/test_azure_environment.py +210 -0
- pdt_cli-0.1.2/tests/test_azure_environment_key.py +23 -0
- pdt_cli-0.1.2/tests/test_azure_group_ownership.py +21 -0
- pdt_cli-0.1.2/tests/test_azure_job_diagnostics.py +132 -0
- pdt_cli-0.1.2/tests/test_azure_names.py +24 -0
- pdt_cli-0.1.2/tests/test_azure_retries.py +113 -0
- pdt_cli-0.1.2/tests/test_azure_side_resources.py +39 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_azure_storage.py +19 -0
- pdt_cli-0.1.2/tests/test_azure_subscription_state.py +29 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_ci_azure.py +8 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_ci_google_cloud.py +0 -17
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_cli.py +15 -4
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_completion.py +0 -12
- pdt_cli-0.1.2/tests/test_env_secret.py +142 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_env_sources.py +52 -1
- pdt_cli-0.1.2/tests/test_gcloud_sdk.py +24 -0
- pdt_cli-0.1.2/tests/test_google_cloud_inventory.py +23 -0
- pdt_cli-0.1.2/tests/test_google_cloud_not_found.py +13 -0
- pdt_cli-0.1.2/tests/test_google_cloud_retries.py +41 -0
- pdt_cli-0.1.2/tests/test_google_cloud_service_account.py +24 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_project_discovery.py +7 -0
- pdt_cli-0.1.2/tests/test_secrets.py +110 -0
- pdt_cli-0.1.2/tests/test_verify.py +277 -0
- pdt_cli-0.1.2/verify/README.md +92 -0
- pdt_cli-0.1.1/src/pdt/deploy_azure_container_apps.py +0 -402
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/.gitignore +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/ci/tests/test_close_stale_drafts.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/ci/tests/test_rebase_task.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/__init__.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/examples/hello-world/config.yml +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/examples/hello-world/run.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/examples/impossible-travel-report/config.yml +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/examples/impossible-travel-report/env.template +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/examples/impossible-travel-report/run.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/examples/monday-orphaned-account-report/config.yml +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/examples/monday-orphaned-account-report/env.template +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/examples/monday-orphaned-account-report/run.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/scaffold.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/storage_cli.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/utils/__init__.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/utils/entra.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/utils/log.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/utils/send_email.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/utils/send_email_graph.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/utils/storage.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/src/pdt/utils/web.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_aws_account.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_aws_storage.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_config_merge.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_console.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_cost_estimate.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_deploy_state.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_dockerfile.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_fetch_json.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_google_cloud_storage.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_init_questions.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_log_colour.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_run_log_links.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_save_platform_key.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_scaffold.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_schedule.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_starter_app.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_storage.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_storage_cli.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_strict_validation.py +0 -0
- {pdt_cli-0.1.1 → pdt_cli-0.1.2}/tests/test_windows_storage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pdt-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Easily deploy your process to AWS, Azure, Google Cloud, or Windows.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -17,6 +17,7 @@ Requires-Dist: shellingham
|
|
|
17
17
|
Provides-Extra: apps
|
|
18
18
|
Requires-Dist: adlfs; extra == 'apps'
|
|
19
19
|
Requires-Dist: azure-identity; extra == 'apps'
|
|
20
|
+
Requires-Dist: azure-keyvault-secrets; extra == 'apps'
|
|
20
21
|
Requires-Dist: azure-storage-blob; extra == 'apps'
|
|
21
22
|
Requires-Dist: boto3; extra == 'apps'
|
|
22
23
|
Requires-Dist: cryptography; extra == 'apps'
|
|
@@ -40,7 +41,7 @@ uv tool install pdt-cli
|
|
|
40
41
|
|
|
41
42
|
That puts a `pdt` command on your PATH. To update it later, run `uv tool upgrade pdt-cli`.
|
|
42
43
|
|
|
43
|
-
The first interactive `pdt` command turns on tab completion without a prompt. It works in every new terminal: Tab finishes pdt commands, their options, your app names, example names for `new --from
|
|
44
|
+
The first interactive `pdt` command turns on tab completion without a prompt. It works in every new terminal: Tab finishes pdt commands, their options, your app names, and example names for `new --from`. It supports bash, zsh, fish, and PowerShell. `pdt completion [SHELL]` sets up a second shell, and `pdt completion --script bash` prints the script instead of writing it, if you keep your own dotfiles.
|
|
44
45
|
|
|
45
46
|
You can also clone this repository and run `./pdt` (or `.\pdt.bat` on Windows) instead. It installs `uv` for you if you do not have it. Both ways give you the same commands.
|
|
46
47
|
|
|
@@ -116,18 +117,22 @@ pdt login my-report
|
|
|
116
117
|
| `pdt init [DIR]` | create a project here, or in DIR |
|
|
117
118
|
| `pdt examples` | list the example apps bundled with pdt |
|
|
118
119
|
| `pdt new APP --from EXAMPLE` | add an app to the project |
|
|
119
|
-
| `pdt list` | show every app with its schedule and provider |
|
|
120
|
+
| `pdt list` | show every app with its schedule and provider; `--names` prints only the enabled app names |
|
|
120
121
|
| `pdt validate` | check the config files and the required env vars |
|
|
121
122
|
| `pdt run APP` | run an app on this machine |
|
|
122
123
|
| `pdt deploy APP` | deploy an app to its configured platform |
|
|
123
124
|
| `pdt destroy APP` | remove everything deploy created |
|
|
125
|
+
| `pdt secrets APP` | show which `.env` values differ from the deployed app |
|
|
126
|
+
| `pdt secrets APP save` | send your `.env` values to the deployed app; the next run uses them |
|
|
127
|
+
| `pdt secrets APP get` | copy the deployed values into a `.env.<provider>` file |
|
|
128
|
+
| `pdt secrets APP set NAME` | put one value, read from stdin, into the deployed app's secrets |
|
|
124
129
|
| `pdt login APP` | sign in again to the app's cloud provider |
|
|
125
130
|
| `pdt storage APP ls|get|query|destroy` | look at, fetch, query, or delete the app's stored files |
|
|
126
131
|
| `pdt az ...` | run the Azure CLI that pdt installs |
|
|
127
132
|
| `pdt gcloud ...` | run the Google Cloud CLI that pdt installs |
|
|
128
133
|
| `pdt completion [SHELL]` | turn on tab completion for a shell |
|
|
129
134
|
|
|
130
|
-
Leave `APP` off `run`, `deploy`, `destroy`, or `login`, or mistype it, and pdt lists the apps in the project so you can pick one.
|
|
135
|
+
Leave `APP` off `run`, `deploy`, `destroy`, `secrets`, or `login`, or mistype it, and pdt lists the apps in the project so you can pick one.
|
|
131
136
|
|
|
132
137
|
`pdt az` and `pdt gcloud` hand your arguments straight to the cloud tool, and install it first if it is missing. For example, `pdt az account list`.
|
|
133
138
|
|
|
@@ -165,6 +170,8 @@ store.push(Path(".pdt-state"), "state/", lease)
|
|
|
165
170
|
|
|
166
171
|
From your own computer, `pdt storage APP ls`, `get`, and `query` read the files with your own cloud sign-in. `pdt storage APP destroy` is the only command that deletes them, and it asks first. An app that needs none of this sets `storage: false` in its `config.yml`.
|
|
167
172
|
|
|
173
|
+
An app that is not ready sets `enabled: false` in its `config.yml`. `pdt list` still shows it, and every other command acts as if the app is not there. `uv run run.py` in the app folder still runs it.
|
|
174
|
+
|
|
168
175
|
## Running pdt from CI
|
|
169
176
|
|
|
170
177
|
Every command reads its values from the environment it runs in. A build server sets them as CI variables, and no `.env` file is needed.
|
|
@@ -186,10 +193,14 @@ platform:
|
|
|
186
193
|
subscription: 00000000-0000-0000-0000-000000000000
|
|
187
194
|
region: eastus
|
|
188
195
|
resource_group: pdt
|
|
196
|
+
# Optional. An environment you already own, as <resource-group>/<name>.
|
|
197
|
+
# environment: my-group/my-environment
|
|
189
198
|
```
|
|
190
199
|
|
|
191
200
|
`subscription` is optional. When it is missing or wrong, the deploy asks you to choose one.
|
|
192
201
|
|
|
202
|
+
Every pdt project in a subscription runs its jobs in one shared Container Apps environment per region, `pdt-<region>` in the resource group `pdt-shared`, because a subscription allows only a few environments. Destroying the last app that uses the environment removes it, and removes `pdt-shared` once it holds no environment. Set `environment` to use an environment you already have; pdt then never creates, changes, or deletes it.
|
|
203
|
+
|
|
193
204
|
### AWS
|
|
194
205
|
|
|
195
206
|
```yaml
|
|
@@ -198,8 +209,12 @@ platform:
|
|
|
198
209
|
region: us-east-1
|
|
199
210
|
# Written for you on the first deploy, from your credentials.
|
|
200
211
|
account: "123456789012"
|
|
212
|
+
# Optional. Which profile in ~/.aws to use. Written for you if pdt has to ask.
|
|
213
|
+
profile: work
|
|
201
214
|
```
|
|
202
215
|
|
|
216
|
+
`profile` is optional. When it is missing, pdt uses `AWS_PROFILE`, or the only profile on your computer. When there are several and none is chosen, or the one in the file is not on this computer, the deploy lists them and asks you to choose, then writes your answer here.
|
|
217
|
+
|
|
203
218
|
### Google Cloud
|
|
204
219
|
|
|
205
220
|
```yaml
|
|
@@ -289,7 +304,7 @@ An app is a folder with a `run.py` that has a `main()` function. It declares its
|
|
|
289
304
|
```python
|
|
290
305
|
# /// script
|
|
291
306
|
# requires-python = ">=3.12"
|
|
292
|
-
# dependencies = ["pdt-cli[apps]==0.1.
|
|
307
|
+
# dependencies = ["pdt-cli[apps]==0.1.2"]
|
|
293
308
|
# ///
|
|
294
309
|
from pathlib import Path
|
|
295
310
|
|
|
@@ -10,7 +10,7 @@ uv tool install pdt-cli
|
|
|
10
10
|
|
|
11
11
|
That puts a `pdt` command on your PATH. To update it later, run `uv tool upgrade pdt-cli`.
|
|
12
12
|
|
|
13
|
-
The first interactive `pdt` command turns on tab completion without a prompt. It works in every new terminal: Tab finishes pdt commands, their options, your app names, example names for `new --from
|
|
13
|
+
The first interactive `pdt` command turns on tab completion without a prompt. It works in every new terminal: Tab finishes pdt commands, their options, your app names, and example names for `new --from`. It supports bash, zsh, fish, and PowerShell. `pdt completion [SHELL]` sets up a second shell, and `pdt completion --script bash` prints the script instead of writing it, if you keep your own dotfiles.
|
|
14
14
|
|
|
15
15
|
You can also clone this repository and run `./pdt` (or `.\pdt.bat` on Windows) instead. It installs `uv` for you if you do not have it. Both ways give you the same commands.
|
|
16
16
|
|
|
@@ -86,18 +86,22 @@ pdt login my-report
|
|
|
86
86
|
| `pdt init [DIR]` | create a project here, or in DIR |
|
|
87
87
|
| `pdt examples` | list the example apps bundled with pdt |
|
|
88
88
|
| `pdt new APP --from EXAMPLE` | add an app to the project |
|
|
89
|
-
| `pdt list` | show every app with its schedule and provider |
|
|
89
|
+
| `pdt list` | show every app with its schedule and provider; `--names` prints only the enabled app names |
|
|
90
90
|
| `pdt validate` | check the config files and the required env vars |
|
|
91
91
|
| `pdt run APP` | run an app on this machine |
|
|
92
92
|
| `pdt deploy APP` | deploy an app to its configured platform |
|
|
93
93
|
| `pdt destroy APP` | remove everything deploy created |
|
|
94
|
+
| `pdt secrets APP` | show which `.env` values differ from the deployed app |
|
|
95
|
+
| `pdt secrets APP save` | send your `.env` values to the deployed app; the next run uses them |
|
|
96
|
+
| `pdt secrets APP get` | copy the deployed values into a `.env.<provider>` file |
|
|
97
|
+
| `pdt secrets APP set NAME` | put one value, read from stdin, into the deployed app's secrets |
|
|
94
98
|
| `pdt login APP` | sign in again to the app's cloud provider |
|
|
95
99
|
| `pdt storage APP ls|get|query|destroy` | look at, fetch, query, or delete the app's stored files |
|
|
96
100
|
| `pdt az ...` | run the Azure CLI that pdt installs |
|
|
97
101
|
| `pdt gcloud ...` | run the Google Cloud CLI that pdt installs |
|
|
98
102
|
| `pdt completion [SHELL]` | turn on tab completion for a shell |
|
|
99
103
|
|
|
100
|
-
Leave `APP` off `run`, `deploy`, `destroy`, or `login`, or mistype it, and pdt lists the apps in the project so you can pick one.
|
|
104
|
+
Leave `APP` off `run`, `deploy`, `destroy`, `secrets`, or `login`, or mistype it, and pdt lists the apps in the project so you can pick one.
|
|
101
105
|
|
|
102
106
|
`pdt az` and `pdt gcloud` hand your arguments straight to the cloud tool, and install it first if it is missing. For example, `pdt az account list`.
|
|
103
107
|
|
|
@@ -135,6 +139,8 @@ store.push(Path(".pdt-state"), "state/", lease)
|
|
|
135
139
|
|
|
136
140
|
From your own computer, `pdt storage APP ls`, `get`, and `query` read the files with your own cloud sign-in. `pdt storage APP destroy` is the only command that deletes them, and it asks first. An app that needs none of this sets `storage: false` in its `config.yml`.
|
|
137
141
|
|
|
142
|
+
An app that is not ready sets `enabled: false` in its `config.yml`. `pdt list` still shows it, and every other command acts as if the app is not there. `uv run run.py` in the app folder still runs it.
|
|
143
|
+
|
|
138
144
|
## Running pdt from CI
|
|
139
145
|
|
|
140
146
|
Every command reads its values from the environment it runs in. A build server sets them as CI variables, and no `.env` file is needed.
|
|
@@ -156,10 +162,14 @@ platform:
|
|
|
156
162
|
subscription: 00000000-0000-0000-0000-000000000000
|
|
157
163
|
region: eastus
|
|
158
164
|
resource_group: pdt
|
|
165
|
+
# Optional. An environment you already own, as <resource-group>/<name>.
|
|
166
|
+
# environment: my-group/my-environment
|
|
159
167
|
```
|
|
160
168
|
|
|
161
169
|
`subscription` is optional. When it is missing or wrong, the deploy asks you to choose one.
|
|
162
170
|
|
|
171
|
+
Every pdt project in a subscription runs its jobs in one shared Container Apps environment per region, `pdt-<region>` in the resource group `pdt-shared`, because a subscription allows only a few environments. Destroying the last app that uses the environment removes it, and removes `pdt-shared` once it holds no environment. Set `environment` to use an environment you already have; pdt then never creates, changes, or deletes it.
|
|
172
|
+
|
|
163
173
|
### AWS
|
|
164
174
|
|
|
165
175
|
```yaml
|
|
@@ -168,8 +178,12 @@ platform:
|
|
|
168
178
|
region: us-east-1
|
|
169
179
|
# Written for you on the first deploy, from your credentials.
|
|
170
180
|
account: "123456789012"
|
|
181
|
+
# Optional. Which profile in ~/.aws to use. Written for you if pdt has to ask.
|
|
182
|
+
profile: work
|
|
171
183
|
```
|
|
172
184
|
|
|
185
|
+
`profile` is optional. When it is missing, pdt uses `AWS_PROFILE`, or the only profile on your computer. When there are several and none is chosen, or the one in the file is not on this computer, the deploy lists them and asks you to choose, then writes your answer here.
|
|
186
|
+
|
|
173
187
|
### Google Cloud
|
|
174
188
|
|
|
175
189
|
```yaml
|
|
@@ -259,7 +273,7 @@ An app is a folder with a `run.py` that has a `main()` function. It declares its
|
|
|
259
273
|
```python
|
|
260
274
|
# /// script
|
|
261
275
|
# requires-python = ">=3.12"
|
|
262
|
-
# dependencies = ["pdt-cli[apps]==0.1.
|
|
276
|
+
# dependencies = ["pdt-cli[apps]==0.1.2"]
|
|
263
277
|
# ///
|
|
264
278
|
from pathlib import Path
|
|
265
279
|
|
|
@@ -204,6 +204,9 @@ def test_claude_command_shape(tmp_path):
|
|
|
204
204
|
assert command[command.index("--disallowedTools") + 1] == "Bash(git push -f*)"
|
|
205
205
|
assert command[command.index("--max-budget-usd") + 1] == "20.0"
|
|
206
206
|
assert "--append-system-prompt-file" not in command
|
|
207
|
+
assert "--effort" not in command
|
|
208
|
+
high = runner.claude_command(runner.load_task("demo", make_task(tmp_path / "e", "effort: high\n")))
|
|
209
|
+
assert high[high.index("--effort") + 1] == "high"
|
|
207
210
|
assert runner.claude_command(task, 0.1)[runner.claude_command(task, 0.1).index(
|
|
208
211
|
"--max-budget-usd") + 1] == "0.1"
|
|
209
212
|
|
|
@@ -136,6 +136,21 @@ def test_a_simple_floor_says_why():
|
|
|
136
136
|
"simple", ["only documentation, tests, or example apps changed"])
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
def test_a_few_tested_lines_in_a_covered_file_floor_at_simple():
|
|
140
|
+
small = change("src/pdt/deploy_common.py", lines=4)
|
|
141
|
+
test = change("tests/test_dockerfile.py")
|
|
142
|
+
floor, reasons = select.rule_floor(mr(), [small, test])
|
|
143
|
+
assert floor == "simple" and "src/pdt/deploy_common.py" in reasons[0]
|
|
144
|
+
assert select.rule_floor(mr(), [small])[0] == "architectural"
|
|
145
|
+
assert select.rule_floor(mr(), [small, change("ci/tests/test_x.py")])[0] == "architectural"
|
|
146
|
+
five = change("src/pdt/deploy_common.py", lines=5)
|
|
147
|
+
assert select.rule_floor(mr(), [five, test])[0] == "architectural"
|
|
148
|
+
gated = change("src/pdt/cli.py", extra="+token = 1\n")
|
|
149
|
+
assert select.rule_floor(mr(), [gated, test])[0] == "architectural"
|
|
150
|
+
for path in ("src/pdt/utils/log.py", ".gitlab-ci.yml", "AGENTS.md", "pdt"):
|
|
151
|
+
assert select.rule_floor(mr(), [change(path), test])[0] == "architectural", path
|
|
152
|
+
|
|
153
|
+
|
|
139
154
|
def test_deleted_renamed_and_large_files_bump_simple_to_review():
|
|
140
155
|
for flag in ("deleted_file", "renamed_file", "too_large", "collapsed"):
|
|
141
156
|
floor, reasons = select.rule_floor(mr(), [change("README.md", **{flag: True})])
|
|
@@ -265,6 +280,9 @@ def test_status_and_note_body():
|
|
|
265
280
|
assert body.splitlines()[0] == check.format_marker("abc", "review", "architectural", "raised")
|
|
266
281
|
assert "`tier::architectural`" in body and "Claude raised it" in body
|
|
267
282
|
assert "- touches deploy dispatch" in body and "http://job" in body
|
|
283
|
+
assert check.MERGE_FOOTER not in body and check.FOOTER in body
|
|
284
|
+
simple = check.note_body("abc", check.Score("simple", "simple", [], "agree"), "")
|
|
285
|
+
assert check.MERGE_FOOTER in simple
|
|
268
286
|
|
|
269
287
|
|
|
270
288
|
def test_label_change_leaves_exactly_one_tier_label():
|
|
@@ -276,16 +294,6 @@ def test_label_change_leaves_exactly_one_tier_label():
|
|
|
276
294
|
|
|
277
295
|
# --- select hook end to end ---------------------------------------------------
|
|
278
296
|
|
|
279
|
-
def webhook_event(tmp_path, monkeypatch, iid, action="open", changes=None) -> None:
|
|
280
|
-
"""Point TRIGGER_PAYLOAD at a merge request webhook payload for ``iid``."""
|
|
281
|
-
payload = {"object_kind": "merge_request", "event_type": "merge_request",
|
|
282
|
-
"object_attributes": {"iid": iid, "action": action},
|
|
283
|
-
"changes": changes or {}}
|
|
284
|
-
path = tmp_path / "payload.json"
|
|
285
|
-
path.write_text(json.dumps(payload))
|
|
286
|
-
monkeypatch.setenv("TRIGGER_PAYLOAD", str(path))
|
|
287
|
-
|
|
288
|
-
|
|
289
297
|
def run_select(monkeypatch, capsys, mrs, notes, dry_run="false"):
|
|
290
298
|
arm(monkeypatch, dry_run)
|
|
291
299
|
fetches = []
|
|
@@ -389,61 +397,8 @@ def test_select_names_missing_variables(monkeypatch, capsys):
|
|
|
389
397
|
|
|
390
398
|
# --- a webhook run looks at one MR --------------------------------------------
|
|
391
399
|
|
|
392
|
-
def
|
|
393
|
-
|
|
394
|
-
return {"object_kind": "merge_request",
|
|
395
|
-
"object_attributes": {"iid": 5, "action": action, **attrs},
|
|
396
|
-
"changes": changes or {}}
|
|
397
|
-
|
|
398
|
-
assert select.event_scope(None) == ("all", None)
|
|
399
|
-
assert select.event_scope({"object_kind": "push"}) == ("all", None)
|
|
400
|
-
assert select.event_scope({"object_kind": "merge_request", "object_attributes": {}}) == (
|
|
401
|
-
"all", None)
|
|
402
|
-
ready = {"draft": {"previous": True, "current": False}, "title": {}, "updated_at": {}}
|
|
403
|
-
assert select.event_scope(event("open")) == ("one", 5)
|
|
404
|
-
assert select.event_scope(event("reopen")) == ("one", 5)
|
|
405
|
-
assert select.event_scope(event("update", ready)) == ("one", 5)
|
|
406
|
-
assert select.event_scope(event("update", {"work_in_progress": ready["draft"]})) == ("one", 5)
|
|
407
|
-
# Everything else does nothing: pushes, rebases, labels, edits, going to Draft, closing.
|
|
408
|
-
assert select.event_scope(event("update", {"last_commit": {}, "updated_at": {}})) == ("none", 5)
|
|
409
|
-
assert select.event_scope(event("update", {"labels": {}, "updated_at": {}})) == ("none", 5)
|
|
410
|
-
assert select.event_scope(event("update", {"draft": {"previous": False, "current": True}})) == (
|
|
411
|
-
"none", 5)
|
|
412
|
-
assert select.event_scope(event("update", {"description": {}})) == ("none", 5)
|
|
413
|
-
assert select.event_scope(event("update")) == ("none", 5)
|
|
414
|
-
assert select.event_scope(event("approved")) == ("none", 5)
|
|
415
|
-
assert select.event_scope(event("close")) == ("none", 5)
|
|
416
|
-
assert select.event_scope(event("merge")) == ("none", 5)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def test_gate_says_skip_for_an_event_that_does_nothing_and_run_otherwise(monkeypatch, capsys, tmp_path):
|
|
420
|
-
monkeypatch.delenv("TRIGGER_PAYLOAD", raising=False)
|
|
421
|
-
assert select.main(["--gate"]) == 0
|
|
422
|
-
assert capsys.readouterr().out.strip() == "run"
|
|
423
|
-
webhook_event(tmp_path, monkeypatch, 4, action="update", changes={"last_commit": {}})
|
|
424
|
-
assert select.main(["--gate"]) == 0
|
|
425
|
-
out, err = capsys.readouterr()
|
|
426
|
-
assert out.strip() == "skip" and "webhook event for !4 is 'update'" in err
|
|
427
|
-
webhook_event(tmp_path, monkeypatch, 4, action="update",
|
|
428
|
-
changes={"draft": {"previous": True, "current": False}})
|
|
429
|
-
assert select.main(["--gate"]) == 0
|
|
430
|
-
assert capsys.readouterr().out.strip() == "run"
|
|
431
|
-
webhook_event(tmp_path, monkeypatch, 4)
|
|
432
|
-
assert select.main(["--gate"]) == 0
|
|
433
|
-
assert capsys.readouterr().out.strip() == "run"
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
def test_the_ci_job_gates_on_the_event_before_installing_anything():
|
|
437
|
-
ci = yaml.safe_load((REPO / ".gitlab-ci.yml").read_text())
|
|
438
|
-
gate = ci[".claude-task"]["before_script"][0]
|
|
439
|
-
assert gate.startswith("if [ -n \"$CLAUDE_TASK_GATE\" ]") and "= skip" in gate and "exit 0" in gate
|
|
440
|
-
assert ci["score-mrs"]["variables"]["CLAUDE_TASK_GATE"] == \
|
|
441
|
-
"python ci/claude-tasks/score-mrs/select_mrs.py --gate"
|
|
442
|
-
assert "CLAUDE_TASK_GATE" not in ci["rebase-mrs"].get("variables", {})
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
def test_a_webhook_run_scores_only_the_mr_in_the_event(monkeypatch, capsys, tmp_path):
|
|
446
|
-
webhook_event(tmp_path, monkeypatch, 2)
|
|
400
|
+
def test_a_webhook_run_scores_only_the_mr_in_the_event(monkeypatch, capsys):
|
|
401
|
+
monkeypatch.setenv("MR_IID", "2")
|
|
447
402
|
mrs = {1: {**mr(1), "_files": [change("docs/a.md")]},
|
|
448
403
|
2: {**mr(2), "_files": [change("docs/b.md")]},
|
|
449
404
|
3: {**mr(3), "_files": [change("docs/c.md")]}}
|
|
@@ -452,53 +407,103 @@ def test_a_webhook_run_scores_only_the_mr_in_the_event(monkeypatch, capsys, tmp_
|
|
|
452
407
|
assert "webhook event for !2: looking at that MR only" in err
|
|
453
408
|
assert "1 open MR(s)" in err
|
|
454
409
|
touched = {p for _, p, _ in calls if "/merge_requests/" in p}
|
|
455
|
-
assert touched == {"/projects/42/merge_requests/2",
|
|
456
|
-
"/projects/42/merge_requests/2/
|
|
410
|
+
assert touched == {"/projects/42/merge_requests/2",
|
|
411
|
+
"/projects/42/merge_requests/2/diffs?per_page=100&page=1",
|
|
412
|
+
"/projects/42/merge_requests/2/notes?sort=desc&order_by=created_at"
|
|
413
|
+
"&per_page=100&page=1"}
|
|
457
414
|
assert fetches == [["git", "fetch", "--quiet", "origin",
|
|
458
415
|
"+refs/heads/master:refs/remotes/origin/master",
|
|
459
416
|
"+refs/heads/branch-2:refs/remotes/origin/branch-2"]]
|
|
460
417
|
|
|
461
418
|
|
|
462
|
-
def
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
419
|
+
def test_a_webhook_run_for_an_mr_that_is_no_longer_open_lists_nothing(monkeypatch, capsys):
|
|
420
|
+
monkeypatch.setenv("MR_IID", "9")
|
|
421
|
+
items, err, calls, fetches = run_select(monkeypatch, capsys, {1: {**mr(1), "_files": []}}, {})
|
|
422
|
+
assert items == [] and fetches == []
|
|
423
|
+
assert "not an open MR of this project" in err
|
|
424
|
+
assert [p for _, p, _ in calls] == [
|
|
425
|
+
"/projects/42/merge_requests?state=opened&target_branch=master&per_page=100&page=1"]
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def test_a_run_without_an_event_sweeps_every_open_mr(monkeypatch, capsys):
|
|
429
|
+
monkeypatch.delenv("MR_IID", raising=False)
|
|
466
430
|
mrs = {1: {**mr(1), "_files": [change("docs/a.md")]},
|
|
467
431
|
2: {**mr(2), "_files": [change("docs/b.md")]}}
|
|
468
432
|
items, err, _, _ = run_select(monkeypatch, capsys, mrs, {})
|
|
469
|
-
assert [item["iid"] for item in items] == [2]
|
|
470
|
-
assert "
|
|
433
|
+
assert [item["iid"] for item in items] == [1, 2]
|
|
434
|
+
assert "webhook event" not in err
|
|
435
|
+
monkeypatch.setenv("MR_IID", " ")
|
|
436
|
+
items, _, _, _ = run_select(monkeypatch, capsys, mrs, {})
|
|
437
|
+
assert [item["iid"] for item in items] == [1, 2]
|
|
471
438
|
|
|
472
439
|
|
|
473
|
-
|
|
474
|
-
for action, changes in (("close", None),
|
|
475
|
-
("update", {"last_commit": {"previous": {}, "current": {}}}),
|
|
476
|
-
("update", {"labels": {"previous": [], "current": [{"title": "tier::review"}]},
|
|
477
|
-
"updated_at": {"previous": "a", "current": "b"}})):
|
|
478
|
-
webhook_event(tmp_path, monkeypatch, 2, action=action, changes=changes)
|
|
479
|
-
items, err, calls, fetches = run_select(monkeypatch, capsys,
|
|
480
|
-
{2: {**mr(2), "_files": []}}, {})
|
|
481
|
-
assert items == [] and calls == [] and fetches == [], action
|
|
482
|
-
assert f"webhook event for !2 is '{action}'" in err and "nothing to do" in err
|
|
440
|
+
# --- which events run which job -----------------------------------------------
|
|
483
441
|
|
|
442
|
+
def job_runs(job: str, variables: dict) -> bool:
|
|
443
|
+
"""Evaluate one job's rules the way GitLab would for these variables.
|
|
484
444
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
445
|
+
Only what those rules use is understood: ``$X == "v"``, ``$X != "v"`` and
|
|
446
|
+
``$X == null``, joined with ``&&``, and a rule with no ``if`` that matches
|
|
447
|
+
everything. The first matching rule decides; no match means the job does
|
|
448
|
+
not run.
|
|
449
|
+
"""
|
|
450
|
+
ci = yaml.safe_load((REPO / ".gitlab-ci.yml").read_text())
|
|
451
|
+
for rule in ci[job]["rules"]:
|
|
452
|
+
matched = True
|
|
453
|
+
for clause in rule["if"].split(" && ") if "if" in rule else ():
|
|
454
|
+
if " != " in clause:
|
|
455
|
+
name, _, value = clause.partition(" != ")
|
|
456
|
+
matched &= variables.get(name.strip("$ ")) != value.strip('"')
|
|
457
|
+
else:
|
|
458
|
+
name, _, value = clause.partition(" == ")
|
|
459
|
+
expected = None if value == "null" else value.strip('"')
|
|
460
|
+
matched &= variables.get(name.strip("$ ")) == expected
|
|
461
|
+
if matched:
|
|
462
|
+
return rule.get("when") != "never"
|
|
463
|
+
return False
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def trigger(action, before="null", after="null"):
|
|
467
|
+
"""The variables the webhook's template sends; a missing Draft change renders as null."""
|
|
468
|
+
return {"CI_PIPELINE_SOURCE": "trigger", "mode": "score_mrs", "MR_IID": "5",
|
|
469
|
+
"MR_ACTION": action, "MR_DRAFT_BEFORE": before, "MR_DRAFT_AFTER": after}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
SCORED_EVENTS = [trigger("open"), trigger("reopen"), trigger("update", "true", "false")]
|
|
473
|
+
# Pushes, rebases, labels, edits, going to Draft, approvals, closing, merging.
|
|
474
|
+
SKIPPED_EVENTS = [trigger("update"), trigger("update", "false", "true"),
|
|
475
|
+
trigger("update", "true", "true"), trigger("approved"),
|
|
476
|
+
trigger("close"), trigger("merge"), trigger("")]
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def test_a_webhook_event_scores_only_when_the_mr_opens_or_leaves_draft():
|
|
480
|
+
for variables in SCORED_EVENTS:
|
|
481
|
+
assert job_runs("score-mrs", variables), variables
|
|
482
|
+
assert not job_runs("score-mrs-skip", variables), variables
|
|
483
|
+
for variables in SKIPPED_EVENTS:
|
|
484
|
+
assert not job_runs("score-mrs", variables), variables
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def test_every_webhook_event_gets_a_pipeline_so_the_hook_never_sees_a_4xx():
|
|
488
|
+
# The trigger API answers 400 when a pipeline has no job or workflow rules
|
|
489
|
+
# filter it out, and GitLab disables a webhook after four 4xx in a row.
|
|
490
|
+
ci = yaml.safe_load((REPO / ".gitlab-ci.yml").read_text())
|
|
491
|
+
assert "workflow" not in ci, "workflow rules would filter trigger pipelines out"
|
|
492
|
+
for variables in SCORED_EVENTS + SKIPPED_EVENTS:
|
|
493
|
+
assert job_runs("score-mrs", variables) != job_runs("score-mrs-skip", variables), variables
|
|
491
494
|
|
|
492
495
|
|
|
493
|
-
def
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
496
|
+
def test_the_skip_job_never_runs_outside_a_score_mrs_trigger():
|
|
497
|
+
for variables in [{"CI_PIPELINE_SOURCE": "merge_request_event"},
|
|
498
|
+
{"CI_PIPELINE_SOURCE": "push", "CI_COMMIT_BRANCH": "master"},
|
|
499
|
+
{"CI_PIPELINE_SOURCE": "schedule", "mode": "score_mrs"},
|
|
500
|
+
{"CI_PIPELINE_SOURCE": "schedule", "mode": "close_stale_drafts"},
|
|
501
|
+
{"CI_PIPELINE_SOURCE": "web", "mode": "score_mrs"},
|
|
502
|
+
{"CI_PIPELINE_SOURCE": "web"},
|
|
503
|
+
{"CI_PIPELINE_SOURCE": "trigger", "mode": "other"}]:
|
|
504
|
+
assert not job_runs("score-mrs-skip", variables), variables
|
|
505
|
+
assert job_runs("score-mrs", {"CI_PIPELINE_SOURCE": "schedule", "mode": "score_mrs"})
|
|
506
|
+
assert job_runs("score-mrs", {"CI_PIPELINE_SOURCE": "web", "mode": "score_mrs"})
|
|
502
507
|
|
|
503
508
|
|
|
504
509
|
# --- check hook end to end ----------------------------------------------------
|
|
@@ -613,7 +618,8 @@ def test_score_mrs_job_runs_on_a_webhook_trigger_and_its_schedule():
|
|
|
613
618
|
rules = {rule["if"]: rule.get("variables", {}) for rule in job["rules"]}
|
|
614
619
|
assert not [k for k in rules if "CI_DEFAULT_BRANCH" in k], "no run on every merge"
|
|
615
620
|
trigger = [v for k, v in rules.items() if '"trigger"' in k and "score_mrs" in k]
|
|
616
|
-
assert trigger ==
|
|
621
|
+
assert len(trigger) == 3, "open, reopen, and leaving Draft"
|
|
622
|
+
assert all(v == {"DRY_RUN": "false"} for v in trigger)
|
|
617
623
|
schedule = [v for k, v in rules.items() if '"schedule"' in k and "score_mrs" in k]
|
|
618
624
|
assert schedule == [{"DRY_RUN": "false"}]
|
|
619
625
|
web = [(k, v) for k, v in rules.items() if '"web"' in k and "score_mrs" in k]
|
|
@@ -627,7 +633,8 @@ def test_a_mode_pipeline_runs_only_its_own_task():
|
|
|
627
633
|
if not isinstance(job, dict) or "rules" not in job:
|
|
628
634
|
continue
|
|
629
635
|
for rule in job["rules"]:
|
|
630
|
-
|
|
636
|
+
condition = rule.get("if", "")
|
|
637
|
+
if "$mode ==" in condition and "$mode == null" not in condition:
|
|
631
638
|
continue # the job a mode pipeline is for
|
|
632
|
-
if "CI_DEFAULT_BRANCH" in
|
|
633
|
-
assert "$mode == null" in
|
|
639
|
+
if "CI_DEFAULT_BRANCH" in condition:
|
|
640
|
+
assert "$mode == null" in condition, name
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pdt-cli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "Easily deploy your process to AWS, Azure, Google Cloud, or Windows."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -31,6 +31,7 @@ apps = [
|
|
|
31
31
|
"google-cloud-storage",
|
|
32
32
|
"azure-storage-blob",
|
|
33
33
|
"azure-identity",
|
|
34
|
+
"azure-keyvault-secrets",
|
|
34
35
|
]
|
|
35
36
|
|
|
36
37
|
[project.scripts]
|
|
@@ -47,7 +48,7 @@ packages = ["src/pdt"]
|
|
|
47
48
|
include = ["src/pdt", "README.md", "tests"]
|
|
48
49
|
|
|
49
50
|
[dependency-groups]
|
|
50
|
-
dev = ["pytest", "ruff", "fsspec"]
|
|
51
|
+
dev = ["pytest", "ruff", "fsspec", "boto3"]
|
|
51
52
|
|
|
52
53
|
[tool.pytest.ini_options]
|
|
53
54
|
testpaths = ["tests", "ci/tests"]
|
|
@@ -16,7 +16,7 @@ from pathlib import Path
|
|
|
16
16
|
|
|
17
17
|
import rich_argparse
|
|
18
18
|
|
|
19
|
-
from pdt import __version__, completion, config, console, deploy, scaffold
|
|
19
|
+
from pdt import __version__, completion, config, console, deploy, deploy_common, scaffold
|
|
20
20
|
from pdt.config import ConfigError
|
|
21
21
|
from pdt.utils.send_email import auth_env_file, email_problems, prepare_email_auth
|
|
22
22
|
|
|
@@ -46,6 +46,15 @@ def say_no_apps() -> None:
|
|
|
46
46
|
console.command("pdt new my-report --from <example>")
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
APP_QUESTIONS = {
|
|
50
|
+
"run": "Which app do you want to run?",
|
|
51
|
+
"deploy": "Which app do you want to deploy?",
|
|
52
|
+
"login": "Which app's cloud provider do you want to sign in to?",
|
|
53
|
+
"destroy": "Which app do you want to destroy?",
|
|
54
|
+
"secrets": "Which app's secrets?",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
49
58
|
def choose_app(name: str | None, command: str) -> str | None:
|
|
50
59
|
"""Return the app `pdt <command>` should act on, or None after guiding the user.
|
|
51
60
|
|
|
@@ -60,7 +69,7 @@ def choose_app(name: str | None, command: str) -> str | None:
|
|
|
60
69
|
if name in apps:
|
|
61
70
|
return name
|
|
62
71
|
if name is None:
|
|
63
|
-
console.heading(f"
|
|
72
|
+
console.heading(f"{APP_QUESTIONS[command]} This project has:")
|
|
64
73
|
else:
|
|
65
74
|
console.error(f"no app named {name!r}. This project has:")
|
|
66
75
|
shown = apps[:5]
|
|
@@ -73,20 +82,25 @@ def choose_app(name: str | None, command: str) -> str | None:
|
|
|
73
82
|
return None
|
|
74
83
|
|
|
75
84
|
|
|
76
|
-
def cmd_list(
|
|
77
|
-
|
|
85
|
+
def cmd_list(args) -> int:
|
|
86
|
+
if args.names:
|
|
87
|
+
for name in config.find_apps():
|
|
88
|
+
console.say(name)
|
|
89
|
+
return 0
|
|
90
|
+
apps = config.app_folders()
|
|
78
91
|
if not apps:
|
|
79
92
|
say_no_apps()
|
|
80
93
|
return 0
|
|
81
94
|
rows = []
|
|
82
95
|
for name in apps:
|
|
96
|
+
enabled = "true" if config.is_enabled(name) else "false"
|
|
83
97
|
try:
|
|
84
98
|
app = config.merged_app(name)
|
|
85
99
|
rows.append([name, app["schedule"] or "-",
|
|
86
|
-
app["platform"].get("provider", "-")])
|
|
100
|
+
app["platform"].get("provider", "-"), enabled])
|
|
87
101
|
except ConfigError as e:
|
|
88
|
-
rows.append([name, "-", f"config error: {e}"])
|
|
89
|
-
console.table(["
|
|
102
|
+
rows.append([name, "-", f"config error: {e}", enabled])
|
|
103
|
+
console.table(["name", "schedule", "provider", "enabled"], rows, ["bold cyan"])
|
|
90
104
|
return 0
|
|
91
105
|
|
|
92
106
|
|
|
@@ -145,21 +159,28 @@ def cmd_deploy(args) -> int:
|
|
|
145
159
|
name = choose_app(args.app, "deploy")
|
|
146
160
|
if name is None:
|
|
147
161
|
return 1
|
|
148
|
-
return deploy.deploy(name, assume_yes=args.yes
|
|
162
|
+
return deploy.deploy(name, assume_yes=args.yes)
|
|
149
163
|
|
|
150
164
|
|
|
151
165
|
def cmd_login(args) -> int:
|
|
152
166
|
name = choose_app(args.app, "login")
|
|
153
167
|
if name is None:
|
|
154
168
|
return 1
|
|
155
|
-
return deploy.login(name
|
|
169
|
+
return deploy.login(name)
|
|
156
170
|
|
|
157
171
|
|
|
158
172
|
def cmd_destroy(args) -> int:
|
|
159
173
|
name = choose_app(args.app, "destroy")
|
|
160
174
|
if name is None:
|
|
161
175
|
return 1
|
|
162
|
-
return deploy.destroy(name, assume_yes=args.yes
|
|
176
|
+
return deploy.destroy(name, assume_yes=args.yes)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def cmd_secrets(args) -> int:
|
|
180
|
+
name = choose_app(args.app, "secrets")
|
|
181
|
+
if name is None:
|
|
182
|
+
return 1
|
|
183
|
+
return deploy.secrets(name, args.action, assume_yes=args.yes, name=args.name)
|
|
163
184
|
|
|
164
185
|
|
|
165
186
|
def cmd_storage(args) -> int:
|
|
@@ -196,7 +217,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
196
217
|
help="which example to copy; run `pdt examples` to see them")
|
|
197
218
|
source.completer = completion.examples
|
|
198
219
|
p.set_defaults(func=cmd_new)
|
|
199
|
-
add_parser("list", help="show every app")
|
|
220
|
+
p = add_parser("list", help="show every app")
|
|
221
|
+
p.add_argument("--names", action="store_true",
|
|
222
|
+
help="print only the name of each enabled app, one per line")
|
|
223
|
+
p.set_defaults(func=cmd_list)
|
|
200
224
|
add_parser("validate", help="check config and env").set_defaults(func=cmd_validate)
|
|
201
225
|
p = add_parser("run", help="run an app locally")
|
|
202
226
|
app = p.add_argument("app", nargs="?",
|
|
@@ -208,24 +232,30 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
208
232
|
help="the app's folder name; omit to see the choices")
|
|
209
233
|
app.completer = completion.apps
|
|
210
234
|
p.add_argument("--yes", action="store_true", help="skip the confirmation prompt")
|
|
211
|
-
profile = p.add_argument("--profile", help="AWS profile name (AWS only)")
|
|
212
|
-
profile.completer = completion.profiles
|
|
213
235
|
p.set_defaults(func=cmd_deploy)
|
|
214
236
|
p = add_parser("login", help="sign in again to an app's cloud provider")
|
|
215
237
|
app = p.add_argument("app", nargs="?",
|
|
216
238
|
help="the app's folder name; omit to see the choices")
|
|
217
239
|
app.completer = completion.apps
|
|
218
|
-
profile = p.add_argument("--profile", help="AWS profile name (AWS only)")
|
|
219
|
-
profile.completer = completion.profiles
|
|
220
240
|
p.set_defaults(func=cmd_login)
|
|
221
241
|
p = add_parser("destroy", help="tear down an app's deployed resources")
|
|
222
242
|
app = p.add_argument("app", nargs="?",
|
|
223
243
|
help="the app's folder name; omit to see the choices")
|
|
224
244
|
app.completer = completion.apps
|
|
225
245
|
p.add_argument("--yes", action="store_true", help="skip the confirmation prompt")
|
|
226
|
-
profile = p.add_argument("--profile", help="AWS profile name (AWS only)")
|
|
227
|
-
profile.completer = completion.profiles
|
|
228
246
|
p.set_defaults(func=cmd_destroy)
|
|
247
|
+
p = add_parser("secrets", help="compare, send, or fetch a deployed app's .env values")
|
|
248
|
+
app = p.add_argument("app", nargs="?",
|
|
249
|
+
help="the app's folder name; omit to see the choices")
|
|
250
|
+
app.completer = completion.apps
|
|
251
|
+
p.add_argument("action", nargs="?", choices=deploy_common.SECRET_ACTIONS, default="diff",
|
|
252
|
+
help="diff shows what save would change (the default); "
|
|
253
|
+
"save sends your .env values to the deployed app; "
|
|
254
|
+
"get copies the deployed values into a file; "
|
|
255
|
+
"set NAME puts one value, read from stdin, into the deployed app")
|
|
256
|
+
p.add_argument("name", nargs="?", help="the env var that set changes")
|
|
257
|
+
p.add_argument("--yes", action="store_true", help="skip the confirmation prompt")
|
|
258
|
+
p.set_defaults(func=cmd_secrets)
|
|
229
259
|
p = add_parser("storage", help="read or manage an app's data store")
|
|
230
260
|
app = p.add_argument("app", help="the app's folder name")
|
|
231
261
|
app.completer = completion.apps
|