bmsdna-devtools 0.43.1__tar.gz → 0.45.0__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 (132) hide show
  1. bmsdna_devtools-0.43.1/README.md → bmsdna_devtools-0.45.0/PKG-INFO +108 -55
  2. bmsdna_devtools-0.43.1/PKG-INFO → bmsdna_devtools-0.45.0/README.md +90 -72
  3. bmsdna_devtools-0.45.0/bmsdna/devtools/ado_auth.py +28 -0
  4. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/app_service_logs.py +38 -16
  5. bmsdna_devtools-0.45.0/bmsdna/devtools/azure_arm.py +98 -0
  6. bmsdna_devtools-0.45.0/bmsdna/devtools/azure_auth.py +30 -0
  7. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/bdt_config.py +5 -1
  8. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/cli.py +121 -101
  9. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/cli_tools.py +5 -8
  10. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/find_repo.py +87 -46
  11. bmsdna_devtools-0.45.0/bmsdna/devtools/gh_args.py +72 -0
  12. bmsdna_devtools-0.45.0/bmsdna/devtools/gh_issue.py +719 -0
  13. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/gh_pr.py +421 -175
  14. bmsdna_devtools-0.45.0/bmsdna/devtools/github_api.py +180 -0
  15. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/issue_do.py +4 -6
  16. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/logs.py +39 -45
  17. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_build.py +206 -37
  18. bmsdna_devtools-0.45.0/bmsdna/devtools/pr_create_args.py +281 -0
  19. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/worktree.py +40 -1
  20. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/pyproject.toml +2 -1
  21. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/skills/bmsdna-devtools/SKILL.md +16 -15
  22. bmsdna_devtools-0.45.0/tests/fake_github.py +78 -0
  23. bmsdna_devtools-0.45.0/tests/test_ado_auth.py +71 -0
  24. bmsdna_devtools-0.45.0/tests/test_app_service_logs.py +152 -0
  25. bmsdna_devtools-0.45.0/tests/test_azure_arm.py +136 -0
  26. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_bdt_config.py +21 -0
  27. bmsdna_devtools-0.45.0/tests/test_cli_pr_create.py +461 -0
  28. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_cli_pr_id.py +50 -1
  29. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_find_repo.py +218 -104
  30. bmsdna_devtools-0.45.0/tests/test_gh_issue.py +722 -0
  31. bmsdna_devtools-0.45.0/tests/test_gh_pr.py +1320 -0
  32. bmsdna_devtools-0.45.0/tests/test_github_api.py +180 -0
  33. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_issue_do.py +7 -0
  34. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_issue_for_me.py +52 -52
  35. bmsdna_devtools-0.45.0/tests/test_logs.py +162 -0
  36. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_pr_build.py +4 -13
  37. bmsdna_devtools-0.45.0/tests/test_pr_build_create_options.py +166 -0
  38. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_pr_build_flow.py +46 -0
  39. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_pr_choice.py +42 -27
  40. bmsdna_devtools-0.45.0/tests/test_pr_create_args.py +152 -0
  41. bmsdna_devtools-0.45.0/tests/test_pr_description_not_truncated.py +200 -0
  42. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_pr_info.py +21 -16
  43. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_worktree_clean.py +66 -0
  44. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/uv.lock +203 -1
  45. bmsdna_devtools-0.43.1/bmsdna/devtools/ado_auth.py +0 -44
  46. bmsdna_devtools-0.43.1/bmsdna/devtools/gh_issue.py +0 -541
  47. bmsdna_devtools-0.43.1/tests/test_ado_auth.py +0 -62
  48. bmsdna_devtools-0.43.1/tests/test_app_service_logs.py +0 -39
  49. bmsdna_devtools-0.43.1/tests/test_cli_pr_create.py +0 -569
  50. bmsdna_devtools-0.43.1/tests/test_gh_issue.py +0 -611
  51. bmsdna_devtools-0.43.1/tests/test_gh_pr.py +0 -1072
  52. bmsdna_devtools-0.43.1/tests/test_pr_description_not_truncated.py +0 -138
  53. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/.github/workflows/auto-release.yml +0 -0
  54. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/.github/workflows/python-publish.yml +0 -0
  55. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/.github/workflows/python-test.yml +0 -0
  56. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/.gitignore +0 -0
  57. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/.python-version +0 -0
  58. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/__init__.py +0 -0
  59. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/_api_dump.py +0 -0
  60. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/ado_issue.py +0 -0
  61. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/agent_mentions.py +0 -0
  62. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/api_usage.py +0 -0
  63. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/commit.py +0 -0
  64. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/dead_code.py +0 -0
  65. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/env_config.py +0 -0
  66. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/env_files.py +0 -0
  67. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/find_injection.py +0 -0
  68. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/gitrepo.py +0 -0
  69. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/injection_frontend.py +0 -0
  70. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/injection_python.py +0 -0
  71. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/issue_show.py +0 -0
  72. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/issue_watch.py +0 -0
  73. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint.py +0 -0
  74. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint_findings.py +0 -0
  75. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint_models.py +0 -0
  76. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint_sql.py +0 -0
  77. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint_sql_files.py +0 -0
  78. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint_tooling.py +0 -0
  79. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint_typescript.py +0 -0
  80. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_choice.py +0 -0
  81. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_info.py +0 -0
  82. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_issue_link.py +0 -0
  83. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_labels.py +0 -0
  84. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_markdown.py +0 -0
  85. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pull.py +0 -0
  86. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/testdb.py +0 -0
  87. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/translate.py +0 -0
  88. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/__init__.py +0 -0
  89. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/cli.py +0 -0
  90. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/pipeline.py +0 -0
  91. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/pricing.py +0 -0
  92. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/report.py +0 -0
  93. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/00_schema.sql +0 -0
  94. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/01_ingest_claude.sql +0 -0
  95. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/01_ingest_codex.sql +0 -0
  96. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/02_messages.sql +0 -0
  97. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/03_markers.sql +0 -0
  98. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/04_facts.sql +0 -0
  99. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/05_views.sql +0 -0
  100. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/justfile +0 -0
  101. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/conftest.py +0 -0
  102. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_ado_issue.py +0 -0
  103. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_ado_issue_flow.py +0 -0
  104. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_agent_mentions.py +0 -0
  105. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_api_usage.py +0 -0
  106. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_cli_cleanup.py +0 -0
  107. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_cli_commit.py +0 -0
  108. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_cli_issue.py +0 -0
  109. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_cli_pull.py +0 -0
  110. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_cli_tools.py +0 -0
  111. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_cli_version.py +0 -0
  112. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_commit.py +0 -0
  113. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_env_config.py +0 -0
  114. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_env_files.py +0 -0
  115. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_find_injection.py +0 -0
  116. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_gitrepo.py +0 -0
  117. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_issue_show.py +0 -0
  118. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_issue_watch.py +0 -0
  119. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_lint.py +0 -0
  120. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_lint_models.py +0 -0
  121. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_lint_sql.py +0 -0
  122. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_lint_sql_files.py +0 -0
  123. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_lint_tooling.py +0 -0
  124. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_lint_typescript.py +0 -0
  125. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_pr_issue_link.py +0 -0
  126. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_pr_labels.py +0 -0
  127. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_pr_markdown.py +0 -0
  128. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_pull.py +0 -0
  129. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_testdb.py +0 -0
  130. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_translate.py +0 -0
  131. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_usage.py +0 -0
  132. {bmsdna_devtools-0.43.1 → bmsdna_devtools-0.45.0}/tests/test_wait_timeout.py +0 -0
