pdt-cli 0.1.3__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.3 → pdt_cli-0.1.4}/PKG-INFO +12 -4
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/README.md +11 -3
- {pdt_cli-0.1.3 → 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.3 → pdt_cli-0.1.4}/pyproject.toml +1 -1
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/cli.py +158 -59
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/completion.py +3 -1
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/config.py +71 -9
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy.py +6 -1
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_aws.py +25 -4
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_azure_container_apps.py +98 -23
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_common.py +39 -5
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_google_cloud.py +5 -4
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_windows.py +84 -9
- 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.3 → pdt_cli-0.1.4}/src/pdt/gcloud_sdk.py +2 -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.3 → pdt_cli-0.1.4}/src/pdt/run_windows_task.py +7 -2
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/scaffold.py +107 -5
- pdt_cli-0.1.4/src/pdt/scan_powershell.ps1 +170 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/storage_cli.py +1 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/env_secret.py +3 -3
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/send_email.py +3 -2
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/storage.py +1 -1
- pdt_cli-0.1.4/tests/test_aws_cli.py +25 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_inventory.py +2 -2
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_build.py +5 -5
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_environment.py +35 -1
- {pdt_cli-0.1.3 → 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.3 → pdt_cli-0.1.4}/tests/test_azure_runs.py +1 -1
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_shared_locks.py +3 -2
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_cli.py +194 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_deploy_state.py +14 -1
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_dockerfile.py +81 -1
- pdt_cli-0.1.4/tests/test_download.py +83 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_env_sources.py +106 -1
- pdt_cli-0.1.4/tests/test_google_cloud_inventory.py +73 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_google_cloud_retries.py +14 -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.3 → 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.3 → pdt_cli-0.1.4}/tests/test_scaffold.py +80 -3
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_secrets.py +18 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_verify.py +101 -18
- pdt_cli-0.1.4/tests/test_windows_data.py +213 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_windows_runner.py +17 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_windows_runs.py +1 -1
- pdt_cli-0.1.4/verify/README.md +97 -0
- pdt_cli-0.1.3/ci/tests/test_close_stale_drafts.py +0 -79
- pdt_cli-0.1.3/ci/tests/test_rebase_task.py +0 -333
- pdt_cli-0.1.3/ci/tests/test_score_task.py +0 -640
- pdt_cli-0.1.3/tests/test_google_cloud_inventory.py +0 -23
- pdt_cli-0.1.3/tests/test_windows_data.py +0 -101
- pdt_cli-0.1.3/verify/README.md +0 -92
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/.gitignore +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/LICENSE +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/__init__.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/console.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_aws_batch.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_azure.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/hello-world/config.yml +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/hello-world/run.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/impossible-travel-report/config.yml +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/impossible-travel-report/env.template +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/impossible-travel-report/run.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/monday-orphaned-account-report/config.yml +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/monday-orphaned-account-report/env.template +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/monday-orphaned-account-report/run.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/runs_cli.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/__init__.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/email_auth.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/entra.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/log.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/send_email_graph.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/web.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/conftest.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_app_name.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_account.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_cost_estimate.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_deployer_policy.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_destroy.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_job_definition.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_names.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_not_found.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_profile.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_runs.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_shared_deletes.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_storage.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_deleted_vault.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_environment_key.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_group_ownership.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_names.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_retries.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_side_resources.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_storage.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_subscription_state.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_build_context.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_ci_azure.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_ci_google_cloud.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_completion.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_config_merge.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_console.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_cost_estimate.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_env_secret.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_fetch_json.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_gcloud_sdk.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_google_cloud_not_found.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_google_cloud_runs.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_google_cloud_service_account.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_google_cloud_storage.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_init_questions.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_log_colour.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_private_file.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_run_log_links.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_runs_cli.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_save_platform_key.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_schedule.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_set_app_enabled.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_starter_app.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_storage.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_storage_cli.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_strict_validation.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_windows_storage.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_winget_launcher.py +0 -0
- {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_winget_manifests.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.4
|
|
4
4
|
Summary: Easily deploy your process to AWS, Azure, Google Cloud, or Windows.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -132,12 +132,12 @@ pdt login my-report
|
|
|
132
132
|
| `pdt storage APP ls|get|query|destroy` | look at, fetch, query, or delete the app's stored files |
|
|
133
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
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]` | show whether each app's last run succeeded; exits 1 when one failed |
|
|
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 |
|
|
136
136
|
| `pdt az ...` | run the Azure CLI that pdt installs |
|
|
137
137
|
| `pdt gcloud ...` | run the Google Cloud CLI that pdt installs |
|
|
138
138
|
| `pdt completion [SHELL]` | turn on tab completion for a shell |
|
|
139
139
|
|
|
140
|
-
|
|
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.
|
|
141
141
|
|
|
142
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`.
|
|
143
143
|
|
|
@@ -314,7 +314,7 @@ An app is a folder with a `run.py` that has a `main()` function. It declares its
|
|
|
314
314
|
```python
|
|
315
315
|
# /// script
|
|
316
316
|
# requires-python = ">=3.12"
|
|
317
|
-
# dependencies = ["pdt-cli[apps]==0.1.
|
|
317
|
+
# dependencies = ["pdt-cli[apps]==0.1.4"]
|
|
318
318
|
# ///
|
|
319
319
|
from pathlib import Path
|
|
320
320
|
|
|
@@ -330,6 +330,14 @@ def main() -> int:
|
|
|
330
330
|
|
|
331
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.
|
|
332
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
|
+
|
|
333
341
|
# Utilities
|
|
334
342
|
|
|
335
343
|
`pdt.utils` provides built-in support for common functionality:
|
|
@@ -100,12 +100,12 @@ pdt login my-report
|
|
|
100
100
|
| `pdt storage APP ls|get|query|destroy` | look at, fetch, query, or delete the app's stored files |
|
|
101
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
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]` | show whether each app's last run succeeded; exits 1 when one failed |
|
|
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 |
|
|
104
104
|
| `pdt az ...` | run the Azure CLI that pdt installs |
|
|
105
105
|
| `pdt gcloud ...` | run the Google Cloud CLI that pdt installs |
|
|
106
106
|
| `pdt completion [SHELL]` | turn on tab completion for a shell |
|
|
107
107
|
|
|
108
|
-
|
|
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.
|
|
109
109
|
|
|
110
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`.
|
|
111
111
|
|
|
@@ -282,7 +282,7 @@ An app is a folder with a `run.py` that has a `main()` function. It declares its
|
|
|
282
282
|
```python
|
|
283
283
|
# /// script
|
|
284
284
|
# requires-python = ">=3.12"
|
|
285
|
-
# dependencies = ["pdt-cli[apps]==0.1.
|
|
285
|
+
# dependencies = ["pdt-cli[apps]==0.1.4"]
|
|
286
286
|
# ///
|
|
287
287
|
from pathlib import Path
|
|
288
288
|
|
|
@@ -298,6 +298,14 @@ def main() -> int:
|
|
|
298
298
|
|
|
299
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.
|
|
300
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
|
+
|
|
301
309
|
# Utilities
|
|
302
310
|
|
|
303
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"]
|