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.
Files changed (133) hide show
  1. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/PKG-INFO +12 -4
  2. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/README.md +11 -3
  3. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/ci/tests/test_claude_task.py +23 -20
  4. pdt_cli-0.1.4/ci/tests/test_close_stale_drafts.py +105 -0
  5. pdt_cli-0.1.4/ci/tests/test_score_task.py +679 -0
  6. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/pyproject.toml +1 -1
  7. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/cli.py +158 -59
  8. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/completion.py +3 -1
  9. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/config.py +71 -9
  10. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy.py +6 -1
  11. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_aws.py +25 -4
  12. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_azure_container_apps.py +98 -23
  13. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_common.py +39 -5
  14. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_google_cloud.py +5 -4
  15. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_windows.py +84 -9
  16. pdt_cli-0.1.4/src/pdt/download.py +64 -0
  17. pdt_cli-0.1.4/src/pdt/examples/powershell-report/config.yml +18 -0
  18. pdt_cli-0.1.4/src/pdt/examples/powershell-report/report.ps1 +24 -0
  19. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/gcloud_sdk.py +2 -39
  20. pdt_cli-0.1.4/src/pdt/powershell.py +752 -0
  21. pdt_cli-0.1.4/src/pdt/pwsh.py +120 -0
  22. pdt_cli-0.1.4/src/pdt/run_powershell.py +100 -0
  23. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/run_windows_task.py +7 -2
  24. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/scaffold.py +107 -5
  25. pdt_cli-0.1.4/src/pdt/scan_powershell.ps1 +170 -0
  26. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/storage_cli.py +1 -0
  27. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/env_secret.py +3 -3
  28. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/send_email.py +3 -2
  29. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/storage.py +1 -1
  30. pdt_cli-0.1.4/tests/test_aws_cli.py +25 -0
  31. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_inventory.py +2 -2
  32. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_build.py +5 -5
  33. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_environment.py +35 -1
  34. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_job_diagnostics.py +2 -1
  35. pdt_cli-0.1.4/tests/test_azure_job_name.py +79 -0
  36. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_runs.py +1 -1
  37. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_shared_locks.py +3 -2
  38. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_cli.py +194 -0
  39. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_deploy_state.py +14 -1
  40. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_dockerfile.py +81 -1
  41. pdt_cli-0.1.4/tests/test_download.py +83 -0
  42. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_env_sources.py +106 -1
  43. pdt_cli-0.1.4/tests/test_google_cloud_inventory.py +73 -0
  44. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_google_cloud_retries.py +14 -0
  45. pdt_cli-0.1.4/tests/test_powershell_app_config.py +87 -0
  46. pdt_cli-0.1.4/tests/test_powershell_rules.py +428 -0
  47. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_project_discovery.py +30 -1
  48. pdt_cli-0.1.4/tests/test_pwsh.py +82 -0
  49. pdt_cli-0.1.4/tests/test_run_powershell.py +147 -0
  50. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_scaffold.py +80 -3
  51. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_secrets.py +18 -0
  52. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_verify.py +101 -18
  53. pdt_cli-0.1.4/tests/test_windows_data.py +213 -0
  54. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_windows_runner.py +17 -0
  55. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_windows_runs.py +1 -1
  56. pdt_cli-0.1.4/verify/README.md +97 -0
  57. pdt_cli-0.1.3/ci/tests/test_close_stale_drafts.py +0 -79
  58. pdt_cli-0.1.3/ci/tests/test_rebase_task.py +0 -333
  59. pdt_cli-0.1.3/ci/tests/test_score_task.py +0 -640
  60. pdt_cli-0.1.3/tests/test_google_cloud_inventory.py +0 -23
  61. pdt_cli-0.1.3/tests/test_windows_data.py +0 -101
  62. pdt_cli-0.1.3/verify/README.md +0 -92
  63. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/.gitignore +0 -0
  64. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/LICENSE +0 -0
  65. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/__init__.py +0 -0
  66. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/console.py +0 -0
  67. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_aws_batch.py +0 -0
  68. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/deploy_azure.py +0 -0
  69. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/hello-world/config.yml +0 -0
  70. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/hello-world/run.py +0 -0
  71. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/impossible-travel-report/config.yml +0 -0
  72. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/impossible-travel-report/env.template +0 -0
  73. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/impossible-travel-report/run.py +0 -0
  74. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/monday-orphaned-account-report/config.yml +0 -0
  75. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/monday-orphaned-account-report/env.template +0 -0
  76. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/examples/monday-orphaned-account-report/run.py +0 -0
  77. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/runs_cli.py +0 -0
  78. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/__init__.py +0 -0
  79. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/email_auth.py +0 -0
  80. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/entra.py +0 -0
  81. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/log.py +0 -0
  82. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/send_email_graph.py +0 -0
  83. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/src/pdt/utils/web.py +0 -0
  84. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/conftest.py +0 -0
  85. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_app_name.py +0 -0
  86. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_account.py +0 -0
  87. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_cost_estimate.py +0 -0
  88. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_deployer_policy.py +0 -0
  89. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_destroy.py +0 -0
  90. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_job_definition.py +0 -0
  91. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_names.py +0 -0
  92. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_not_found.py +0 -0
  93. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_profile.py +0 -0
  94. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_runs.py +0 -0
  95. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_shared_deletes.py +0 -0
  96. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_aws_storage.py +0 -0
  97. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_deleted_vault.py +0 -0
  98. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_environment_key.py +0 -0
  99. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_group_ownership.py +0 -0
  100. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_names.py +0 -0
  101. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_retries.py +0 -0
  102. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_side_resources.py +0 -0
  103. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_storage.py +0 -0
  104. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_azure_subscription_state.py +0 -0
  105. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_build_context.py +0 -0
  106. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_ci_azure.py +0 -0
  107. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_ci_google_cloud.py +0 -0
  108. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_completion.py +0 -0
  109. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_config_merge.py +0 -0
  110. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_console.py +0 -0
  111. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_cost_estimate.py +0 -0
  112. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_env_secret.py +0 -0
  113. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_fetch_json.py +0 -0
  114. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_gcloud_sdk.py +0 -0
  115. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_google_cloud_not_found.py +0 -0
  116. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_google_cloud_runs.py +0 -0
  117. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_google_cloud_service_account.py +0 -0
  118. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_google_cloud_storage.py +0 -0
  119. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_init_questions.py +0 -0
  120. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_log_colour.py +0 -0
  121. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_private_file.py +0 -0
  122. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_run_log_links.py +0 -0
  123. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_runs_cli.py +0 -0
  124. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_save_platform_key.py +0 -0
  125. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_schedule.py +0 -0
  126. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_set_app_enabled.py +0 -0
  127. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_starter_app.py +0 -0
  128. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_storage.py +0 -0
  129. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_storage_cli.py +0 -0
  130. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_strict_validation.py +0 -0
  131. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_windows_storage.py +0 -0
  132. {pdt_cli-0.1.3 → pdt_cli-0.1.4}/tests/test_winget_launcher.py +0 -0
  133. {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
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
- 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.
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.3"]
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
- 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.
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.3"]
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 test_claude_env_maps_the_gitlab_name_to_the_name_claude_reads():
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"]