@@ -1,3 +1,21 @@
1
+ Metadata-Version: 2.5
2
+ Name: bmsdna-devtools
3
+ Version: 0.45.0
4
+ Summary: Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects
5
+ Requires-Python: >=3.14
6
+ Requires-Dist: azure-identity>=1.19.0
7
+ Requires-Dist: pgdevkit[db]>=0.7.1
8
+ Requires-Dist: requests>=2.32.0
9
+ Requires-Dist: rich>=13.0.0
10
+ Requires-Dist: tomli-w>=1.0.0
11
+ Requires-Dist: typer>=0.26.7
12
+ Provides-Extra: lint
13
+ Requires-Dist: sqlglot>=27.0.0; extra == 'lint'
14
+ Provides-Extra: usage
15
+ Requires-Dist: duckdb>=1.3.0; extra == 'usage'
16
+ Requires-Dist: plotly>=5.24.0; extra == 'usage'
17
+ Description-Content-Type: text/markdown
18
+
1
19
  # bmsdna-devtools
2
20
 
3
21
  Shared developer tooling for BMS projects: PR build/check status and a
@@ -8,16 +26,24 @@ postgres/psycopg (and arrowbricks) SQL rules, pydantic-model placement, hand-wir
8
26
  TypeScript and baseline tooling, plus `bdt dead-code` for `.sql` files nothing loads and
9
27
  backend routes nothing calls.
10
28
  `bdt pr *` and `bdt issue *` auto-detect whether the current repo's `origin`
11
- remote is Azure DevOps or GitHub and use `az`/`gh` accordingly.
29
+ remote is Azure DevOps or GitHub and call the matching REST API.
12
30
  Consolidates near-duplicate scripts that used to be copy-pasted across
13
31
  OneSales, ccmt2, and MDMApp into one versioned package with a `bdt` CLI.
14
32
 
15
- Requires `git` always, plus `az` (Azure DevOps commands, and all `bdt logs`
16
- commands) and/or `gh` (GitHub commands) on PATH as needed — each is checked
17
- lazily, only when a command actually needs it, with a clear error and an
18
- install link if missing rather than a raw traceback. Works on Windows: CLI
19
- shims (e.g. `az.cmd`) are resolved via `shutil.which` (which honors
20
- `PATHEXT`) rather than shelling out, output is decoded as UTF-8 rather than
33
+ Requires only `git` on PATH (plus `psql` for `bdt testdb`); the `az` and `gh` CLIs are
34
+ **not** needed — everything talks to GitHub (REST + GraphQL), Azure DevOps and Azure
35
+ (ARM / Application Insights) over raw HTTP (`requests`).
36
+
37
+ Auth:
38
+ - **GitHub**: the `GH_TOKEN` / `GITHUB_TOKEN` environment variable, else (if `gh` happens to be
39
+ installed) the output of `gh auth token`. The token needs the `repo` scope, plus `project` for
40
+ boards and `workflow` for reruns.
41
+ - **Azure DevOps**: `--pat` / `AZURE_DEVOPS_EXT_PAT` / `AZURE_DEVOPS_PAT`, else azure-identity's
42
+ `DefaultAzureCredential` (environment variables, managed identity, a cached `az login`, ...).
43
+ - **Azure (`bdt logs *`)**: the same `DefaultAzureCredential`, against ARM / Application Insights;
44
+ `AZURE_SUBSCRIPTION_ID` selects the subscription, else the only enabled one is used.
45
+
46
+ Works on Windows: output is decoded as UTF-8 rather than
21
47
  relying on the console's default codepage, and file arguments accept either
22
48
  slash style.
23
49
 
@@ -76,17 +102,16 @@ polling forever (a failed policy lookup is ignored and the wait continues).
76
102
  **Azure DevOps**: org/project/repo are auto-detected from
77
103
  `git remote get-url origin` (handles SSH, `dev.azure.com` HTTPS, and
78
104
  `*.visualstudio.com` HTTPS forms). Auth is an explicit PAT (`--pat` or
79
- `AZURE_DEVOPS_EXT_PAT`/`AZURE_DEVOPS_PAT` env var), falling back to a short-lived token from the
80
- caller's own `az login` — never embed a PAT literal in a script or CI file.
105
+ `AZURE_DEVOPS_EXT_PAT`/`AZURE_DEVOPS_PAT` env var), falling back to a short-lived token from
106
+ azure-identity's `DefaultAzureCredential` (e.g. the caller's own `az login`) — never embed a PAT literal in a script or CI file.
81
107
  `--target-branch` optionally restricts the lookup to PRs into that branch (default: any).
82
108
 
83
- **GitHub**: uses `gh`'s own auth (`gh auth login`) and resolves the open
84
- PR opened from the current branch (`gh pr list --head`, narrowed by
85
- `--target-branch` to that base branch if given; with none open, `gh pr view`
86
- finds a merged/closed one); the PR's actual base branch
87
- is shown in the output. Check status is computed from
88
- `gh pr view --json statusCheckRollup` rather than `gh pr checks --json`,
89
- since the latter flag isn't available in all `gh` releases.
109
+ **GitHub**: uses the `GH_TOKEN`/`GITHUB_TOKEN` auth above and resolves the open
110
+ PR opened from the current branch (REST pulls filtered by head, narrowed by
111
+ `--target-branch` to that base branch if given; with none open, a merged/closed one
112
+ is looked up); the PR's actual base branch
113
+ is shown in the output. Check status is computed from the PR's
114
+ `statusCheckRollup`, fetched via GraphQL.
90
115
 
91
116
  Pass `--pr-id <number>` to check a specific PR directly instead of resolving
92
117
  it from the current branch — useful when that PR's branch isn't checked out
@@ -135,7 +160,7 @@ anything depending on them) in place — distinct from queuing a brand new
135
160
  build. One retry call per pipeline that has a failed build for the PR.
136
161
  `--target-branch` selects which PR to look at, same as `bdt pr status`.
137
162
 
138
- **GitHub**: uses `gh run rerun <run-id> --failed`, which reruns only the
163
+ **GitHub**: POSTs `actions/runs/<run-id>/rerun-failed-jobs`, which reruns only the
139
164
  failed job(s) (and their dependents) of a workflow run — the run ID(s) are
140
165
  found automatically from the PR's failing checks. Checks that aren't backed
141
166
  by a GitHub Actions run (e.g. a legacy commit status from an external CI)
@@ -166,10 +191,10 @@ skipped rather than blocking or crashing `pr status`.
166
191
  `pr status` uses, just queried by `branchName=refs/heads/<target-branch>`
167
192
  instead of the PR's merge/source ref.
168
193
 
169
- **GitHub**: uses `gh run list --branch <target-branch> --event push`, which
194
+ **GitHub**: lists workflow runs via REST filtered by `branch=<target-branch>&event=push`, which
170
195
  selects workflow runs triggered by a push to that branch — as opposed to a
171
196
  `pull_request`-triggered run for some still-open PR targeting the same
172
- branch. Failed steps are printed via `gh run view <id> --log-failed`.
197
+ branch. Failed steps are printed as the tail of each failed job's log.
173
198
 
174
199
  ## `bdt pr create`
175
200
 
@@ -178,17 +203,34 @@ bdt pr create --target dev # or --target main / test
178
203
  ```
179
204
 
180
205
  Creates a PR from the current branch into `--target`. On Azure DevOps,
181
- a thin wrapper around `az repos pr create` (org/project/repo inferred by
182
- `az` itself from the git remote). On GitHub, `gh pr create --fill` (autofills
183
- title/body from commit info so it never blocks on an interactive prompt).
184
- Extra arguments pass through either way, e.g.
185
- `bdt pr create --target dev -- --reviewers "..."`.
206
+ a REST POST to `pullrequests` (org/project/repo inferred from the git remote). On GitHub,
207
+ a REST POST to `pulls`; title/body default to the branch's commit(s) so it never blocks on an
208
+ interactive prompt.
209
+
210
+ Extra arguments after `--` take the union of `gh pr create` and `az repos pr create` spellings on **both**
211
+ hosts (`--opt value` and `--opt=value`; list flags take every value up to the next option, comma-separated
212
+ values work too; boolean flags take an optional `true`/`false`):
213
+
214
+ | Meaning | Flags |
215
+ |---|---|
216
+ | title / body | `--title`/`-t`, `--body`/`--description`/`-b` (`-d` on Azure DevOps), `--body-file`/`-F` (`-` = stdin) |
217
+ | branches | `--base`/`-B`/`--target-branch`, `--head`/`-H`/`--source-branch`/`-s` |
218
+ | draft | `--draft [true\|false]` (`-d` on GitHub) |
219
+ | labels / reviewers | `--label`/`--labels`/`-l`, `--reviewer`/`--reviewers` (`-r` on GitHub), `--required-reviewers` |
220
+ | work items / issues | `--work-items` (same as `--issue`: linked and tagged `pr-available`) |
221
+ | merge | `--auto-complete`/`--auto-merge`, `--squash`, `--delete-source-branch`, `--merge-commit-message`, `--bypass-policy`, `--bypass-policy-reason`, `--transition-work-items` |
222
+ | other | `--assignee`/`-a`, `--milestone`/`-m`, `--project`/`-p` (GitHub board), `--fill`/`-f`, `--web`/`-w`/`--open`, `--dry-run` |
223
+ | Azure DevOps target | `--organization`/`--org`, `--project`/`-p`, `--repository`/`-r` override the remote-derived values |
224
+ | accepted, ignored | `--detect`, `--verbose`, `--debug`, `--subscription`, `--output`/`-o`, `--query` |
225
+
226
+ Azure DevOps resolves reviewers (GUID, e-mail or display name) through the Identities API, sets
227
+ `--auto-complete` as the current user and applies the merge options in a follow-up update, as `az` does.
228
+ A flag the host has no counterpart for (e.g. `--milestone` on Azure DevOps, `--delete-source-branch` on
229
+ GitHub) is reported on stderr (`Note: --milestone is ignored on Azure DevOps`), never dropped silently.
230
+ Unknown flags fail with the list of supported ones.
186
231
 
187
232
  Pass the text with `--title "..."` and `--description "$(cat pr.md)"` rather than as passthrough
188
- args: it is kept in full. On GitHub the description goes to `gh` on stdin (`--body-file -`); on
189
- Azure DevOps it is set over the REST API right after `az` creates the PR, because
190
- `az repos pr create --description` has been seen cutting long text at ~450 characters (a `--title`/
191
- `--description` given after `--` is intercepted the same way). Azure DevOps itself allows at most
233
+ args: the description is always sent in full. Azure DevOps itself allows at most
192
234
  4000 characters in a PR description: a longer one is rejected up front, and a description the
193
235
  server stored shorter than sent is reported as a warning (`bdt pr update` does the same check).
194
236
 
@@ -198,15 +240,15 @@ it ready for review. Pass `--no-draft` to open it ready for review
198
240
  immediately instead.
199
241
 
200
242
  `--label` (repeatable) applies labels to the PR on either host: on GitHub
201
- these map to `gh pr create --label`, so the label must already exist on the
202
- repo (`gh label create`); on Azure DevOps they map to `az repos pr create
203
- --labels`, which are freeform and get created on the fly. A successful
204
- create prints the PR's web/GUI link (not just the REST API URL Azure DevOps'
205
- `az` output otherwise gives you).
243
+ these are applied to the PR, and unknown labels are created automatically
244
+ by GitHub (no pre-existing label required); on Azure DevOps labels are freeform and get
245
+ created on the fly. A successful
246
+ create prints the PR's web/GUI link (not just the REST API URL Azure DevOps
247
+ returns).
206
248
 
207
249
  `[tool.bdt.pr.required_labels]` in pyproject.toml can require at least one
208
250
  label from each named group before the PR is created — checked locally
209
- (no `gh`/`az` call happens if a group isn't satisfied):
251
+ (no API call happens if a group isn't satisfied):
210
252
 
211
253
  ```toml
212
254
  [tool.bdt.pr.required_labels]
@@ -241,6 +283,16 @@ create prints a reminder to run `bdt pr status` afterward to check whether
241
283
  the CI build passes. This is a best-effort check — failures reading policy
242
284
  config (auth, permissions) fail open and simply skip the reminder.
243
285
 
