bmsdna-devtools
Shared developer tooling for BMS projects: PR build/check status, PR
creation, issue/work item creation and comments, git worktrees, a
commit-and-push helper with pre-flight checks, and Azure log queries.
bdt pr * and bdt issue * auto-detect whether the current repo's origin
remote is Azure DevOps or GitHub and use az/gh accordingly.
Consolidates near-duplicate scripts that used to be copy-pasted across
OneSales, ccmt2, and MDMApp into one versioned package with a bdt CLI.
Requires git always, plus az (Azure DevOps commands, and all bdt logs
commands) and/or gh (GitHub commands) on PATH as needed — each is checked
lazily, only when a command actually needs it, with a clear error and an
install link if missing rather than a raw traceback. Works on Windows: CLI
shims (e.g. az.cmd) are resolved via shutil.which (which honors
PATHEXT) rather than shelling out, output is decoded as UTF-8 rather than
relying on the console's default codepage, and file arguments accept either
slash style.
Install
uv tool install bmsdna-devtools
Or as a project dependency: uv add bmsdna-devtools.
bdt pr status
Find the PR opened from the current branch and report build/check status
(failed steps print their logs inline). Works against Azure DevOps or
GitHub — whichever origin points at.
bdt pr status [--target-branch main] [--wait]
If the PR can't be merged, that's reported immediately instead of polling
for builds/checks that will never run — e.g. on Azure DevOps:
PR #42 ('feat: widgets') has merge conflicts with the target branch (mergeStatus=conflicts);
on GitHub: PR #42 ('feat: widgets') has merge conflicts with 'main' (mergeable=CONFLICTING).
Exit code 1 either way.
Azure DevOps: org/project/repo are auto-detected from
git remote get-url origin (handles SSH, dev.azure.com HTTPS, and
*.visualstudio.com HTTPS forms). Auth is an explicit PAT (--pat or
AZURE_DEVOPS_EXT_PAT/AZURE_DEVOPS_PAT env var), falling back to a short-lived token from the
caller's own az login — never embed a PAT literal in a script or CI file.
--target-branch selects which PR to look at (ADO's search API needs one).
GitHub: uses gh's own auth (gh auth login) and always resolves the
PR opened from the current branch — gh pr view has no target-branch
filter, so --target-branch is ignored here; the PR's actual base branch
is shown in the output. Check status is computed from
gh pr view --json statusCheckRollup rather than gh pr checks --json,
since the latter flag isn't available in all gh releases.
bdt pr create
bdt pr create --target main # or --target test
Creates a PR from the current branch into --target. On Azure DevOps,
a thin wrapper around az repos pr create (org/project/repo inferred by
az itself from the git remote). On GitHub, gh pr create --fill (autofills
title/body from commit info so it never blocks on an interactive prompt).
Extra arguments pass through either way, e.g.
bdt pr create --target main -- --title "...".
--label (repeatable) applies labels to the PR on either host: on GitHub
these map to gh pr create --label, so the label must already exist on the
repo (gh label create); on Azure DevOps they map to az repos pr create --labels, which are freeform and get created on the fly. A successful
create prints the PR's web/GUI link (not just the REST API URL Azure DevOps'
az output otherwise gives you).
[tool.bdt.pr.required_labels] in pyproject.toml can require at least one
label from each named group before the PR is created — checked locally
(no gh/az call happens if a group isn't satisfied):
[tool.bdt.pr.required_labels]
type = ["bug", "feature", "chore"]
risk = ["breaking", "non-breaking"]
With the above, bdt pr create --label feature --label breaking passes,
but bdt pr create --label feature fails with a message naming the unmet
group (risk) and its allowed choices.
If --target has a build policy configured (an Azure DevOps Build policy,
or a GitHub branch protection rule requiring status checks), a successful
create prints a reminder to run bdt pr status afterward to check whether
the CI build passes. This is a best-effort check — failures reading policy
config (auth, permissions) fail open and simply skip the reminder.
bdt issue create / update / delete, bdt issue comment add / update / delete
bdt issue create --title "Nightly job fails" --description "..." --type Bug --screenshot before.png
bdt issue update 1234 --state Resolved --tag fixed
bdt issue delete 1234 --yes
bdt issue comment add 1234 --message "Repro'd, see attached" --screenshot repro.png
bdt issue comment update 1234 5678 --message "Actually, see the second screenshot"
bdt issue comment delete 1234 5678 --yes
Creates/updates/deletes an issue (GitHub) or work item (Azure DevOps), and
adds/edits/deletes comments on one, auto-detected from origin like bdt pr *. bdt issue comment add prints the new comment's ID so you can pass it to
update/delete later.
Destructive commands (issue delete, issue comment delete) require an
explicit --yes — there's no interactive confirmation prompt, since bdt is
also invoked by AI-agent callers that can't answer one.
Azure DevOps: --type selects the work item type on create (Bug,
Task, User Story, ... — whatever the project's process defines; default
Bug). --tag sets/replaces the full tag list (repeatable; omit on
update to leave tags unchanged). --state (update only) sets
System.State, e.g. Active, Resolved, Closed. --screenshot uploads
each image as a work item attachment (visible in the Attachments tab) and
posts a comment embedding them inline with Markdown — the Description field
defaults to HTML via the REST API, where a raw ![]() would just show as
literal text, but the work item Discussion/Comments control has always
rendered Markdown. issue delete soft-deletes to the project's Recycle Bin
(restorable, not permanent).
--board <team> sets the work item's Area Path to that Azure Boards team's
default, so it shows up on that team's board — a CLI flag beats
[tool.bdt.ado].board in pyproject.toml, which beats filing under the
project's root area:
[tool.bdt.ado]
board = "My Team"
On update, --board only moves the item when you pass it explicitly — it
never falls back to pyproject.toml, so an unrelated field update (e.g.
just --title) can't silently relocate the item to a different board.
GitHub: a thin wrapper around gh issue create / edit / delete /
comment. --label adds a label on create, or adds/removes one on
update (paired with --remove-label); labels must already exist in the
repo. --screenshot pushes images to a pr-assets branch (same trick bdt pr create --screenshot uses, since GitHub has no API for uploading an image
into an issue) and appends them to the issue body / comment as Markdown.
issue delete is permanent — GitHub has no recycle bin for issues.
Comment update/delete go through gh api directly (gh issue has no
subcommand for editing/deleting an arbitrary comment by ID). Extra arguments
to bdt issue create pass through to gh issue create, e.g.
bdt issue create --title "..." -- --assignee @me.
bdt worktree
bdt worktree my-feature [--base dev] [--env-file .local_env] [--no-submodules] [--install "just install"]
Creates .worktrees/<name> branched from --base, initializes submodules
(unless --no-submodules), and copies an env file into the new worktree as
.env (auto-detects .local_env then .env if --env-file isn't given).
bdt commit
bdt commit "feat(x): add widget support" file1.py file2.py [--json] [--no-verify] [--subrepo database]
Stages, commits, and pushes the given files. Pre-flight checks: files
exist, commit message looks like type: description (skip with
--skip-message-check), not on main/master (skip with --allow-main).
Retries once (re-git add) if a pre-commit hook reformats files. Pass
--subrepo <dir> (repeatable) for repos that vendor a submodule (e.g.
database) — files under that prefix are committed/pushed inside the
submodule first, then the bump is staged in the parent repo.
Set IS_BMS_AI_SANDBOX=1 to skip the push step (commit only) — used when
an AI coding sandbox pushes on its own schedule separately.
--json emits a machine-readable result for AI-agent callers:
{
"success": true, "committed": true, "pushed": true,
"message": "...", "files": ["..."], "commit_sha": "abc1234",
"error": null, "hint": null
}
bdt logs roles / bdt logs tail
Query Application Insights (KQL over traces/exceptions) via
az monitor app-insights query. No defaults are baked in — pass
--resource-group/--app-insights explicitly (or set
AZURE_RESOURCE_GROUP/AZURE_APP_INSIGHTS), since which Azure resource
"this repo" maps to isn't derivable from the git remote.
bdt logs roles --resource-group my-rg --app-insights my-app-insights --minutes 60
bdt logs tail --resource-group my-rg --app-insights my-app-insights --role my-service --level warning
bdt logs fetch
Downloads the App Service log archive for a webapp/slot via
az webapp log download, unzips it, and writes every line matching a
common error/warning marker (ERROR, CRITICAL, WARNING, tracebacks,
4xx/5xx, FAILED, FATAL) to <out>/<slot>_errors.log. Simpler and
often preferable to the KQL commands above when you just want "what broke
recently" rather than a queryable trace stream.
The webapp/resource-group/slot come from a named environment configured in
the calling repo's pyproject.toml. slot is optional — omit it for an
app's default/production slot (no --slot is passed to az); set it for a
named deployment slot:
[tool.bdt.envs.prod]
webapp = "my-webapp"
resource_group = "my-rg"
[tool.bdt.envs.test]
webapp = "my-webapp"
resource_group = "my-rg"
slot = "test"
bdt logs fetch --env prod
bdt logs fetch --env prod --out logs/ --keep-archive
Releasing
Bump version in pyproject.toml as part of your PR, same as any other
change. Once that PR merges to main and the Python Test workflow passes
for that commit, .github/workflows/auto-release.yml automatically tags it
vX.Y.Z, cuts a GitHub Release (skipping if that version was already
released, e.g. a merge that didn't touch the version), and dispatches
python-publish.yml to publish it to PyPI — no manual release step, and no
extra secret to configure. Two non-obvious GitHub Actions quirks shaped
this (see the comments at the top of auto-release.yml for the full
reasoning, since both were hit and confirmed the hard way):
- A release created with the default
GITHUB_TOKENdoes not trigger other workflows'release: publishedlisteners (an anti-recursion safeguard) —workflow_dispatchis the documented exception, soauto-release.ymldispatchespython-publish.ymldirectly (gh workflow run) instead of relying on the release to cascade into it. python-publish.ymldeliberately stays a plain, directly-triggered top-level workflow rather than somethingauto-release.ymlcalls viaworkflow_call: PyPI's OIDC trusted publishing does not support reusable/called workflows and silently rejects the token in that shape.
workflow_dispatch (or an actual GitHub UI release) on python-publish.yml
still works as a manual fallback if you ever need to re-publish a version
without going through auto-release.yml.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bmsdna_devtools-0.8.0.tar.gz.
File metadata
- Download URL: bmsdna_devtools-0.8.0.tar.gz
- Upload date:
- Size: 67.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d8786ee65eff09b8ad2eb5e3ac8f50484af1a11d614ad9545f9396f12ac97893
|
|
| MD5 |
850ded244487badfe4aa7b89b3bd4d40
|
|
| BLAKE2b-256 |
8ec8e36a772297f6685cb0e8013f6ce5c3b07ee6e3f9b571f20f4cfd1f196e8d
|
Provenance
The following attestation bundles were made for bmsdna_devtools-0.8.0.tar.gz:
Publisher:
python-publish.yml on bmsuisse/devtools
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bmsdna_devtools-0.8.0.tar.gz -
Subject digest:
d8786ee65eff09b8ad2eb5e3ac8f50484af1a11d614ad9545f9396f12ac97893 - Sigstore transparency entry: 2766020855
- Sigstore integration time:
-
Permalink:
bmsuisse/devtools@2cab74185aa2252c831b02a67e6c93181940371f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/bmsuisse
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@2cab74185aa2252c831b02a67e6c93181940371f -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file bmsdna_devtools-0.8.0-py3-none-any.whl.
File metadata
- Download URL: bmsdna_devtools-0.8.0-py3-none-any.whl
- Upload date:
- Size: 46.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2f04e157544e62d0ce19790a5a086171d3759d69ce31f00ea0e504f93cb46cf1
|
|
| MD5 |
91ec4a28deed57e07b6101782c28170b
|
|
| BLAKE2b-256 |
ba64666be8d5fb40effa962178e3b34fcc24d7a12440f368b0b618cc30cf4aab
|
Provenance
The following attestation bundles were made for bmsdna_devtools-0.8.0-py3-none-any.whl:
Publisher:
python-publish.yml on bmsuisse/devtools
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bmsdna_devtools-0.8.0-py3-none-any.whl -
Subject digest:
2f04e157544e62d0ce19790a5a086171d3759d69ce31f00ea0e504f93cb46cf1 - Sigstore transparency entry: 2766021324
- Sigstore integration time:
-
Permalink:
bmsuisse/devtools@2cab74185aa2252c831b02a67e6c93181940371f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/bmsuisse
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@2cab74185aa2252c831b02a67e6c93181940371f -
Trigger Event:
workflow_dispatch
-
Statement type: