pdt-cli 0.1.2__tar.gz → 0.1.4__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.2 → pdt_cli-0.1.4}/.gitignore +1 -0
- pdt_cli-0.1.4/LICENSE +9 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/PKG-INFO +28 -10
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/README.md +25 -8
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/ci/tests/test_claude_task.py +23 -20
- pdt_cli-0.1.4/ci/tests/test_close_stale_drafts.py +105 -0
- pdt_cli-0.1.4/ci/tests/test_score_task.py +679 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/pyproject.toml +3 -3
- pdt_cli-0.1.4/src/pdt/cli.py +527 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/completion.py +4 -2
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/config.py +182 -33
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/console.py +23 -6
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/deploy.py +71 -6
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/deploy_aws.py +225 -57
- pdt_cli-0.1.4/src/pdt/deploy_aws_batch.py +796 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/deploy_azure.py +57 -8
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/deploy_azure_container_apps.py +285 -39
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/deploy_common.py +66 -13
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/deploy_google_cloud.py +207 -38
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/deploy_windows.py +286 -47
- pdt_cli-0.1.4/src/pdt/download.py +64 -0
- pdt_cli-0.1.4/src/pdt/examples/powershell-report/config.yml +18 -0
- pdt_cli-0.1.4/src/pdt/examples/powershell-report/report.ps1 +24 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/gcloud_sdk.py +9 -39
- pdt_cli-0.1.4/src/pdt/powershell.py +752 -0
- pdt_cli-0.1.4/src/pdt/pwsh.py +120 -0
- pdt_cli-0.1.4/src/pdt/run_powershell.py +100 -0
- pdt_cli-0.1.4/src/pdt/run_windows_task.py +64 -0
- pdt_cli-0.1.4/src/pdt/runs_cli.py +349 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/scaffold.py +114 -5
- pdt_cli-0.1.4/src/pdt/scan_powershell.ps1 +170 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/storage_cli.py +1 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/utils/email_auth.py +3 -1
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/utils/env_secret.py +12 -4
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/utils/send_email.py +3 -2
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/utils/storage.py +5 -4
- pdt_cli-0.1.4/tests/test_app_name.py +41 -0
- pdt_cli-0.1.4/tests/test_aws_cli.py +25 -0
- pdt_cli-0.1.4/tests/test_aws_cost_estimate.py +20 -0
- pdt_cli-0.1.4/tests/test_aws_deployer_policy.py +148 -0
- pdt_cli-0.1.4/tests/test_aws_destroy.py +251 -0
- pdt_cli-0.1.4/tests/test_aws_inventory.py +63 -0
- pdt_cli-0.1.4/tests/test_aws_job_definition.py +127 -0
- pdt_cli-0.1.4/tests/test_aws_names.py +20 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_aws_not_found.py +4 -0
- pdt_cli-0.1.4/tests/test_aws_runs.py +121 -0
- pdt_cli-0.1.4/tests/test_aws_shared_deletes.py +143 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_azure_build.py +5 -5
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_azure_deleted_vault.py +5 -1
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_azure_environment.py +38 -4
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_azure_job_diagnostics.py +2 -1
- pdt_cli-0.1.4/tests/test_azure_job_name.py +79 -0
- pdt_cli-0.1.4/tests/test_azure_runs.py +117 -0
- pdt_cli-0.1.4/tests/test_azure_shared_locks.py +188 -0
- pdt_cli-0.1.4/tests/test_build_context.py +62 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_ci_azure.py +33 -0
- pdt_cli-0.1.4/tests/test_ci_google_cloud.py +200 -0
- pdt_cli-0.1.4/tests/test_cli.py +519 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_completion.py +35 -1
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_config_merge.py +61 -0
- pdt_cli-0.1.4/tests/test_deploy_state.py +116 -0
- pdt_cli-0.1.4/tests/test_dockerfile.py +177 -0
- pdt_cli-0.1.4/tests/test_download.py +83 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_env_sources.py +106 -1
- pdt_cli-0.1.4/tests/test_gcloud_sdk.py +52 -0
- pdt_cli-0.1.4/tests/test_google_cloud_inventory.py +73 -0
- pdt_cli-0.1.4/tests/test_google_cloud_retries.py +138 -0
- pdt_cli-0.1.4/tests/test_google_cloud_runs.py +154 -0
- pdt_cli-0.1.4/tests/test_google_cloud_storage.py +124 -0
- pdt_cli-0.1.4/tests/test_powershell_app_config.py +87 -0
- pdt_cli-0.1.4/tests/test_powershell_rules.py +428 -0
- pdt_cli-0.1.4/tests/test_private_file.py +73 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_project_discovery.py +30 -1
- pdt_cli-0.1.4/tests/test_pwsh.py +82 -0
- pdt_cli-0.1.4/tests/test_run_powershell.py +147 -0
- pdt_cli-0.1.4/tests/test_runs_cli.py +387 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_save_platform_key.py +60 -1
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_scaffold.py +80 -3
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_secrets.py +18 -0
- pdt_cli-0.1.4/tests/test_set_app_enabled.py +49 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_storage.py +1 -2
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_verify.py +117 -17
- pdt_cli-0.1.4/tests/test_windows_data.py +213 -0
- pdt_cli-0.1.4/tests/test_windows_runner.py +113 -0
- pdt_cli-0.1.4/tests/test_windows_runs.py +159 -0
- pdt_cli-0.1.4/tests/test_windows_storage.py +144 -0
- pdt_cli-0.1.4/tests/test_winget_launcher.py +66 -0
- pdt_cli-0.1.4/tests/test_winget_manifests.py +48 -0
- pdt_cli-0.1.4/verify/README.md +97 -0
- pdt_cli-0.1.2/ci/tests/test_close_stale_drafts.py +0 -79
- pdt_cli-0.1.2/ci/tests/test_rebase_task.py +0 -333
- pdt_cli-0.1.2/ci/tests/test_score_task.py +0 -640
- pdt_cli-0.1.2/src/pdt/cli.py +0 -299
- pdt_cli-0.1.2/src/pdt/deploy_aws_fargate.py +0 -508
- pdt_cli-0.1.2/tests/test_aws_inventory.py +0 -38
- pdt_cli-0.1.2/tests/test_aws_names.py +0 -9
- pdt_cli-0.1.2/tests/test_ci_google_cloud.py +0 -93
- pdt_cli-0.1.2/tests/test_cli.py +0 -104
- pdt_cli-0.1.2/tests/test_deploy_state.py +0 -52
- pdt_cli-0.1.2/tests/test_dockerfile.py +0 -79
- pdt_cli-0.1.2/tests/test_gcloud_sdk.py +0 -24
- pdt_cli-0.1.2/tests/test_google_cloud_inventory.py +0 -23
- pdt_cli-0.1.2/tests/test_google_cloud_retries.py +0 -41
- pdt_cli-0.1.2/tests/test_google_cloud_storage.py +0 -25
- pdt_cli-0.1.2/tests/test_windows_storage.py +0 -50
- pdt_cli-0.1.2/verify/README.md +0 -92
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/__init__.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/examples/hello-world/config.yml +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/examples/hello-world/run.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/examples/impossible-travel-report/config.yml +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/examples/impossible-travel-report/env.template +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/examples/impossible-travel-report/run.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/examples/monday-orphaned-account-report/config.yml +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/examples/monday-orphaned-account-report/env.template +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/examples/monday-orphaned-account-report/run.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/utils/__init__.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/utils/entra.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/utils/log.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/utils/send_email_graph.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/src/pdt/utils/web.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/conftest.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_aws_account.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_aws_profile.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_aws_storage.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_azure_environment_key.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_azure_group_ownership.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_azure_names.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_azure_retries.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_azure_side_resources.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_azure_storage.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_azure_subscription_state.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_console.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_cost_estimate.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_env_secret.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_fetch_json.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_google_cloud_not_found.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_google_cloud_service_account.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_init_questions.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_log_colour.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_run_log_links.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_schedule.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_starter_app.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_storage_cli.py +0 -0
- {pdt_cli-0.1.2 → pdt_cli-0.1.4}/tests/test_strict_validation.py +0 -0
pdt_cli-0.1.4/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AutoIDM
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pdt-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Easily deploy your process to AWS, Azure, Google Cloud, or Windows.
|
|
5
5
|
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Requires-Python: >=3.12
|
|
7
8
|
Requires-Dist: argcomplete
|
|
8
9
|
Requires-Dist: backoff
|
|
@@ -26,7 +27,7 @@ Requires-Dist: gcsfs; extra == 'apps'
|
|
|
26
27
|
Requires-Dist: google-cloud-secret-manager; extra == 'apps'
|
|
27
28
|
Requires-Dist: google-cloud-storage; extra == 'apps'
|
|
28
29
|
Requires-Dist: pyjwt; extra == 'apps'
|
|
29
|
-
Requires-Dist: s3fs; extra == 'apps'
|
|
30
|
+
Requires-Dist: s3fs>=2024; extra == 'apps'
|
|
30
31
|
Description-Content-Type: text/markdown
|
|
31
32
|
|
|
32
33
|
# pdt
|
|
@@ -41,7 +42,7 @@ uv tool install pdt-cli
|
|
|
41
42
|
|
|
42
43
|
That puts a `pdt` command on your PATH. To update it later, run `uv tool upgrade pdt-cli`.
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
On Windows you can run `winget install AutoIDM.pdt` instead, which also installs `uv` if you do not have it and puts the same `pdt` command on your PATH. `winget upgrade AutoIDM.pdt` moves pdt to the latest release, and the first `pdt` command after an install or upgrade finishes setting up that version.
|
|
45
46
|
|
|
46
47
|
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.
|
|
47
48
|
|
|
@@ -121,6 +122,7 @@ pdt login my-report
|
|
|
121
122
|
| `pdt validate` | check the config files and the required env vars |
|
|
122
123
|
| `pdt run APP` | run an app on this machine |
|
|
123
124
|
| `pdt deploy APP` | deploy an app to its configured platform |
|
|
125
|
+
| `pdt deploy --all` | deploy every enabled app, in order; asks whether to skip and disable an app that fails; add `--yes --skip-failures` to run unattended |
|
|
124
126
|
| `pdt destroy APP` | remove everything deploy created |
|
|
125
127
|
| `pdt secrets APP` | show which `.env` values differ from the deployed app |
|
|
126
128
|
| `pdt secrets APP save` | send your `.env` values to the deployed app; the next run uses them |
|
|
@@ -128,11 +130,14 @@ pdt login my-report
|
|
|
128
130
|
| `pdt secrets APP set NAME` | put one value, read from stdin, into the deployed app's secrets |
|
|
129
131
|
| `pdt login APP` | sign in again to the app's cloud provider |
|
|
130
132
|
| `pdt storage APP ls|get|query|destroy` | look at, fetch, query, or delete the app's stored files |
|
|
133
|
+
| `pdt runs APP` | list the deployed app's runs with each run's exit code: the 10 newest, or with `--since 3d` (or `12h`, `2w`, `2026-09-20`, `2026-09-20T14:00`) every run since then, `--span 1d` keeping only the runs within that long after `--since` and `--count N` keeping only the N newest; a run's number is its place among every run pdt can still find, so it stays the same whichever runs print |
|
|
134
|
+
| `pdt logs APP [N]` | read the log of run N as `pdt runs` numbers it; with no N, the newest run, and `--failed` picks the newest failed run instead; `--since`, `--span`, and `--count` limit which runs those two choose from, `--errors` leaves out DEBUG and INFO lines; the last 20 lines print, `--lines N` prints N instead, `--head` prints the first lines instead of the last, and `--full` prints every line |
|
|
135
|
+
| `pdt health [APP] [--all]` | show whether each app's last run succeeded; exits 1 when one failed; inside an app folder it checks only that app, and `--all` checks every app |
|
|
131
136
|
| `pdt az ...` | run the Azure CLI that pdt installs |
|
|
132
137
|
| `pdt gcloud ...` | run the Google Cloud CLI that pdt installs |
|
|
133
138
|
| `pdt completion [SHELL]` | turn on tab completion for a shell |
|
|
134
139
|
|
|
135
|
-
|
|
140
|
+
Inside an app folder, or any folder below it, leave `APP` off any command that takes one and pdt uses that app. For example, `pdt logs 3` in the `hello-world` folder reads run 3 of `hello-world`. Anywhere else, leave `APP` off or mistype it and pdt lists the apps in the project so you can pick one.
|
|
136
141
|
|
|
137
142
|
`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`.
|
|
138
143
|
|
|
@@ -140,7 +145,7 @@ Leave `APP` off `run`, `deploy`, `destroy`, `secrets`, or `login`, or mistype it
|
|
|
140
145
|
|
|
141
146
|
Your app runs, writes some files, and stops. Then the computer it ran on is thrown away, and the files go with it. So pdt gives every app a folder in your cloud account that stays. Deploy the app, destroy it, deploy it again: the folder and everything in it is still there.
|
|
142
147
|
|
|
143
|
-
pdt creates one bucket per cloud account, named `pdt-data-` plus a short code, and gives each app its own folder inside it. When your app runs in the cloud, `PDT_STORAGE_URL` points at that folder. When you run `pdt run APP` on your own computer, it points at `.pdt/storage/APP/` inside your project instead. The app code is the same in
|
|
148
|
+
pdt creates one bucket per cloud account, named `pdt-data-` plus a short code, and gives each app its own folder inside it. When your app runs in the cloud, `PDT_STORAGE_URL` points at that folder. A Windows scheduled task points it at `%ProgramData%\pdt\APP\storage\`. When you run `pdt run APP` on your own computer, it points at `.pdt/storage/APP/` inside your project instead. The app code is the same in every place.
|
|
144
149
|
|
|
145
150
|
Write and read files with `pdt.utils.storage`:
|
|
146
151
|
|
|
@@ -183,7 +188,7 @@ Every command reads its values from the environment it runs in. A build server s
|
|
|
183
188
|
|
|
184
189
|
## Choosing where jobs run
|
|
185
190
|
|
|
186
|
-
Set `platform:` in `pdt.yml` for every app, or in an app's own `config.yml` for one app. An app's own file wins.
|
|
191
|
+
Set `platform:` in `pdt.yml` for every app, or in an app's own `config.yml` for one app. An app's own file wins. `timezone` may live under `platform:` as the default for every app, and an app's own `timezone` overrides it.
|
|
187
192
|
|
|
188
193
|
### Azure
|
|
189
194
|
|
|
@@ -228,7 +233,7 @@ platform:
|
|
|
228
233
|
|
|
229
234
|
### Bringing your own Dockerfile
|
|
230
235
|
|
|
231
|
-
Every cloud provider runs a job as a container: AWS on Fargate, Azure on Container Apps Jobs, Google Cloud on Cloud Run Jobs. The deploy builds an image for each app from a generated Dockerfile. It copies the app folder and `pdt.yml` into `/workspace`, installs the script-header dependencies of `run.py` with `uv sync --script`, and runs `run.py` as the entrypoint.
|
|
236
|
+
Every cloud provider runs a job as a container: AWS on Batch (Fargate), Azure on Container Apps Jobs, Google Cloud on Cloud Run Jobs. The deploy builds an image for each app from a generated Dockerfile. It copies the app folder and `pdt.yml` into `/workspace`, installs the script-header dependencies of `run.py` with `uv sync --script`, and runs `run.py` as the entrypoint.
|
|
232
237
|
|
|
233
238
|
Put a `Dockerfile` in the app folder to build the image your own way, for example to add system packages or to install a heavy tool at build time instead of on every run. The build context is the same as the generated one: the app folder under its own name, next to `pdt.yml`. Start from the generated file:
|
|
234
239
|
|
|
@@ -236,7 +241,7 @@ Put a `Dockerfile` in the app folder to build the image your own way, for exampl
|
|
|
236
241
|
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
|
237
242
|
COPY . /workspace
|
|
238
243
|
WORKDIR /workspace/my-app
|
|
239
|
-
ENV PDT_PROJECT=/workspace
|
|
244
|
+
ENV PDT_PROJECT=/workspace NO_COLOR=1 DBT_USE_COLORS=false
|
|
240
245
|
RUN uv sync --script run.py
|
|
241
246
|
ENTRYPOINT ["uv", "run", "--script", "run.py"]
|
|
242
247
|
```
|
|
@@ -253,10 +258,11 @@ platform:
|
|
|
253
258
|
|
|
254
259
|
apps:
|
|
255
260
|
- name: my-report
|
|
256
|
-
timezone: local
|
|
257
261
|
schedule: daily
|
|
258
262
|
```
|
|
259
263
|
|
|
264
|
+
The Windows provider runs each job on the PC's local time, so `timezone` defaults to `local` there, the only value it accepts.
|
|
265
|
+
|
|
260
266
|
The job runs as the SYSTEM account. Windows accepts these schedules:
|
|
261
267
|
|
|
262
268
|
- `hourly`, `daily`, `weekly`, `monthly`, and `yearly`
|
|
@@ -267,6 +273,8 @@ The job runs as the SYSTEM account. Windows accepts these schedules:
|
|
|
267
273
|
|
|
268
274
|
Other cron forms are rejected, because they do not translate to Windows Task Scheduler.
|
|
269
275
|
|
|
276
|
+
Each app keeps its run data in `%ProgramData%\pdt\<app>\`. The task writes one log file per run to the `logs\` folder in it, named by the run's UTC start time, and `pdt runs <app>` and `pdt logs <app>` read those files. The app's own files (see [Keeping files between runs](#keeping-files-between-runs)) live in the `storage\` folder next to it. Deploy creates the folder and gives the SYSTEM account full control and you modify rights, so `pdt storage <app> destroy` works without an administrator. `pdt destroy <app>` removes the task and the `logs\` folder and keeps `storage\`. A run is stopped after 30 minutes, the same limit as the cloud providers, and a run that is still going when the next one is due is left alone; the new start is skipped.
|
|
277
|
+
|
|
270
278
|
## The bundled examples
|
|
271
279
|
|
|
272
280
|
Run `pdt examples` to list them, then `pdt new <name> --from <example>` to copy one.
|
|
@@ -290,9 +298,11 @@ Reports active Monday users whose email address does not match the `userPrincipa
|
|
|
290
298
|
| Item | Where |
|
|
291
299
|
| --- | --- |
|
|
292
300
|
| the `pdt` command and its code | the tool's own environment, from `uv tool install` |
|
|
301
|
+
| the `pdt` launcher from winget | `%LOCALAPPDATA%\Microsoft\WinGet\Packages`, linked from `%LOCALAPPDATA%\Microsoft\WinGet\Links` |
|
|
293
302
|
| the example apps | inside the pdt package, copied out by `pdt new` |
|
|
294
303
|
| your apps, `pdt.yml`, `.env` | your project folder, under version control |
|
|
295
304
|
| the Google Cloud CLI pdt downloads | `~/.local/share/pdt/gcloud`, or `%LOCALAPPDATA%\pdt\gcloud` |
|
|
305
|
+
| a Windows scheduled task's run logs and files | `%ProgramData%\pdt\<app>\logs` and `%ProgramData%\pdt\<app>\storage` |
|
|
296
306
|
| cloud sign-in state | `~/.azure` and `~/.config/gcloud`, as usual |
|
|
297
307
|
|
|
298
308
|
Set `PDT_PROJECT` to name the project folder directly, instead of letting pdt search upward. Deployed jobs get it set for them.
|
|
@@ -304,7 +314,7 @@ An app is a folder with a `run.py` that has a `main()` function. It declares its
|
|
|
304
314
|
```python
|
|
305
315
|
# /// script
|
|
306
316
|
# requires-python = ">=3.12"
|
|
307
|
-
# dependencies = ["pdt-cli[apps]==0.1.
|
|
317
|
+
# dependencies = ["pdt-cli[apps]==0.1.4"]
|
|
308
318
|
# ///
|
|
309
319
|
from pathlib import Path
|
|
310
320
|
|
|
@@ -320,6 +330,14 @@ def main() -> int:
|
|
|
320
330
|
|
|
321
331
|
The pinned version matters. A deployed job keeps using the version in its header, so upgrading pdt on your machine does not change a job already running in the cloud.
|
|
322
332
|
|
|
333
|
+
### PowerShell apps
|
|
334
|
+
|
|
335
|
+
A folder of `.ps1` files with no `run.py` is a PowerShell app: pdt reads the scripts to find the modules to install, then runs the entry scripts in name order through pwsh. A `run.ps1` file is the only entry script. Otherwise, use `run_scripts` in `config.yml` to choose entries and set their order. Do not use `run.ps1` and `run_scripts` together. By default, pdt stops after the first failed script. Set `continue_on_error: true` to run every entry script. The run still fails with the first script's exit code. To change either step, run `pdt new <app> --from-scripts`. It writes `requirements.psd1`, which lists the modules with pinned versions, and `run.py`, which runs the scripts. Once they exist, pdt installs exactly the modules in `requirements.psd1` (`pdt validate` warns about a module the scripts need that it leaves out), and `pdt run` and deploy run your `run.py`.
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
pdt new my-report --from-scripts
|
|
339
|
+
```
|
|
340
|
+
|
|
323
341
|
# Utilities
|
|
324
342
|
|
|
325
343
|
`pdt.utils` provides built-in support for common functionality:
|
|
@@ -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
|
-
|
|
13
|
+
On Windows you can run `winget install AutoIDM.pdt` instead, which also installs `uv` if you do not have it and puts the same `pdt` command on your PATH. `winget upgrade AutoIDM.pdt` moves pdt to the latest release, and the first `pdt` command after an install or upgrade finishes setting up that version.
|
|
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
|
|
|
@@ -90,6 +90,7 @@ pdt login my-report
|
|
|
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
|
+
| `pdt deploy --all` | deploy every enabled app, in order; asks whether to skip and disable an app that fails; add `--yes --skip-failures` to run unattended |
|
|
93
94
|
| `pdt destroy APP` | remove everything deploy created |
|
|
94
95
|
| `pdt secrets APP` | show which `.env` values differ from the deployed app |
|
|
95
96
|
| `pdt secrets APP save` | send your `.env` values to the deployed app; the next run uses them |
|
|
@@ -97,11 +98,14 @@ pdt login my-report
|
|
|
97
98
|
| `pdt secrets APP set NAME` | put one value, read from stdin, into the deployed app's secrets |
|
|
98
99
|
| `pdt login APP` | sign in again to the app's cloud provider |
|
|
99
100
|
| `pdt storage APP ls|get|query|destroy` | look at, fetch, query, or delete the app's stored files |
|
|
101
|
+
| `pdt runs APP` | list the deployed app's runs with each run's exit code: the 10 newest, or with `--since 3d` (or `12h`, `2w`, `2026-09-20`, `2026-09-20T14:00`) every run since then, `--span 1d` keeping only the runs within that long after `--since` and `--count N` keeping only the N newest; a run's number is its place among every run pdt can still find, so it stays the same whichever runs print |
|
|
102
|
+
| `pdt logs APP [N]` | read the log of run N as `pdt runs` numbers it; with no N, the newest run, and `--failed` picks the newest failed run instead; `--since`, `--span`, and `--count` limit which runs those two choose from, `--errors` leaves out DEBUG and INFO lines; the last 20 lines print, `--lines N` prints N instead, `--head` prints the first lines instead of the last, and `--full` prints every line |
|
|
103
|
+
| `pdt health [APP] [--all]` | show whether each app's last run succeeded; exits 1 when one failed; inside an app folder it checks only that app, and `--all` checks every app |
|
|
100
104
|
| `pdt az ...` | run the Azure CLI that pdt installs |
|
|
101
105
|
| `pdt gcloud ...` | run the Google Cloud CLI that pdt installs |
|
|
102
106
|
| `pdt completion [SHELL]` | turn on tab completion for a shell |
|
|
103
107
|
|
|
104
|
-
|
|
108
|
+
Inside an app folder, or any folder below it, leave `APP` off any command that takes one and pdt uses that app. For example, `pdt logs 3` in the `hello-world` folder reads run 3 of `hello-world`. Anywhere else, leave `APP` off or mistype it and pdt lists the apps in the project so you can pick one.
|
|
105
109
|
|
|
106
110
|
`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`.
|
|
107
111
|
|
|
@@ -109,7 +113,7 @@ Leave `APP` off `run`, `deploy`, `destroy`, `secrets`, or `login`, or mistype it
|
|
|
109
113
|
|
|
110
114
|
Your app runs, writes some files, and stops. Then the computer it ran on is thrown away, and the files go with it. So pdt gives every app a folder in your cloud account that stays. Deploy the app, destroy it, deploy it again: the folder and everything in it is still there.
|
|
111
115
|
|
|
112
|
-
pdt creates one bucket per cloud account, named `pdt-data-` plus a short code, and gives each app its own folder inside it. When your app runs in the cloud, `PDT_STORAGE_URL` points at that folder. When you run `pdt run APP` on your own computer, it points at `.pdt/storage/APP/` inside your project instead. The app code is the same in
|
|
116
|
+
pdt creates one bucket per cloud account, named `pdt-data-` plus a short code, and gives each app its own folder inside it. When your app runs in the cloud, `PDT_STORAGE_URL` points at that folder. A Windows scheduled task points it at `%ProgramData%\pdt\APP\storage\`. When you run `pdt run APP` on your own computer, it points at `.pdt/storage/APP/` inside your project instead. The app code is the same in every place.
|
|
113
117
|
|
|
114
118
|
Write and read files with `pdt.utils.storage`:
|
|
115
119
|
|
|
@@ -152,7 +156,7 @@ Every command reads its values from the environment it runs in. A build server s
|
|
|
152
156
|
|
|
153
157
|
## Choosing where jobs run
|
|
154
158
|
|
|
155
|
-
Set `platform:` in `pdt.yml` for every app, or in an app's own `config.yml` for one app. An app's own file wins.
|
|
159
|
+
Set `platform:` in `pdt.yml` for every app, or in an app's own `config.yml` for one app. An app's own file wins. `timezone` may live under `platform:` as the default for every app, and an app's own `timezone` overrides it.
|
|
156
160
|
|
|
157
161
|
### Azure
|
|
158
162
|
|
|
@@ -197,7 +201,7 @@ platform:
|
|
|
197
201
|
|
|
198
202
|
### Bringing your own Dockerfile
|
|
199
203
|
|
|
200
|
-
Every cloud provider runs a job as a container: AWS on Fargate, Azure on Container Apps Jobs, Google Cloud on Cloud Run Jobs. The deploy builds an image for each app from a generated Dockerfile. It copies the app folder and `pdt.yml` into `/workspace`, installs the script-header dependencies of `run.py` with `uv sync --script`, and runs `run.py` as the entrypoint.
|
|
204
|
+
Every cloud provider runs a job as a container: AWS on Batch (Fargate), Azure on Container Apps Jobs, Google Cloud on Cloud Run Jobs. The deploy builds an image for each app from a generated Dockerfile. It copies the app folder and `pdt.yml` into `/workspace`, installs the script-header dependencies of `run.py` with `uv sync --script`, and runs `run.py` as the entrypoint.
|
|
201
205
|
|
|
202
206
|
Put a `Dockerfile` in the app folder to build the image your own way, for example to add system packages or to install a heavy tool at build time instead of on every run. The build context is the same as the generated one: the app folder under its own name, next to `pdt.yml`. Start from the generated file:
|
|
203
207
|
|
|
@@ -205,7 +209,7 @@ Put a `Dockerfile` in the app folder to build the image your own way, for exampl
|
|
|
205
209
|
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
|
206
210
|
COPY . /workspace
|
|
207
211
|
WORKDIR /workspace/my-app
|
|
208
|
-
ENV PDT_PROJECT=/workspace
|
|
212
|
+
ENV PDT_PROJECT=/workspace NO_COLOR=1 DBT_USE_COLORS=false
|
|
209
213
|
RUN uv sync --script run.py
|
|
210
214
|
ENTRYPOINT ["uv", "run", "--script", "run.py"]
|
|
211
215
|
```
|
|
@@ -222,10 +226,11 @@ platform:
|
|
|
222
226
|
|
|
223
227
|
apps:
|
|
224
228
|
- name: my-report
|
|
225
|
-
timezone: local
|
|
226
229
|
schedule: daily
|
|
227
230
|
```
|
|
228
231
|
|
|
232
|
+
The Windows provider runs each job on the PC's local time, so `timezone` defaults to `local` there, the only value it accepts.
|
|
233
|
+
|
|
229
234
|
The job runs as the SYSTEM account. Windows accepts these schedules:
|
|
230
235
|
|
|
231
236
|
- `hourly`, `daily`, `weekly`, `monthly`, and `yearly`
|
|
@@ -236,6 +241,8 @@ The job runs as the SYSTEM account. Windows accepts these schedules:
|
|
|
236
241
|
|
|
237
242
|
Other cron forms are rejected, because they do not translate to Windows Task Scheduler.
|
|
238
243
|
|
|
244
|
+
Each app keeps its run data in `%ProgramData%\pdt\<app>\`. The task writes one log file per run to the `logs\` folder in it, named by the run's UTC start time, and `pdt runs <app>` and `pdt logs <app>` read those files. The app's own files (see [Keeping files between runs](#keeping-files-between-runs)) live in the `storage\` folder next to it. Deploy creates the folder and gives the SYSTEM account full control and you modify rights, so `pdt storage <app> destroy` works without an administrator. `pdt destroy <app>` removes the task and the `logs\` folder and keeps `storage\`. A run is stopped after 30 minutes, the same limit as the cloud providers, and a run that is still going when the next one is due is left alone; the new start is skipped.
|
|
245
|
+
|
|
239
246
|
## The bundled examples
|
|
240
247
|
|
|
241
248
|
Run `pdt examples` to list them, then `pdt new <name> --from <example>` to copy one.
|
|
@@ -259,9 +266,11 @@ Reports active Monday users whose email address does not match the `userPrincipa
|
|
|
259
266
|
| Item | Where |
|
|
260
267
|
| --- | --- |
|
|
261
268
|
| the `pdt` command and its code | the tool's own environment, from `uv tool install` |
|
|
269
|
+
| the `pdt` launcher from winget | `%LOCALAPPDATA%\Microsoft\WinGet\Packages`, linked from `%LOCALAPPDATA%\Microsoft\WinGet\Links` |
|
|
262
270
|
| the example apps | inside the pdt package, copied out by `pdt new` |
|
|
263
271
|
| your apps, `pdt.yml`, `.env` | your project folder, under version control |
|
|
264
272
|
| the Google Cloud CLI pdt downloads | `~/.local/share/pdt/gcloud`, or `%LOCALAPPDATA%\pdt\gcloud` |
|
|
273
|
+
| a Windows scheduled task's run logs and files | `%ProgramData%\pdt\<app>\logs` and `%ProgramData%\pdt\<app>\storage` |
|
|
265
274
|
| cloud sign-in state | `~/.azure` and `~/.config/gcloud`, as usual |
|
|
266
275
|
|
|
267
276
|
Set `PDT_PROJECT` to name the project folder directly, instead of letting pdt search upward. Deployed jobs get it set for them.
|
|
@@ -273,7 +282,7 @@ An app is a folder with a `run.py` that has a `main()` function. It declares its
|
|
|
273
282
|
```python
|
|
274
283
|
# /// script
|
|
275
284
|
# requires-python = ">=3.12"
|
|
276
|
-
# dependencies = ["pdt-cli[apps]==0.1.
|
|
285
|
+
# dependencies = ["pdt-cli[apps]==0.1.4"]
|
|
277
286
|
# ///
|
|
278
287
|
from pathlib import Path
|
|
279
288
|
|
|
@@ -289,6 +298,14 @@ def main() -> int:
|
|
|
289
298
|
|
|
290
299
|
The pinned version matters. A deployed job keeps using the version in its header, so upgrading pdt on your machine does not change a job already running in the cloud.
|
|
291
300
|
|
|
301
|
+
### PowerShell apps
|
|
302
|
+
|
|
303
|
+
A folder of `.ps1` files with no `run.py` is a PowerShell app: pdt reads the scripts to find the modules to install, then runs the entry scripts in name order through pwsh. A `run.ps1` file is the only entry script. Otherwise, use `run_scripts` in `config.yml` to choose entries and set their order. Do not use `run.ps1` and `run_scripts` together. By default, pdt stops after the first failed script. Set `continue_on_error: true` to run every entry script. The run still fails with the first script's exit code. To change either step, run `pdt new <app> --from-scripts`. It writes `requirements.psd1`, which lists the modules with pinned versions, and `run.py`, which runs the scripts. Once they exist, pdt installs exactly the modules in `requirements.psd1` (`pdt validate` warns about a module the scripts need that it leaves out), and `pdt run` and deploy run your `run.py`.
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
pdt new my-report --from-scripts
|
|
307
|
+
```
|
|
308
|
+
|
|
292
309
|
# Utilities
|
|
293
310
|
|
|
294
311
|
`pdt.utils` provides built-in support for common functionality:
|
|
@@ -91,18 +91,6 @@ def test_missing_task_or_prompt(tmp_path):
|
|
|
91
91
|
runner.load_task("bare", tmp_path)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
def test_shipped_rebase_task_loads():
|
|
95
|
-
task = runner.load_task("rebase-mrs")
|
|
96
|
-
assert task.model == "opus"
|
|
97
|
-
assert task.select.name == "select_mrs.py"
|
|
98
|
-
assert task.check.name == "check_mrs.py"
|
|
99
|
-
assert "Bash(git push --force-with-lease*)" in task.allowed_tools
|
|
100
|
-
assert not any("git push -f" in tool for tool in task.allowed_tools)
|
|
101
|
-
assert runner.render_prompt(task.prompt, {
|
|
102
|
-
"source_branch": "b", "target_branch": "master", "iid": 1, "title": "t",
|
|
103
|
-
"before_sha": "abc", "ahead": 2})
|
|
104
|
-
|
|
105
|
-
|
|
106
94
|
def test_render_prompt_fills_and_names_missing_fields():
|
|
107
95
|
assert runner.render_prompt("Rebase {branch} for !{iid}", {"branch": "x", "iid": 7}) == \
|
|
108
96
|
"Rebase x for !7"
|
|
@@ -126,7 +114,7 @@ def test_missing_token_message_names_the_variable():
|
|
|
126
114
|
assert runner.missing_token_problem({"CLAUDE_CODE_OAUTH_TOKEN": "tok"}) == ""
|
|
127
115
|
|
|
128
116
|
|
|
129
|
-
def
|
|
117
|
+
def test_claude_env_maps_the_secret_name_to_the_name_claude_reads():
|
|
130
118
|
env = runner.claude_env({"CLAUDE_TOKEN": "tok", "PATH": "/bin"})
|
|
131
119
|
assert env["CLAUDE_CODE_OAUTH_TOKEN"] == "tok"
|
|
132
120
|
assert env["PATH"] == "/bin"
|
|
@@ -193,6 +181,28 @@ def test_exit_codes():
|
|
|
193
181
|
assert runner.exit_code(["needs_human", "error"]) == 1
|
|
194
182
|
|
|
195
183
|
|
|
184
|
+
def test_run_url_links_the_workflow_run():
|
|
185
|
+
env = {"GITHUB_SERVER_URL": "https://github.com", "GITHUB_REPOSITORY": "AutoIDM/pdt",
|
|
186
|
+
"GITHUB_RUN_ID": "77"}
|
|
187
|
+
assert runner.run_url(env) == "https://github.com/AutoIDM/pdt/actions/runs/77"
|
|
188
|
+
assert runner.run_url({}) == ""
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_an_item_that_needs_a_person_prints_a_warning_annotation(tmp_path, monkeypatch, capsys):
|
|
192
|
+
make_task(tmp_path)
|
|
193
|
+
load = runner.load_task
|
|
194
|
+
monkeypatch.setattr(runner, "load_task", lambda name: load(name, tmp_path))
|
|
195
|
+
monkeypatch.setattr(runner, "verify_token", lambda task, env, report_dir: "")
|
|
196
|
+
monkeypatch.setattr(runner, "REPORT_DIR", tmp_path / "report")
|
|
197
|
+
monkeypatch.setattr(runner, "run_items", lambda *a: [
|
|
198
|
+
{"id": "7", "status": "needs_human", "message": "needs discussion:\ncli.py"},
|
|
199
|
+
{"id": "8", "status": "ok", "message": "tier::simple"}])
|
|
200
|
+
assert runner.main(["demo"]) == runner.EXIT_NEEDS_HUMAN
|
|
201
|
+
warnings = [line for line in capsys.readouterr().out.splitlines()
|
|
202
|
+
if line.startswith("::warning")]
|
|
203
|
+
assert warnings == ["::warning title=demo 7 needs a person::needs discussion: cli.py"]
|
|
204
|
+
|
|
205
|
+
|
|
196
206
|
def test_claude_command_shape(tmp_path):
|
|
197
207
|
task = runner.load_task("demo", make_task(
|
|
198
208
|
tmp_path, "allowed_tools: [Read, 'Bash(git *)']\ndisallowed_tools: ['Bash(git push -f*)']\n"))
|
|
@@ -223,13 +233,6 @@ def test_parallel_parses_and_defaults_to_one(tmp_path):
|
|
|
223
233
|
runner.load_task("demo", make_task(tmp_path / "zero", "parallel: 0\n"))
|
|
224
234
|
|
|
225
235
|
|
|
226
|
-
def test_claude_env_makes_a_relative_uv_cache_absolute(tmp_path):
|
|
227
|
-
env = runner.claude_env({"UV_CACHE_DIR": ".uv-cache"}, cwd=tmp_path)
|
|
228
|
-
assert env["UV_CACHE_DIR"] == str(tmp_path.resolve() / ".uv-cache")
|
|
229
|
-
assert runner.claude_env({"UV_CACHE_DIR": "/abs/cache"})["UV_CACHE_DIR"] == "/abs/cache"
|
|
230
|
-
assert "UV_CACHE_DIR" not in runner.claude_env({})
|
|
231
|
-
|
|
232
|
-
|
|
233
236
|
def test_items_run_side_by_side_and_outcomes_keep_order(tmp_path, monkeypatch):
|
|
234
237
|
task = runner.load_task("demo", make_task(tmp_path, "parallel: 3\n"))
|
|
235
238
|
started = threading.Barrier(3, timeout=5)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""close_stale_drafts.py: what counts as a person touching a Draft PR."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import date
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
SCRIPT = Path(__file__).resolve().parent.parent / "close_stale_drafts.py"
|
|
11
|
+
REPO = SCRIPT.parent.parent
|
|
12
|
+
|
|
13
|
+
spec = importlib.util.spec_from_file_location("close_stale_drafts", SCRIPT)
|
|
14
|
+
stale = importlib.util.module_from_spec(spec)
|
|
15
|
+
sys.modules["close_stale_drafts"] = stale
|
|
16
|
+
spec.loader.exec_module(stale)
|
|
17
|
+
|
|
18
|
+
BOT = "pdt-bot"
|
|
19
|
+
PERSON = "alice"
|
|
20
|
+
TODAY = date(2026, 9, 15) # a Tuesday
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def pr(**overrides):
|
|
24
|
+
base = {"number": 5, "title": "thing", "draft": True,
|
|
25
|
+
"created_at": "2026-08-03T09:00:00Z"}
|
|
26
|
+
return {**base, **overrides}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def commit(authored):
|
|
30
|
+
return {"commit": {"author": {"date": authored}}}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def comment(login, when):
|
|
34
|
+
return {"user": {"login": login}, "updated_at": when}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def review(login, when):
|
|
38
|
+
return {"user": {"login": login}, "submitted_at": when}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_business_days_skip_weekends():
|
|
42
|
+
friday, monday = date(2026, 9, 11), date(2026, 9, 14)
|
|
43
|
+
assert stale.business_days_since(friday, monday) == 1
|
|
44
|
+
assert stale.business_days_since(friday, TODAY) == 2
|
|
45
|
+
assert stale.business_days_since(TODAY, TODAY) == 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_opening_the_pr_counts():
|
|
49
|
+
assert stale.last_person_activity(pr(), [], [], [], BOT) == date(2026, 8, 3)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_the_token_user_and_bots_do_not_count():
|
|
53
|
+
"""A label or comment from score-prs, or any bot, leaves no trace."""
|
|
54
|
+
commits = [commit("2026-08-04T10:00:00Z")]
|
|
55
|
+
comments = [comment(BOT, "2026-09-14T10:00:05Z"),
|
|
56
|
+
comment("github-actions[bot]", "2026-09-14T10:00:06Z"),
|
|
57
|
+
comment("dependabot[bot]", "2026-09-14T10:00:07Z")]
|
|
58
|
+
reviews = [review("copilot-pull-request-reviewer[bot]", "2026-09-14T11:00:00Z")]
|
|
59
|
+
assert stale.last_person_activity(pr(), commits, comments, reviews, BOT) == date(2026, 8, 4)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_a_person_committing_commenting_or_reviewing_counts():
|
|
63
|
+
commits = [commit("2026-08-04T10:00:00Z")]
|
|
64
|
+
comments = [comment(BOT, "2026-09-14T10:00:00Z"), comment(PERSON, "2026-09-09T15:00:00Z")]
|
|
65
|
+
reviews = [review(PERSON, "2026-09-10T15:00:00Z"), {"user": {"login": PERSON},
|
|
66
|
+
"submitted_at": None}]
|
|
67
|
+
assert stale.last_person_activity(pr(), commits, comments, reviews, BOT) == date(2026, 9, 10)
|
|
68
|
+
commits.append(commit("2026-09-12T08:00:00Z"))
|
|
69
|
+
assert stale.last_person_activity(pr(), commits, comments, reviews, BOT) == date(2026, 9, 12)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_timestamps_are_read_in_utc():
|
|
73
|
+
comments = [comment(PERSON, "2026-09-10T22:00:00-05:00")]
|
|
74
|
+
assert stale.last_person_activity(pr(), [], comments, [], BOT) == date(2026, 9, 11)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_stale_needs_more_than_the_limit_and_a_draft():
|
|
78
|
+
limit = stale.STALE_BUSINESS_DAYS
|
|
79
|
+
assert stale.business_days_since(date(2026, 9, 8), TODAY) == limit
|
|
80
|
+
assert not stale.is_stale(pr(), date(2026, 9, 8), TODAY)
|
|
81
|
+
assert stale.is_stale(pr(), date(2026, 9, 7), TODAY)
|
|
82
|
+
assert not stale.is_stale(pr(draft=False), date(2026, 8, 1), TODAY)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_dry_run_is_the_default():
|
|
86
|
+
assert stale.dry_run_wanted({})
|
|
87
|
+
assert stale.dry_run_wanted({"DRY_RUN": "False "}) is False
|
|
88
|
+
assert stale.dry_run_wanted({"DRY_RUN": "no"})
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_slack_text_lists_each_closed_pr():
|
|
92
|
+
text = stale.slack_text("AutoIDM/pdt", [pr(html_url="https://github.com/AutoIDM/pdt/pull/5")])
|
|
93
|
+
assert text.splitlines() == [
|
|
94
|
+
"Closed 1 stale Draft PR(s) in AutoIDM/pdt (nobody touched them for more than "
|
|
95
|
+
"5 business days):",
|
|
96
|
+
"• #5 thing https://github.com/AutoIDM/pdt/pull/5"]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_workflow_runs_armed_on_its_schedule_and_dry_by_hand():
|
|
100
|
+
flow = yaml.safe_load((REPO / ".github" / "workflows" / "close-stale-drafts.yml").read_text())
|
|
101
|
+
on = flow[True] # YAML 1.1 reads the bare key on as true
|
|
102
|
+
assert on["schedule"] and on["workflow_dispatch"]["inputs"]["dry_run"]["default"] is True
|
|
103
|
+
env = flow["jobs"]["close-stale-drafts"]["env"]
|
|
104
|
+
assert "inputs.dry_run" in env["DRY_RUN"]
|
|
105
|
+
assert "secrets.SLACK_WEBHOOK_URL" in env["SLACK_WEBHOOK_URL"]
|