286
+ ## `bdt pr comment`
287
+
288
+ ```bash
289
+ bdt pr comment --message "Looks good" [--screenshot shot.png] [--file report.pdf] [--pr-id 123]
290
+ bdt pr comment --message-file plan.md # post a file's text as the comment (`-` = stdin)
291
+ ```
292
+
293
+ `--file` links the file as an *attachment* (a download link in the comment); it does not post the file's
294
+ content. To post a markdown document (an implementation plan, review findings) use `--message-file`.
295
+
244
296
  ## `bdt issue create` / `show` / `update` / `delete`, `bdt issue comment add` / `update` / `delete`
245
297
 
246
298
  ```bash
@@ -336,27 +388,26 @@ On `update`, `--board` only moves the item when you pass it explicitly — it
336
388
  never falls back to `pyproject.toml`, so an unrelated field update (e.g.
337
389
  just `--title`) can't silently relocate the item to a different board.
338
390
 
339
- **GitHub**: a thin wrapper around `gh issue create` / `edit` / `delete` /
340
- `comment`. `--label`/`--tag` add a label on `create`, or add/remove one on
391
+ **GitHub**: REST/GraphQL calls for issue create / edit / delete /
392
+ comment. `--label`/`--tag` add a label on `create`, or add/remove one on
341
393
  `update` (paired with `--remove-label`/`--remove-tag`); labels must already
342
394
  exist in the repo. `--screenshot` pushes images to a `pr-assets` branch (same trick `bdt
343
395
  pr create --screenshot` uses, since GitHub has no API for uploading an image
344
396
  into an issue) and appends them to the issue body / comment as Markdown.
345
397
  `issue delete` is **permanent** — GitHub has no recycle bin for issues.
346
- Comment update/delete go through `gh api` directly (`gh issue` has no
347
- subcommand for editing/deleting an arbitrary comment by ID). Extra arguments
348
- to `bdt issue create` pass through to `gh issue create`, e.g.
349
- `bdt issue create --title "..." -- --assignee @me`.
350
-
351
- `--parent` / `--blocked-by` use GitHub's sub-issues and issue dependencies (`gh issue create --parent` /
352
- `--blocked-by`, `gh issue edit --parent` / `--add-blocked-by`), which need a recent `gh`; `issue show` reads
353
- them with `gh issue view --json parent,subIssues,blockedBy,blocking`. `--iteration` isn't supported: a
398
+ Comment update/delete go through REST. Extra arguments
399
+ to `bdt issue create` accept the `gh issue create` flags `--title`/`-t`, `--body`/`-b`, `--body-file`/`-F`
400
+ (`-` = stdin), `--label`/`-l`, `--assignee`/`-a` (`@me` works), `--milestone`/`-m` (title or number),
401
+ `--project`/`-p` (Projects board, repeatable) and `--web`/`-w`; `--template` / `--recover` are rejected. E.g.
402
+ `bdt issue create --title "..." -- --assignee @me --project Roadmap`.
403
+
404
+ `--parent` / `--blocked-by` use GitHub's sub-issues and issue dependencies REST endpoints; `issue show` reads
405
+ the parent, sub-issues, blocked-by and blocking links the same way. `--iteration` isn't supported: a
354
406
  GitHub Projects (v2) iteration is a per-project field, not a property of the issue.
355
407
 
356
408
  `--board <name>` on `create` adds the issue to that GitHub Projects (v2)
357
- board by title (needs a token with the `project` scope — `gh auth refresh -s
358
- project`). On `search`, `--board` (title or number) scopes results to issues
359
- currently on that board; since `gh issue list` has no board filter of its
409
+ board by title (needs a token with the `project` scope). On `search`, `--board` (title or number) scopes results to issues
410
+ currently on that board; since the issue list has no board filter of its
360
411
  own, this fetches a wider raw pool and filters it down client-side, so a
361
412
  board with few matches may return fewer results than `--limit` asks for. A
362
413
  CLI flag beats `[tool.bdt.github].board` in `pyproject.toml`:
@@ -392,7 +443,7 @@ Claims an issue (GitHub) or work item (Azure DevOps): comments `Taken by <you>`
392
443
  running coding agent's session (the same `<Agent> Session: <id>` note every bdt comment gets: a
393
444
  claude.ai link under a bridged Claude session, otherwise the bare session id; nothing outside an
394
445
  agent), so others can see who is on it. `<you>` is the GitHub login
395
- `gh` is authenticated as, else the git `user.name`. Without `NUMBER` it takes the issue the current
446
+ the token authenticates as, else the git `user.name`. Without `NUMBER` it takes the issue the current
396
447
  branch's PR closes (`Fixes #N`, or an issue/work-item URL in the PR body); it refuses to guess if
397
448
  that is none or several. If the issue's newest comment already is a "Taken by ..." claim (by anyone) it
398
449
  says so and posts nothing, so it's safe to run repeatedly -- including before `bdt issue do`.
