bmsdna-devtools 0.2.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.
- bmsdna_devtools-0.2.0/.github/workflows/python-publish.yml +37 -0
- bmsdna_devtools-0.2.0/.github/workflows/python-test.yml +34 -0
- bmsdna_devtools-0.2.0/.gitignore +218 -0
- bmsdna_devtools-0.2.0/.python-version +1 -0
- bmsdna_devtools-0.2.0/PKG-INFO +143 -0
- bmsdna_devtools-0.2.0/README.md +133 -0
- bmsdna_devtools-0.2.0/bmsdna/devtools/__init__.py +0 -0
- bmsdna_devtools-0.2.0/bmsdna/devtools/ado_auth.py +37 -0
- bmsdna_devtools-0.2.0/bmsdna/devtools/app_service_logs.py +76 -0
- bmsdna_devtools-0.2.0/bmsdna/devtools/cli.py +138 -0
- bmsdna_devtools-0.2.0/bmsdna/devtools/cli_tools.py +31 -0
- bmsdna_devtools-0.2.0/bmsdna/devtools/commit.py +204 -0
- bmsdna_devtools-0.2.0/bmsdna/devtools/env_config.py +61 -0
- bmsdna_devtools-0.2.0/bmsdna/devtools/gh_pr.py +140 -0
- bmsdna_devtools-0.2.0/bmsdna/devtools/gitrepo.py +123 -0
- bmsdna_devtools-0.2.0/bmsdna/devtools/logs.py +170 -0
- bmsdna_devtools-0.2.0/bmsdna/devtools/pr_build.py +206 -0
- bmsdna_devtools-0.2.0/bmsdna/devtools/worktree.py +49 -0
- bmsdna_devtools-0.2.0/justfile +27 -0
- bmsdna_devtools-0.2.0/pyproject.toml +36 -0
- bmsdna_devtools-0.2.0/skills/bmsdna-devtools/SKILL.md +111 -0
- bmsdna_devtools-0.2.0/tests/test_env_config.py +95 -0
- bmsdna_devtools-0.2.0/tests/test_gh_pr.py +62 -0
- bmsdna_devtools-0.2.0/tests/test_gitrepo.py +64 -0
- bmsdna_devtools-0.2.0/tests/test_pr_build.py +38 -0
- bmsdna_devtools-0.2.0/uv.lock +330 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# This workflow will upload a Python Package using Twine when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
5
|
+
# They are provided by a third-party and are governed by
|
|
6
|
+
# separate terms of service, privacy policy, and support
|
|
7
|
+
# documentation.
|
|
8
|
+
|
|
9
|
+
name: Upload Python Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
deploy:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
environment:
|
|
20
|
+
name: pypi
|
|
21
|
+
url: https://pypi.org/p/bmsdna-devtools
|
|
22
|
+
permissions:
|
|
23
|
+
id-token: write
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- name: Set up Python
|
|
27
|
+
uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: "3.12"
|
|
30
|
+
- name: Install uv
|
|
31
|
+
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: uv sync --all-extras --all-groups
|
|
34
|
+
- name: Build package
|
|
35
|
+
run: uv build
|
|
36
|
+
- name: Publish package to PyPI
|
|
37
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Python Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
paths-ignore: ["README.md", ".github"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: ["main"]
|
|
9
|
+
paths-ignore: ["README.md", ".github"]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
- name: Install uv
|
|
26
|
+
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
27
|
+
- name: Install project dependencies
|
|
28
|
+
run: uv sync --all-extras --all-groups
|
|
29
|
+
- name: Lint (ruff)
|
|
30
|
+
run: uv run ruff check bmsdna tests
|
|
31
|
+
- name: Type check (ty)
|
|
32
|
+
run: uv run ty check bmsdna
|
|
33
|
+
- name: Test with pytest
|
|
34
|
+
run: uv run -m pytest --capture=tee-sys --maxfail=3 tests
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bmsdna-devtools
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: requests>=2.32.0
|
|
7
|
+
Requires-Dist: rich>=13.0.0
|
|
8
|
+
Requires-Dist: typer>=0.26.7
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# bmsdna-devtools
|
|
12
|
+
|
|
13
|
+
Shared developer tooling for BMS projects: PR build/check status, PR
|
|
14
|
+
creation, git worktrees, a commit-and-push helper with pre-flight checks,
|
|
15
|
+
and Azure log queries. `bdt pr *` auto-detects whether the current repo's
|
|
16
|
+
`origin` remote is Azure DevOps or GitHub and uses `az`/`gh` accordingly.
|
|
17
|
+
Consolidates near-duplicate scripts that used to be copy-pasted across
|
|
18
|
+
OneSales, ccmt2, and MDMApp into one versioned package with a `bdt` CLI.
|
|
19
|
+
|
|
20
|
+
Requires `git` always, plus `az` (Azure DevOps commands, and all `bdt logs`
|
|
21
|
+
commands) and/or `gh` (GitHub commands) on PATH as needed — each is checked
|
|
22
|
+
lazily, only when a command actually needs it, with a clear error and an
|
|
23
|
+
install link if missing rather than a raw traceback. Works on Windows: CLI
|
|
24
|
+
shims (e.g. `az.cmd`) are resolved via `shutil.which` (which honors
|
|
25
|
+
`PATHEXT`) rather than shelling out, output is decoded as UTF-8 rather than
|
|
26
|
+
relying on the console's default codepage, and file arguments accept either
|
|
27
|
+
slash style.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv tool install bmsdna-devtools
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or as a project dependency: `uv add bmsdna-devtools`.
|
|
36
|
+
|
|
37
|
+
## `bdt pr status`
|
|
38
|
+
|
|
39
|
+
Find the PR opened from the current branch and report build/check status
|
|
40
|
+
(failed steps print their logs inline). Works against Azure DevOps or
|
|
41
|
+
GitHub — whichever `origin` points at.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bdt pr status [--target-branch main] [--wait]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If the PR can't be merged, that's reported immediately instead of polling
|
|
48
|
+
for builds/checks that will never run — e.g. on Azure DevOps:
|
|
49
|
+
`PR #42 ('feat: widgets') has merge conflicts with the target branch (mergeStatus=conflicts)`;
|
|
50
|
+
on GitHub: `PR #42 ('feat: widgets') has merge conflicts with 'main' (mergeable=CONFLICTING)`.
|
|
51
|
+
Exit code 1 either way.
|
|
52
|
+
|
|
53
|
+
**Azure DevOps**: org/project/repo are auto-detected from
|
|
54
|
+
`git remote get-url origin` (handles SSH, `dev.azure.com` HTTPS, and
|
|
55
|
+
`*.visualstudio.com` HTTPS forms). Auth is an explicit PAT (`--pat` or
|
|
56
|
+
`AZURE_DEVOPS_PAT` env var), falling back to a short-lived token from the
|
|
57
|
+
caller's own `az login` — never embed a PAT literal in a script or CI file.
|
|
58
|
+
`--target-branch` selects which PR to look at (ADO's search API needs one).
|
|
59
|
+
|
|
60
|
+
**GitHub**: uses `gh`'s own auth (`gh auth login`) and always resolves the
|
|
61
|
+
PR opened from the current branch — `gh pr view` has no target-branch
|
|
62
|
+
filter, so `--target-branch` is ignored here; the PR's actual base branch
|
|
63
|
+
is shown in the output. Check status is computed from
|
|
64
|
+
`gh pr view --json statusCheckRollup` rather than `gh pr checks --json`,
|
|
65
|
+
since the latter flag isn't available in all `gh` releases.
|
|
66
|
+
|
|
67
|
+
## `bdt pr create`
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
bdt pr create --target main # or --target test
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Creates a PR from the current branch into `--target`. On Azure DevOps,
|
|
74
|
+
a thin wrapper around `az repos pr create` (org/project/repo inferred by
|
|
75
|
+
`az` itself from the git remote). On GitHub, `gh pr create --fill` (autofills
|
|
76
|
+
title/body from commit info so it never blocks on an interactive prompt).
|
|
77
|
+
Extra arguments pass through either way, e.g.
|
|
78
|
+
`bdt pr create --target main -- --title "..."`.
|
|
79
|
+
|
|
80
|
+
## `bdt worktree`
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bdt worktree my-feature [--base dev] [--env-file .local_env] [--no-submodules] [--install "just install"]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Creates `.worktrees/<name>` branched from `--base`, initializes submodules
|
|
87
|
+
(unless `--no-submodules`), and copies an env file into the new worktree as
|
|
88
|
+
`.env` (auto-detects `.local_env` then `.env` if `--env-file` isn't given).
|
|
89
|
+
|
|
90
|
+
## `bdt commit`
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
bdt commit "feat(x): add widget support" file1.py file2.py [--json] [--no-verify] [--subrepo database]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Stages, commits, and pushes the given files. Pre-flight checks: files
|
|
97
|
+
exist, commit message looks like `type: description` (skip with
|
|
98
|
+
`--skip-message-check`), not on `main`/`master` (skip with `--allow-main`).
|
|
99
|
+
Retries once (re-`git add`) if a pre-commit hook reformats files. Pass
|
|
100
|
+
`--subrepo <dir>` (repeatable) for repos that vendor a submodule (e.g.
|
|
101
|
+
`database`) — files under that prefix are committed/pushed inside the
|
|
102
|
+
submodule first, then the bump is staged in the parent repo.
|
|
103
|
+
|
|
104
|
+
Set `IS_BMS_AI_SANDBOX=1` to skip the push step (commit only) — used when
|
|
105
|
+
an AI coding sandbox pushes on its own schedule separately.
|
|
106
|
+
|
|
107
|
+
`--json` emits a machine-readable result for AI-agent callers:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"success": true, "committed": true, "pushed": true,
|
|
112
|
+
"message": "...", "files": ["..."], "commit_sha": "abc1234",
|
|
113
|
+
"error": null, "hint": null
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## `bdt logs roles` / `bdt logs tail`
|
|
118
|
+
|
|
119
|
+
Query Application Insights (KQL over `traces`/`exceptions`) via
|
|
120
|
+
`az monitor app-insights query`. No defaults are baked in — pass
|
|
121
|
+
`--resource-group`/`--app-insights` explicitly (or set
|
|
122
|
+
`AZURE_RESOURCE_GROUP`/`AZURE_APP_INSIGHTS`), since which Azure resource
|
|
123
|
+
"this repo" maps to isn't derivable from the git remote.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
bdt logs roles --resource-group my-rg --app-insights my-app-insights --minutes 60
|
|
127
|
+
bdt logs tail --resource-group my-rg --app-insights my-app-insights --role my-service --level warning
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## `bdt logs fetch`
|
|
131
|
+
|
|
132
|
+
Downloads the App Service log archive for a webapp/slot via
|
|
133
|
+
`az webapp log download`, unzips it, and writes every line matching a
|
|
134
|
+
common error/warning marker (`ERROR`, `CRITICAL`, `WARNING`, tracebacks,
|
|
135
|
+
`4xx`/`5xx`, `FAILED`, `FATAL`) to `<out>/<slot>_errors.log`. Simpler and
|
|
136
|
+
often preferable to the KQL commands above when you just want "what broke
|
|
137
|
+
recently" rather than a queryable trace stream — no defaults are baked in
|
|
138
|
+
here either.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
bdt logs fetch --webapp my-webapp --resource-group my-rg --slot production
|
|
142
|
+
bdt logs fetch --webapp my-webapp --resource-group my-rg --slot production --out logs/ --keep-archive
|
|
143
|
+
```
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# bmsdna-devtools
|
|
2
|
+
|
|
3
|
+
Shared developer tooling for BMS projects: PR build/check status, PR
|
|
4
|
+
creation, git worktrees, a commit-and-push helper with pre-flight checks,
|
|
5
|
+
and Azure log queries. `bdt pr *` auto-detects whether the current repo's
|
|
6
|
+
`origin` remote is Azure DevOps or GitHub and uses `az`/`gh` accordingly.
|
|
7
|
+
Consolidates near-duplicate scripts that used to be copy-pasted across
|
|
8
|
+
OneSales, ccmt2, and MDMApp into one versioned package with a `bdt` CLI.
|
|
9
|
+
|
|
10
|
+
Requires `git` always, plus `az` (Azure DevOps commands, and all `bdt logs`
|
|
11
|
+
commands) and/or `gh` (GitHub commands) on PATH as needed — each is checked
|
|
12
|
+
lazily, only when a command actually needs it, with a clear error and an
|
|
13
|
+
install link if missing rather than a raw traceback. Works on Windows: CLI
|
|
14
|
+
shims (e.g. `az.cmd`) are resolved via `shutil.which` (which honors
|
|
15
|
+
`PATHEXT`) rather than shelling out, output is decoded as UTF-8 rather than
|
|
16
|
+
relying on the console's default codepage, and file arguments accept either
|
|
17
|
+
slash style.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv tool install bmsdna-devtools
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or as a project dependency: `uv add bmsdna-devtools`.
|
|
26
|
+
|
|
27
|
+
## `bdt pr status`
|
|
28
|
+
|
|
29
|
+
Find the PR opened from the current branch and report build/check status
|
|
30
|
+
(failed steps print their logs inline). Works against Azure DevOps or
|
|
31
|
+
GitHub — whichever `origin` points at.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bdt pr status [--target-branch main] [--wait]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If the PR can't be merged, that's reported immediately instead of polling
|
|
38
|
+
for builds/checks that will never run — e.g. on Azure DevOps:
|
|
39
|
+
`PR #42 ('feat: widgets') has merge conflicts with the target branch (mergeStatus=conflicts)`;
|
|
40
|
+
on GitHub: `PR #42 ('feat: widgets') has merge conflicts with 'main' (mergeable=CONFLICTING)`.
|
|
41
|
+
Exit code 1 either way.
|
|
42
|
+
|
|
43
|
+
**Azure DevOps**: org/project/repo are auto-detected from
|
|
44
|
+
`git remote get-url origin` (handles SSH, `dev.azure.com` HTTPS, and
|
|
45
|
+
`*.visualstudio.com` HTTPS forms). Auth is an explicit PAT (`--pat` or
|
|
46
|
+
`AZURE_DEVOPS_PAT` env var), falling back to a short-lived token from the
|
|
47
|
+
caller's own `az login` — never embed a PAT literal in a script or CI file.
|
|
48
|
+
`--target-branch` selects which PR to look at (ADO's search API needs one).
|
|
49
|
+
|
|
50
|
+
**GitHub**: uses `gh`'s own auth (`gh auth login`) and always resolves the
|
|
51
|
+
PR opened from the current branch — `gh pr view` has no target-branch
|
|
52
|
+
filter, so `--target-branch` is ignored here; the PR's actual base branch
|
|
53
|
+
is shown in the output. Check status is computed from
|
|
54
|
+
`gh pr view --json statusCheckRollup` rather than `gh pr checks --json`,
|
|
55
|
+
since the latter flag isn't available in all `gh` releases.
|
|
56
|
+
|
|
57
|
+
## `bdt pr create`
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bdt pr create --target main # or --target test
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Creates a PR from the current branch into `--target`. On Azure DevOps,
|
|
64
|
+
a thin wrapper around `az repos pr create` (org/project/repo inferred by
|
|
65
|
+
`az` itself from the git remote). On GitHub, `gh pr create --fill` (autofills
|
|
66
|
+
title/body from commit info so it never blocks on an interactive prompt).
|
|
67
|
+
Extra arguments pass through either way, e.g.
|
|
68
|
+
`bdt pr create --target main -- --title "..."`.
|
|
69
|
+
|
|
70
|
+
## `bdt worktree`
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
bdt worktree my-feature [--base dev] [--env-file .local_env] [--no-submodules] [--install "just install"]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Creates `.worktrees/<name>` branched from `--base`, initializes submodules
|
|
77
|
+
(unless `--no-submodules`), and copies an env file into the new worktree as
|
|
78
|
+
`.env` (auto-detects `.local_env` then `.env` if `--env-file` isn't given).
|
|
79
|
+
|
|
80
|
+
## `bdt commit`
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bdt commit "feat(x): add widget support" file1.py file2.py [--json] [--no-verify] [--subrepo database]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Stages, commits, and pushes the given files. Pre-flight checks: files
|
|
87
|
+
exist, commit message looks like `type: description` (skip with
|
|
88
|
+
`--skip-message-check`), not on `main`/`master` (skip with `--allow-main`).
|
|
89
|
+
Retries once (re-`git add`) if a pre-commit hook reformats files. Pass
|
|
90
|
+
`--subrepo <dir>` (repeatable) for repos that vendor a submodule (e.g.
|
|
91
|
+
`database`) — files under that prefix are committed/pushed inside the
|
|
92
|
+
submodule first, then the bump is staged in the parent repo.
|
|
93
|
+
|
|
94
|
+
Set `IS_BMS_AI_SANDBOX=1` to skip the push step (commit only) — used when
|
|
95
|
+
an AI coding sandbox pushes on its own schedule separately.
|
|
96
|
+
|
|
97
|
+
`--json` emits a machine-readable result for AI-agent callers:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"success": true, "committed": true, "pushed": true,
|
|
102
|
+
"message": "...", "files": ["..."], "commit_sha": "abc1234",
|
|
103
|
+
"error": null, "hint": null
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## `bdt logs roles` / `bdt logs tail`
|
|
108
|
+
|
|
109
|
+
Query Application Insights (KQL over `traces`/`exceptions`) via
|
|
110
|
+
`az monitor app-insights query`. No defaults are baked in — pass
|
|
111
|
+
`--resource-group`/`--app-insights` explicitly (or set
|
|
112
|
+
`AZURE_RESOURCE_GROUP`/`AZURE_APP_INSIGHTS`), since which Azure resource
|
|
113
|
+
"this repo" maps to isn't derivable from the git remote.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
bdt logs roles --resource-group my-rg --app-insights my-app-insights --minutes 60
|
|
117
|
+
bdt logs tail --resource-group my-rg --app-insights my-app-insights --role my-service --level warning
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## `bdt logs fetch`
|
|
121
|
+
|
|
122
|
+
Downloads the App Service log archive for a webapp/slot via
|
|
123
|
+
`az webapp log download`, unzips it, and writes every line matching a
|
|
124
|
+
common error/warning marker (`ERROR`, `CRITICAL`, `WARNING`, tracebacks,
|
|
125
|
+
`4xx`/`5xx`, `FAILED`, `FATAL`) to `<out>/<slot>_errors.log`. Simpler and
|
|
126
|
+
often preferable to the KQL commands above when you just want "what broke
|
|
127
|
+
recently" rather than a queryable trace stream — no defaults are baked in
|
|
128
|
+
here either.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
bdt logs fetch --webapp my-webapp --resource-group my-rg --slot production
|
|
132
|
+
bdt logs fetch --webapp my-webapp --resource-group my-rg --slot production --out logs/ --keep-archive
|
|
133
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Azure DevOps auth: explicit PAT, or fall back to the caller's `az` login."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .cli_tools import require_az
|
|
10
|
+
|
|
11
|
+
# Well-known Azure DevOps resource ID for `az account get-access-token`.
|
|
12
|
+
ADO_RESOURCE_ID = "499b84ac-1321-427f-aa17-267ca6975798"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_az_devops_token() -> str:
|
|
16
|
+
az = require_az()
|
|
17
|
+
result = subprocess.run(
|
|
18
|
+
[az, "account", "get-access-token", "--resource", ADO_RESOURCE_ID, "--query", "accessToken", "-o", "tsv"],
|
|
19
|
+
capture_output=True,
|
|
20
|
+
encoding="utf-8",
|
|
21
|
+
)
|
|
22
|
+
if result.returncode != 0:
|
|
23
|
+
sys.exit(f"az login required and no PAT provided.\n{result.stderr.strip()}")
|
|
24
|
+
return result.stdout.strip()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def auth_header(pat: str | None) -> dict[str, str]:
|
|
28
|
+
"""Basic-auth header for an explicit PAT, else Bearer via `az` token.
|
|
29
|
+
|
|
30
|
+
Never hardcode a PAT literal in a caller — pass it in from an env var
|
|
31
|
+
(e.g. AZURE_DEVOPS_PAT) or a CLI flag, or omit it and let `az` supply a
|
|
32
|
+
short-lived token from the operator's own login.
|
|
33
|
+
"""
|
|
34
|
+
if pat:
|
|
35
|
+
token = base64.b64encode(f":{pat}".encode()).decode()
|
|
36
|
+
return {"Authorization": f"Basic {token}"}
|
|
37
|
+
return {"Authorization": f"Bearer {get_az_devops_token()}"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Download an Azure App Service log archive and filter it for errors/warnings.
|
|
2
|
+
|
|
3
|
+
Pulls the log zip via `az webapp log download`, unpacks it in memory, and
|
|
4
|
+
writes every line matching common error/warning markers to a filtered file.
|
|
5
|
+
The webapp/resource-group/slot for a given `--env` come from the consuming
|
|
6
|
+
repo's pyproject.toml (see env_config.py) rather than being baked in here.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import pathlib
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import zipfile
|
|
16
|
+
|
|
17
|
+
from .cli_tools import require_az
|
|
18
|
+
|
|
19
|
+
# Matches common error/warning markers across granian, uvicorn, and python tracebacks.
|
|
20
|
+
ERROR_RE = re.compile(r"\b(ERROR|CRITICAL|WARNING|Traceback|Exception|\b[45]\d\d\b|FAILED|FATAL)\b")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run_az(args: list[str]) -> str:
|
|
24
|
+
az = require_az()
|
|
25
|
+
proc = subprocess.run([az, *args], capture_output=True, encoding="utf-8")
|
|
26
|
+
if proc.returncode != 0:
|
|
27
|
+
sys.exit(proc.stderr.strip() or proc.stdout.strip())
|
|
28
|
+
return proc.stdout
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def download_logs(webapp: str, resource_group: str, slot: str, archive: pathlib.Path) -> None:
|
|
32
|
+
print(f"Downloading logs for {webapp}/{slot}...", flush=True)
|
|
33
|
+
run_az([
|
|
34
|
+
"webapp", "log", "download",
|
|
35
|
+
"--name", webapp,
|
|
36
|
+
"--resource-group", resource_group,
|
|
37
|
+
"--slot", slot,
|
|
38
|
+
"--log-file", str(archive),
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def extract_errors(archive: pathlib.Path, error_file: pathlib.Path) -> int:
|
|
43
|
+
"""Unzip the archive and write all matching lines to error_file. Returns count."""
|
|
44
|
+
count = 0
|
|
45
|
+
with zipfile.ZipFile(archive) as zf, error_file.open("w") as out:
|
|
46
|
+
for member in zf.namelist():
|
|
47
|
+
if member.endswith("/"):
|
|
48
|
+
continue
|
|
49
|
+
with zf.open(member) as fh:
|
|
50
|
+
for raw in fh:
|
|
51
|
+
line = raw.decode("utf-8", "replace")
|
|
52
|
+
if ERROR_RE.search(line):
|
|
53
|
+
out.write(f"{member}: {line}")
|
|
54
|
+
count += 1
|
|
55
|
+
return count
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def fetch(
|
|
59
|
+
webapp: str,
|
|
60
|
+
resource_group: str,
|
|
61
|
+
slot: str,
|
|
62
|
+
out_dir: pathlib.Path,
|
|
63
|
+
keep_archive: bool = False,
|
|
64
|
+
) -> pathlib.Path:
|
|
65
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
archive = out_dir / f"{slot}_logs.zip"
|
|
67
|
+
error_file = out_dir / f"{slot}_errors.log"
|
|
68
|
+
|
|
69
|
+
download_logs(webapp, resource_group, slot, archive)
|
|
70
|
+
count = extract_errors(archive, error_file)
|
|
71
|
+
|
|
72
|
+
if not keep_archive:
|
|
73
|
+
archive.unlink()
|
|
74
|
+
|
|
75
|
+
print(f"✓ {count} error/warning lines → {error_file}")
|
|
76
|
+
return error_file
|