@@ -413,7 +464,7 @@ addresses the calling agent. It combines with every other `search` filter (keywo
413
464
  the agent that is running `bdt`, or `@Agent`, which addresses every agent. Matching is case-insensitive and
414
465
  on whole words: `@claude`, `@CLAUDE,` and `@Claude.` match, `@Claudette`, `@claude-bot` and `me@claude.ai` do not.
415
466
  Only the newest comment counts, so once someone (including the agent, e.g. with a reply) comments after the
416
- mention, the issue drops out of the list. The newest comment isn't part of the list output of `gh`/Azure DevOps,
467
+ mention, the issue drops out of the list. The newest comment isn't part of the list output of GitHub/Azure DevOps,
417
468
  so this costs one extra request per candidate; candidates are scanned in recently-updated order (up to 10x
418
469
  `--limit` on GitHub, 200 on Azure DevOps) until `--limit` have matched.
419
470
 
@@ -518,7 +569,9 @@ scan_jinja = ["backend/print"] # "KEY" | tr in .jinja2/.j2/.html
518
569
  bdt worktree my-feature [--base dev] [--env-file .local_env] [--no-submodules] [--install "just install"]
519
570
  ```
520
571
 
521
- Creates `.worktrees/<name>` branched from `--base`, initializes submodules
572
+ Creates `.worktrees/<name>` branched from `--base`. It first fetches `origin/<base>` and, if the local
573
+ `<base>` is strictly behind it, branches from `origin/<base>` instead (so the PR starts on current code);
574
+ a local base that is ahead of or has diverged from the remote is kept as is. Then it initializes submodules
522
575
  (unless `--no-submodules`), and copies an env file into the new worktree as
523
576
  `.env` (auto-detects `.local_env` then `.env` if `--env-file` isn't given).
524
577
 
@@ -544,10 +597,10 @@ Configuration (flags override env vars):
544
597
  | Local work dir | `--root` | `AZDO_WORK_DIR`, `BMS_WORK_DIR` | `~/projects` (`C:/Projects` on Windows) |
545
598
  | Azure DevOps org | `--org` | `AZDO_ORG`, `BMS_ORG` | none (ADO not searched) |
546
599
  | GitHub org | `--github-org` | `GITHUB_ORG`, `BMS_GITHUB_ORG` | none (GitHub not searched) |
547
- | ADO PAT for cloning | `--pat` | `AZURE_DEVOPS_EXT_PAT`, `AZURE_DEVOPS_PAT` | falls back to `az` login |
600
+ | ADO PAT for cloning | `--pat` | `AZURE_DEVOPS_EXT_PAT`, `AZURE_DEVOPS_PAT` | falls back to `DefaultAzureCredential` |
548
601
 
549
602
  Set the org env vars in your shell profile to make `bdt find-repo <name>` work anywhere. It replaces
550
- the `cross-repo-discovery` skill's `ALL_REPOS.md` sync. Needs `az` (ADO) / `gh` (GitHub) only when
603
+ the `cross-repo-discovery` skill's `ALL_REPOS.md` sync. Needs ADO auth / a GitHub token only when
551
604
  those orgs are searched.
552
605
 
553
606
  ## `bdt cleanup worktrees` / `bdt cleanup orphaned-dbs` / `bdt cleanup db` / `bdt cleanup worktree`
@@ -702,7 +755,7 @@ sandbox mode too.
702
755
  ## `bdt logs roles` / `bdt logs tail`
703
756
 
704
757
  Query Application Insights (KQL over `traces`/`exceptions`) via
705
- `az monitor app-insights query`. No defaults are baked in — pass
758
+ the Application Insights REST API (azure-identity credential). No defaults are baked in — pass
706
759
  `--resource-group`/`--app-insights` explicitly (or set
707
760
  `AZURE_RESOURCE_GROUP`/`AZURE_APP_INSIGHTS`), since which Azure resource
708
761
  "this repo" maps to isn't derivable from the git remote.
@@ -715,7 +768,7 @@ bdt logs tail --resource-group my-rg --app-insights my-app-insights --role my-se
715
768
  ## `bdt logs fetch`
716
769
 
717
770
  Downloads the App Service log archive for a webapp/slot via
718
- `az webapp log download`, unzips it, and writes every line matching a
771
+ the App Service Kudu/ARM log download, unzips it, and writes every line matching a
719
772
  common error/warning marker (`ERROR`, `CRITICAL`, `WARNING`, tracebacks,
720
773
  `4xx`/`5xx`, `FAILED`, `FATAL`) to `<out>/<slot>_errors.log`. Simpler and
721
774
  often preferable to the KQL commands above when you just want "what broke
@@ -723,7 +776,7 @@ recently" rather than a queryable trace stream.
723
776
 
724
777
  The webapp/resource-group/slot come from a named environment configured in
725
778
  the calling repo's `pyproject.toml`. `slot` is optional — omit it for an
726
- app's default/production slot (no `--slot` is passed to `az`); set it for a
779
+ app's default/production slot (no slot is used); set it for a
727
780
  named deployment slot:
728
781
 
729
782
  ```toml
@@ -1,20 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: bmsdna-devtools
3
- Version: 0.43.1
4
- Summary: Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects
5
- Requires-Python: >=3.14
6
- Requires-Dist: pgdevkit[db]>=0.7.1
7
- Requires-Dist: requests>=2.32.0
8
- Requires-Dist: rich>=13.0.0
9
- Requires-Dist: tomli-w>=1.0.0
10
- Requires-Dist: typer>=0.26.7
11
- Provides-Extra: lint
12
- Requires-Dist: sqlglot>=27.0.0; extra == 'lint'
13
- Provides-Extra: usage
14
- Requires-Dist: duckdb>=1.3.0; extra == 'usage'
15
- Requires-Dist: plotly>=5.24.0; extra == 'usage'
16
- Description-Content-Type: text/markdown
17
-
18
1
  # bmsdna-devtools
19
2
 
20
3
  Shared developer tooling for BMS projects: PR build/check status and a
@@ -25,16 +8,24 @@ postgres/psycopg (and arrowbricks) SQL rules, pydantic-model placement, hand-wir
25
8
  TypeScript and baseline tooling, plus `bdt dead-code` for `.sql` files nothing loads and
26
9
  backend routes nothing calls.
27
10
  `bdt pr *` and `bdt issue *` auto-detect whether the current repo's `origin`
28
- remote is Azure DevOps or GitHub and use `az`/`gh` accordingly.
11
+ remote is Azure DevOps or GitHub and call the matching REST API.
29
12
  Consolidates near-duplicate scripts that used to be copy-pasted across
30
13
  OneSales, ccmt2, and MDMApp into one versioned package with a `bdt` CLI.
31
14
 
32
- Requires `git` always, plus `az` (Azure DevOps commands, and all `bdt logs`
33
- commands) and/or `gh` (GitHub commands) on PATH as needed — each is checked
34
- lazily, only when a command actually needs it, with a clear error and an
35
- install link if missing rather than a raw traceback. Works on Windows: CLI
36
- shims (e.g. `az.cmd`) are resolved via `shutil.which` (which honors
37
- `PATHEXT`) rather than shelling out, output is decoded as UTF-8 rather than
15
+ Requires only `git` on PATH (plus `psql` for `bdt testdb`); the `az` and `gh` CLIs are
16
+ **not** needed — everything talks to GitHub (REST + GraphQL), Azure DevOps and Azure
17
+ (ARM / Application Insights) over raw HTTP (`requests`).
18
+
19
+ Auth:
20
+ - **GitHub**: the `GH_TOKEN` / `GITHUB_TOKEN` environment variable, else (if `gh` happens to be
21
+ installed) the output of `gh auth token`. The token needs the `repo` scope, plus `project` for
22
+ boards and `workflow` for reruns.
23
+ - **Azure DevOps**: `--pat` / `AZURE_DEVOPS_EXT_PAT` / `AZURE_DEVOPS_PAT`, else azure-identity's
24
+ `DefaultAzureCredential` (environment variables, managed identity, a cached `az login`, ...).
25
+ - **Azure (`bdt logs *`)**: the same `DefaultAzureCredential`, against ARM / Application Insights;
26
+ `AZURE_SUBSCRIPTION_ID` selects the subscription, else the only enabled one is used.
27
+
28
+ Works on Windows: output is decoded as UTF-8 rather than
38
29
  relying on the console's default codepage, and file arguments accept either
39
30
  slash style.
40
31
 
@@ -93,17 +84,16 @@ polling forever (a failed policy lookup is ignored and the wait continues).
93
84
  **Azure DevOps**: org/project/repo are auto-detected from
94
85
  `git remote get-url origin` (handles SSH, `dev.azure.com` HTTPS, and
95
86
  `*.visualstudio.com` HTTPS forms). Auth is an explicit PAT (`--pat` or
96
- `AZURE_DEVOPS_EXT_PAT`/`AZURE_DEVOPS_PAT` env var), falling back to a short-lived token from the
97
- caller's own `az login` — never embed a PAT literal in a script or CI file.
87
+ `AZURE_DEVOPS_EXT_PAT`/`AZURE_DEVOPS_PAT` env var), falling back to a short-lived token from
88
+ azure-identity's `DefaultAzureCredential` (e.g. the caller's own `az login`) — never embed a PAT literal in a script or CI file.
98
89
  `--target-branch` optionally restricts the lookup to PRs into that branch (default: any).
99
90
 
100
- **GitHub**: uses `gh`'s own auth (`gh auth login`) and resolves the open
101
- PR opened from the current branch (`gh pr list --head`, narrowed by
102
- `--target-branch` to that base branch if given; with none open, `gh pr view`
103
- finds a merged/closed one); the PR's actual base branch
104
- is shown in the output. Check status is computed from
105
- `gh pr view --json statusCheckRollup` rather than `gh pr checks --json`,
106
- since the latter flag isn't available in all `gh` releases.
91
+ **GitHub**: uses the `GH_TOKEN`/`GITHUB_TOKEN` auth above and resolves the open
92
+ PR opened from the current branch (REST pulls filtered by head, narrowed by
93
+ `--target-branch` to that base branch if given; with none open, a merged/closed one
94
+ is looked up); the PR's actual base branch
95
+ is shown in the output. Check status is computed from the PR's
96
+ `statusCheckRollup`, fetched via GraphQL.
107
97
 
108
98
  Pass `--pr-id <number>` to check a specific PR directly instead of resolving
109
99
  it from the current branch — useful when that PR's branch isn't checked out
@@ -152,7 +142,7 @@ anything depending on them) in place — distinct from queuing a brand new
152
142
  build. One retry call per pipeline that has a failed build for the PR.
153
143
  `--target-branch` selects which PR to look at, same as `bdt pr status`.
154
144
 
155
- **GitHub**: uses `gh run rerun <run-id> --failed`, which reruns only the
145
+ **GitHub**: POSTs `actions/runs/<run-id>/rerun-failed-jobs`, which reruns only the
156
146
  failed job(s) (and their dependents) of a workflow run — the run ID(s) are
157
147
  found automatically from the PR's failing checks. Checks that aren't backed
158
148
  by a GitHub Actions run (e.g. a legacy commit status from an external CI)
@@ -183,10 +173,10 @@ skipped rather than blocking or crashing `pr status`.
183
173
  `pr status` uses, just queried by `branchName=refs/heads/<target-branch>`
184
174
  instead of the PR's merge/source ref.
185
175
 
186
- **GitHub**: uses `gh run list --branch <target-branch> --event push`, which
176
+ **GitHub**: lists workflow runs via REST filtered by `branch=<target-branch>&event=push`, which
187
177
  selects workflow runs triggered by a push to that branch — as opposed to a
188
178
  `pull_request`-triggered run for some still-open PR targeting the same
189
- branch. Failed steps are printed via `gh run view <id> --log-failed`.
179
+ branch. Failed steps are printed as the tail of each failed job's log.
190
180
 
191
181
  ## `bdt pr create`
192
182
 
@@ -195,17 +185,34 @@ bdt pr create --target dev # or --target main / test
195
185
  ```
196
186
 
197
187
  Creates a PR from the current branch into `--target`. On Azure DevOps,
198
- a thin wrapper around `az repos pr create` (org/project/repo inferred by
199
- `az` itself from the git remote). On GitHub, `gh pr create --fill` (autofills
200
- title/body from commit info so it never blocks on an interactive prompt).
201
- Extra arguments pass through either way, e.g.
202
- `bdt pr create --target dev -- --reviewers "..."`.
188
+ a REST POST to `pullrequests` (org/project/repo inferred from the git remote). On GitHub,
189
+ a REST POST to `pulls`; title/body default to the branch's commit(s) so it never blocks on an
190
+ interactive prompt.
191
+
192
+ Extra arguments after `--` take the union of `gh pr create` and `az repos pr create` spellings on **both**
193
+ hosts (`--opt value` and `--opt=value`; list flags take every value up to the next option, comma-separated
194
+ values work too; boolean flags take an optional `true`/`false`):
195
+
196
+ | Meaning | Flags |
197
+ |---|---|
198
+ | title / body | `--title`/`-t`, `--body`/`--description`/`-b` (`-d` on Azure DevOps), `--body-file`/`-F` (`-` = stdin) |
199
+ | branches | `--base`/`-B`/`--target-branch`, `--head`/`-H`/`--source-branch`/`-s` |
200
+ | draft | `--draft [true\|false]` (`-d` on GitHub) |
201
+ | labels / reviewers | `--label`/`--labels`/`-l`, `--reviewer`/`--reviewers` (`-r` on GitHub), `--required-reviewers` |
202
+ | work items / issues | `--work-items` (same as `--issue`: linked and tagged `pr-available`) |
203
+ | merge | `--auto-complete`/`--auto-merge`, `--squash`, `--delete-source-branch`, `--merge-commit-message`, `--bypass-policy`, `--bypass-policy-reason`, `--transition-work-items` |
204
+ | other | `--assignee`/`-a`, `--milestone`/`-m`, `--project`/`-p` (GitHub board), `--fill`/`-f`, `--web`/`-w`/`--open`, `--dry-run` |
205
+ | Azure DevOps target | `--organization`/`--org`, `--project`/`-p`, `--repository`/`-r` override the remote-derived values |
206
+ | accepted, ignored | `--detect`, `--verbose`, `--debug`, `--subscription`, `--output`/`-o`, `--query` |
207
+
208
+ Azure DevOps resolves reviewers (GUID, e-mail or display name) through the Identities API, sets
209
+ `--auto-complete` as the current user and applies the merge options in a follow-up update, as `az` does.
210
+ A flag the host has no counterpart for (e.g. `--milestone` on Azure DevOps, `--delete-source-branch` on
211
+ GitHub) is reported on stderr (`Note: --milestone is ignored on Azure DevOps`), never dropped silently.
212
+ Unknown flags fail with the list of supported ones.
203
213
 
204
214
  Pass the text with `--title "..."` and `--description "$(cat pr.md)"` rather than as passthrough
205
- args: it is kept in full. On GitHub the description goes to `gh` on stdin (`--body-file -`); on
206
- Azure DevOps it is set over the REST API right after `az` creates the PR, because
207
- `az repos pr create --description` has been seen cutting long text at ~450 characters (a `--title`/
208
- `--description` given after `--` is intercepted the same way). Azure DevOps itself allows at most
215
+ args: the description is always sent in full. Azure DevOps itself allows at most
209
216
  4000 characters in a PR description: a longer one is rejected up front, and a description the
210
217
  server stored shorter than sent is reported as a warning (`bdt pr update` does the same check).
211
218
 
@@ -215,15 +222,15 @@ it ready for review. Pass `--no-draft` to open it ready for review
215
222
  immediately instead.
216
223
 
217
224
  `--label` (repeatable) applies labels to the PR on either host: on GitHub
218
- these map to `gh pr create --label`, so the label must already exist on the
219
- repo (`gh label create`); on Azure DevOps they map to `az repos pr create
220
- --labels`, which are freeform and get created on the fly. A successful
221
- create prints the PR's web/GUI link (not just the REST API URL Azure DevOps'
222
- `az` output otherwise gives you).
225
+ these are applied to the PR, and unknown labels are created automatically
226
+ by GitHub (no pre-existing label required); on Azure DevOps labels are freeform and get
227
+ created on the fly. A successful
228
+ create prints the PR's web/GUI link (not just the REST API URL Azure DevOps
229
+ returns).
223
230
 
224
231
  `[tool.bdt.pr.required_labels]` in pyproject.toml can require at least one
225
232
  label from each named group before the PR is created — checked locally
226
- (no `gh`/`az` call happens if a group isn't satisfied):
233
+ (no API call happens if a group isn't satisfied):
227
234
 
228
235
  ```toml
229
236
  [tool.bdt.pr.required_labels]
@@ -258,6 +265,16 @@ create prints a reminder to run `bdt pr status` afterward to check whether
258
265
  the CI build passes. This is a best-effort check — failures reading policy
259
266
  config (auth, permissions) fail open and simply skip the reminder.
260
267
 
268
+ ## `bdt pr comment`
269
+
270
+ ```bash
271
+ bdt pr comment --message "Looks good" [--screenshot shot.png] [--file report.pdf] [--pr-id 123]
272
+ bdt pr comment --message-file plan.md # post a file's text as the comment (`-` = stdin)
273
+ ```
274
+
275
+ `--file` links the file as an *attachment* (a download link in the comment); it does not post the file's
276
+ content. To post a markdown document (an implementation plan, review findings) use `--message-file`.
277
+
261
278
  ## `bdt issue create` / `show` / `update` / `delete`, `bdt issue comment add` / `update` / `delete`
262
279
 
263
280
  ```bash
@@ -353,27 +370,26 @@ On `update`, `--board` only moves the item when you pass it explicitly — it
353
370
  never falls back to `pyproject.toml`, so an unrelated field update (e.g.
354
371
  just `--title`) can't silently relocate the item to a different board.
355
372
 
356
- **GitHub**: a thin wrapper around `gh issue create` / `edit` / `delete` /
357
- `comment`. `--label`/`--tag` add a label on `create`, or add/remove one on
373
+ **GitHub**: REST/GraphQL calls for issue create / edit / delete /
374
+ comment. `--label`/`--tag` add a label on `create`, or add/remove one on
358
375
  `update` (paired with `--remove-label`/`--remove-tag`); labels must already
359
376
  exist in the repo. `--screenshot` pushes images to a `pr-assets` branch (same trick `bdt
360
377
  pr create --screenshot` uses, since GitHub has no API for uploading an image
361
378
  into an issue) and appends them to the issue body / comment as Markdown.
362
379
  `issue delete` is **permanent** — GitHub has no recycle bin for issues.
363
- Comment update/delete go through `gh api` directly (`gh issue` has no
364
- subcommand for editing/deleting an arbitrary comment by ID). Extra arguments
365
- to `bdt issue create` pass through to `gh issue create`, e.g.
366
- `bdt issue create --title "..." -- --assignee @me`.
367
-
368
- `--parent` / `--blocked-by` use GitHub's sub-issues and issue dependencies (`gh issue create --parent` /
369
- `--blocked-by`, `gh issue edit --parent` / `--add-blocked-by`), which need a recent `gh`; `issue show` reads
370
- them with `gh issue view --json parent,subIssues,blockedBy,blocking`. `--iteration` isn't supported: a
380
+ Comment update/delete go through REST. Extra arguments
381
+ to `bdt issue create` accept the `gh issue create` flags `--title`/`-t`, `--body`/`-b`, `--body-file`/`-F`
382
+ (`-` = stdin), `--label`/`-l`, `--assignee`/`-a` (`@me` works), `--milestone`/`-m` (title or number),
383
+ `--project`/`-p` (Projects board, repeatable) and `--web`/`-w`; `--template` / `--recover` are rejected. E.g.
384
+ `bdt issue create --title "..." -- --assignee @me --project Roadmap`.
385
+
386
+ `--parent` / `--blocked-by` use GitHub's sub-issues and issue dependencies REST endpoints; `issue show` reads
387
+ the parent, sub-issues, blocked-by and blocking links the same way. `--iteration` isn't supported: a
371
388
  GitHub Projects (v2) iteration is a per-project field, not a property of the issue.
372
389
 
373
390
  `--board <name>` on `create` adds the issue to that GitHub Projects (v2)
374
- board by title (needs a token with the `project` scope — `gh auth refresh -s
375
- project`). On `search`, `--board` (title or number) scopes results to issues
376
- currently on that board; since `gh issue list` has no board filter of its
391
+ board by title (needs a token with the `project` scope). On `search`, `--board` (title or number) scopes results to issues
392
+ currently on that board; since the issue list has no board filter of its
377
393
  own, this fetches a wider raw pool and filters it down client-side, so a
378
394
  board with few matches may return fewer results than `--limit` asks for. A
379
395
  CLI flag beats `[tool.bdt.github].board` in `pyproject.toml`:
@@ -409,7 +425,7 @@ Claims an issue (GitHub) or work item (Azure DevOps): comments `Taken by <you>`
409
425
  running coding agent's session (the same `<Agent> Session: <id>` note every bdt comment gets: a
410
426
  claude.ai link under a bridged Claude session, otherwise the bare session id; nothing outside an
411
427
  agent), so others can see who is on it. `<you>` is the GitHub login
412
- `gh` is authenticated as, else the git `user.name`. Without `NUMBER` it takes the issue the current
428
+ the token authenticates as, else the git `user.name`. Without `NUMBER` it takes the issue the current
413
429
  branch's PR closes (`Fixes #N`, or an issue/work-item URL in the PR body); it refuses to guess if
414
430
  that is none or several. If the issue's newest comment already is a "Taken by ..." claim (by anyone) it
415
431
  says so and posts nothing, so it's safe to run repeatedly -- including before `bdt issue do`.
@@ -430,7 +446,7 @@ addresses the calling agent. It combines with every other `search` filter (keywo
430
446
  the agent that is running `bdt`, or `@Agent`, which addresses every agent. Matching is case-insensitive and
431
447
  on whole words: `@claude`, `@CLAUDE,` and `@Claude.` match, `@Claudette`, `@claude-bot` and `me@claude.ai` do not.
432
448
  Only the newest comment counts, so once someone (including the agent, e.g. with a reply) comments after the
433
- mention, the issue drops out of the list. The newest comment isn't part of the list output of `gh`/Azure DevOps,
449
+ mention, the issue drops out of the list. The newest comment isn't part of the list output of GitHub/Azure DevOps,
434
450
  so this costs one extra request per candidate; candidates are scanned in recently-updated order (up to 10x
435
451
  `--limit` on GitHub, 200 on Azure DevOps) until `--limit` have matched.
436
452
 
@@ -535,7 +551,9 @@ scan_jinja = ["backend/print"] # "KEY" | tr in .jinja2/.j2/.html
535
551
  bdt worktree my-feature [--base dev] [--env-file .local_env] [--no-submodules] [--install "just install"]
536
552
  ```
537
553
 
538
- Creates `.worktrees/<name>` branched from `--base`, initializes submodules
554
+ Creates `.worktrees/<name>` branched from `--base`. It first fetches `origin/<base>` and, if the local
555
+ `<base>` is strictly behind it, branches from `origin/<base>` instead (so the PR starts on current code);
556
+ a local base that is ahead of or has diverged from the remote is kept as is. Then it initializes submodules
539
557
  (unless `--no-submodules`), and copies an env file into the new worktree as
540
558
  `.env` (auto-detects `.local_env` then `.env` if `--env-file` isn't given).
541
559
 
@@ -561,10 +579,10 @@ Configuration (flags override env vars):
561
579
  | Local work dir | `--root` | `AZDO_WORK_DIR`, `BMS_WORK_DIR` | `~/projects` (`C:/Projects` on Windows) |
562
580
  | Azure DevOps org | `--org` | `AZDO_ORG`, `BMS_ORG` | none (ADO not searched) |
563
581
  | GitHub org | `--github-org` | `GITHUB_ORG`, `BMS_GITHUB_ORG` | none (GitHub not searched) |
564
- | ADO PAT for cloning | `--pat` | `AZURE_DEVOPS_EXT_PAT`, `AZURE_DEVOPS_PAT` | falls back to `az` login |
582
+ | ADO PAT for cloning | `--pat` | `AZURE_DEVOPS_EXT_PAT`, `AZURE_DEVOPS_PAT` | falls back to `DefaultAzureCredential` |
565
583
 
566
584
  Set the org env vars in your shell profile to make `bdt find-repo <name>` work anywhere. It replaces
567
- the `cross-repo-discovery` skill's `ALL_REPOS.md` sync. Needs `az` (ADO) / `gh` (GitHub) only when
585
+ the `cross-repo-discovery` skill's `ALL_REPOS.md` sync. Needs ADO auth / a GitHub token only when
568
586
  those orgs are searched.
569
587
 
570
588
  ## `bdt cleanup worktrees` / `bdt cleanup orphaned-dbs` / `bdt cleanup db` / `bdt cleanup worktree`
@@ -719,7 +737,7 @@ sandbox mode too.
719
737
  ## `bdt logs roles` / `bdt logs tail`
720
738
 
721
739
  Query Application Insights (KQL over `traces`/`exceptions`) via
722
- `az monitor app-insights query`. No defaults are baked in — pass
740
+ the Application Insights REST API (azure-identity credential). No defaults are baked in — pass
723
741
  `--resource-group`/`--app-insights` explicitly (or set
724
742
  `AZURE_RESOURCE_GROUP`/`AZURE_APP_INSIGHTS`), since which Azure resource
725
743
  "this repo" maps to isn't derivable from the git remote.
@@ -732,7 +750,7 @@ bdt logs tail --resource-group my-rg --app-insights my-app-insights --role my-se
732
750
  ## `bdt logs fetch`
733
751
 
734
752
  Downloads the App Service log archive for a webapp/slot via
735
- `az webapp log download`, unzips it, and writes every line matching a
753
+ the App Service Kudu/ARM log download, unzips it, and writes every line matching a
736
754
  common error/warning marker (`ERROR`, `CRITICAL`, `WARNING`, tracebacks,
737
755
  `4xx`/`5xx`, `FAILED`, `FATAL`) to `<out>/<slot>_errors.log`. Simpler and
738
756
  often preferable to the KQL commands above when you just want "what broke
@@ -740,7 +758,7 @@ recently" rather than a queryable trace stream.
740
758
 
741
759
  The webapp/resource-group/slot come from a named environment configured in
742
760
  the calling repo's `pyproject.toml`. `slot` is optional — omit it for an
743
- app's default/production slot (no `--slot` is passed to `az`); set it for a
761
+ app's default/production slot (no slot is used); set it for a
744
762
  named deployment slot:
745
763
 
746
764
  ```toml
@@ -0,0 +1,28 @@
1
+ """Azure DevOps auth: explicit PAT (`--pat` / AZURE_DEVOPS_EXT_PAT / AZURE_DEVOPS_PAT), or a token from
2
+ `DefaultAzureCredential` (see `azure_auth`)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import base64
7
+
8
+ from .azure_auth import get_azure_token
9
+
10
+ # Well-known Azure DevOps resource ID; `<id>/.default` is its AAD scope.
11
+ ADO_RESOURCE_ID = "499b84ac-1321-427f-aa17-267ca6975798"
12
+
13
+
14
+ def get_az_devops_token() -> str:
15
+ return get_azure_token(f"{ADO_RESOURCE_ID}/.default")
16
+
17
+
18
+ def auth_header(pat: str | None) -> dict[str, str]:
19
+ """Basic-auth header for an explicit PAT, else Bearer via `DefaultAzureCredential`.
20
+
21
+ Never hardcode a PAT literal in a caller — pass it in from an env var
22
+ (e.g. AZURE_DEVOPS_EXT_PAT) or a CLI flag, or omit it and let azure-identity supply a
23
+ short-lived token from the operator's own login.
24
+ """
25
+ if pat:
26
+ token = base64.b64encode(f":{pat}".encode()).decode()
27
+ return {"Authorization": f"Basic {token}"}
28
+ return {"Authorization": f"Bearer {get_az_devops_token()}"}