bmsdna-devtools 0.2.0__tar.gz → 0.2.1__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 → bmsdna_devtools-0.2.1}/PKG-INFO +20 -5
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/README.md +19 -4
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/bmsdna/devtools/app_service_logs.py +7 -6
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/bmsdna/devtools/cli.py +1 -1
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/bmsdna/devtools/commit.py +37 -5
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/bmsdna/devtools/env_config.py +8 -2
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/pyproject.toml +1 -1
- bmsdna_devtools-0.2.1/tests/test_app_service_logs.py +39 -0
- bmsdna_devtools-0.2.1/tests/test_commit.py +124 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/tests/test_env_config.py +12 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/uv.lock +1 -1
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/.github/workflows/python-publish.yml +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/.github/workflows/python-test.yml +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/.gitignore +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/.python-version +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/bmsdna/devtools/__init__.py +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/bmsdna/devtools/ado_auth.py +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/bmsdna/devtools/cli_tools.py +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/bmsdna/devtools/gh_pr.py +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/bmsdna/devtools/gitrepo.py +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/bmsdna/devtools/logs.py +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/bmsdna/devtools/pr_build.py +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/bmsdna/devtools/worktree.py +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/justfile +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/skills/bmsdna-devtools/SKILL.md +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/tests/test_gh_pr.py +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/tests/test_gitrepo.py +0 -0
- {bmsdna_devtools-0.2.0 → bmsdna_devtools-0.2.1}/tests/test_pr_build.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bmsdna-devtools
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: requests>=2.32.0
|
|
@@ -134,10 +134,25 @@ Downloads the App Service log archive for a webapp/slot via
|
|
|
134
134
|
common error/warning marker (`ERROR`, `CRITICAL`, `WARNING`, tracebacks,
|
|
135
135
|
`4xx`/`5xx`, `FAILED`, `FATAL`) to `<out>/<slot>_errors.log`. Simpler and
|
|
136
136
|
often preferable to the KQL commands above when you just want "what broke
|
|
137
|
-
recently" rather than a queryable trace stream
|
|
138
|
-
|
|
137
|
+
recently" rather than a queryable trace stream.
|
|
138
|
+
|
|
139
|
+
The webapp/resource-group/slot come from a named environment configured in
|
|
140
|
+
the calling repo's `pyproject.toml`. `slot` is optional — omit it for an
|
|
141
|
+
app's default/production slot (no `--slot` is passed to `az`); set it for a
|
|
142
|
+
named deployment slot:
|
|
143
|
+
|
|
144
|
+
```toml
|
|
145
|
+
[tool.bdt.envs.prod]
|
|
146
|
+
webapp = "my-webapp"
|
|
147
|
+
resource_group = "my-rg"
|
|
148
|
+
|
|
149
|
+
[tool.bdt.envs.test]
|
|
150
|
+
webapp = "my-webapp"
|
|
151
|
+
resource_group = "my-rg"
|
|
152
|
+
slot = "test"
|
|
153
|
+
```
|
|
139
154
|
|
|
140
155
|
```bash
|
|
141
|
-
bdt logs fetch --
|
|
142
|
-
bdt logs fetch --
|
|
156
|
+
bdt logs fetch --env prod
|
|
157
|
+
bdt logs fetch --env prod --out logs/ --keep-archive
|
|
143
158
|
```
|
|
@@ -124,10 +124,25 @@ Downloads the App Service log archive for a webapp/slot via
|
|
|
124
124
|
common error/warning marker (`ERROR`, `CRITICAL`, `WARNING`, tracebacks,
|
|
125
125
|
`4xx`/`5xx`, `FAILED`, `FATAL`) to `<out>/<slot>_errors.log`. Simpler and
|
|
126
126
|
often preferable to the KQL commands above when you just want "what broke
|
|
127
|
-
recently" rather than a queryable trace stream
|
|
128
|
-
|
|
127
|
+
recently" rather than a queryable trace stream.
|
|
128
|
+
|
|
129
|
+
The webapp/resource-group/slot come from a named environment configured in
|
|
130
|
+
the calling repo's `pyproject.toml`. `slot` is optional — omit it for an
|
|
131
|
+
app's default/production slot (no `--slot` is passed to `az`); set it for a
|
|
132
|
+
named deployment slot:
|
|
133
|
+
|
|
134
|
+
```toml
|
|
135
|
+
[tool.bdt.envs.prod]
|
|
136
|
+
webapp = "my-webapp"
|
|
137
|
+
resource_group = "my-rg"
|
|
138
|
+
|
|
139
|
+
[tool.bdt.envs.test]
|
|
140
|
+
webapp = "my-webapp"
|
|
141
|
+
resource_group = "my-rg"
|
|
142
|
+
slot = "test"
|
|
143
|
+
```
|
|
129
144
|
|
|
130
145
|
```bash
|
|
131
|
-
bdt logs fetch --
|
|
132
|
-
bdt logs fetch --
|
|
146
|
+
bdt logs fetch --env prod
|
|
147
|
+
bdt logs fetch --env prod --out logs/ --keep-archive
|
|
133
148
|
```
|
|
@@ -28,13 +28,13 @@ def run_az(args: list[str]) -> str:
|
|
|
28
28
|
return proc.stdout
|
|
29
29
|
|
|
30
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)
|
|
31
|
+
def download_logs(webapp: str, resource_group: str, slot: str | None, archive: pathlib.Path) -> None:
|
|
32
|
+
print(f"Downloading logs for {webapp}/{slot or '(production)'}...", flush=True)
|
|
33
33
|
run_az([
|
|
34
34
|
"webapp", "log", "download",
|
|
35
35
|
"--name", webapp,
|
|
36
36
|
"--resource-group", resource_group,
|
|
37
|
-
"--slot", slot,
|
|
37
|
+
*(["--slot", slot] if slot else []),
|
|
38
38
|
"--log-file", str(archive),
|
|
39
39
|
])
|
|
40
40
|
|
|
@@ -58,13 +58,14 @@ def extract_errors(archive: pathlib.Path, error_file: pathlib.Path) -> int:
|
|
|
58
58
|
def fetch(
|
|
59
59
|
webapp: str,
|
|
60
60
|
resource_group: str,
|
|
61
|
-
slot: str,
|
|
61
|
+
slot: str | None,
|
|
62
62
|
out_dir: pathlib.Path,
|
|
63
63
|
keep_archive: bool = False,
|
|
64
64
|
) -> pathlib.Path:
|
|
65
65
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
label = slot or "production"
|
|
67
|
+
archive = out_dir / f"{label}_logs.zip"
|
|
68
|
+
error_file = out_dir / f"{label}_errors.log"
|
|
68
69
|
|
|
69
70
|
download_logs(webapp, resource_group, slot, archive)
|
|
70
71
|
count = extract_errors(archive, error_file)
|
|
@@ -131,7 +131,7 @@ def logs_fetch(
|
|
|
131
131
|
) -> None:
|
|
132
132
|
"""Download an App Service log archive and extract error/warning lines."""
|
|
133
133
|
cfg = env_config.resolve_env(env)
|
|
134
|
-
app_service_logs.fetch(cfg["webapp"], cfg["resource_group"], cfg
|
|
134
|
+
app_service_logs.fetch(cfg["webapp"], cfg["resource_group"], cfg.get("slot"), out, keep_archive=keep_archive)
|
|
135
135
|
|
|
136
136
|
|
|
137
137
|
if __name__ == "__main__":
|
|
@@ -30,6 +30,26 @@ def _in_sandbox() -> bool:
|
|
|
30
30
|
return os.getenv(IS_SANDBOX_ENV_VAR, "0").lower() in ("1", "true", "yes")
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _staged_deletion(path: str, cwd: str | None = None) -> bool:
|
|
34
|
+
"""True if `path` is already staged as a deletion (e.g. via `git rm`) in
|
|
35
|
+
the repo at `cwd`. This is a deliberate-intent signal, not just "HEAD used
|
|
36
|
+
to have this path" — a file that vanished because a write failed or raced
|
|
37
|
+
would be missing from disk but NOT staged as a deletion, so it still
|
|
38
|
+
fails the check below instead of silently being committed as removed."""
|
|
39
|
+
r = _run(["git", "diff", "--cached", "--name-only", "--diff-filter=D", "--", path], cwd=cwd)
|
|
40
|
+
return r.returncode == 0 and path in r.stdout.splitlines()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _present_or_staged_deletion(path: str, subrepos: list[str]) -> bool:
|
|
44
|
+
if os.path.exists(path):
|
|
45
|
+
return True
|
|
46
|
+
for subrepo in subrepos:
|
|
47
|
+
prefix = subrepo + "/"
|
|
48
|
+
if path.startswith(prefix):
|
|
49
|
+
return _staged_deletion(path[len(prefix):], cwd=subrepo)
|
|
50
|
+
return _staged_deletion(path)
|
|
51
|
+
|
|
52
|
+
|
|
33
53
|
@dataclass
|
|
34
54
|
class CommitResult:
|
|
35
55
|
success: bool
|
|
@@ -64,17 +84,29 @@ def _git_commit(message: str, cwd: str | None = None, no_verify: bool = False) -
|
|
|
64
84
|
return _run(cmd, cwd=cwd)
|
|
65
85
|
|
|
66
86
|
|
|
87
|
+
def _add(files: list[str], cwd: str | None) -> None:
|
|
88
|
+
"""`git add`, excluding paths already fully removed (disk + index) by a
|
|
89
|
+
prior `git rm` -- those have nothing left to add, and `git add` fails its
|
|
90
|
+
ENTIRE invocation (staging nothing, for any path in the same call) if even
|
|
91
|
+
one pathspec doesn't match anything. Passing only what's actually present
|
|
92
|
+
on disk avoids silently dropping the other files' staged content."""
|
|
93
|
+
root = cwd or "."
|
|
94
|
+
addable = [f for f in files if os.path.exists(os.path.join(root, f))]
|
|
95
|
+
if addable:
|
|
96
|
+
_run(["git", "add", *addable], cwd=cwd)
|
|
97
|
+
|
|
98
|
+
|
|
67
99
|
def _commit_with_retry(
|
|
68
100
|
message: str, files: list[str], cwd: str | None, no_verify: bool
|
|
69
101
|
) -> tuple[bool, subprocess.CompletedProcess | None]:
|
|
70
102
|
"""Commit, retrying once (re-`git add`) if a pre-commit hook reformatted files."""
|
|
71
|
-
|
|
103
|
+
_add(files, cwd)
|
|
72
104
|
r = _git_commit(message, cwd=cwd, no_verify=no_verify)
|
|
73
105
|
if r.returncode == 0:
|
|
74
106
|
return True, None
|
|
75
107
|
if "nothing to commit" in r.stdout + r.stderr:
|
|
76
108
|
return False, None
|
|
77
|
-
|
|
109
|
+
_add(files, cwd)
|
|
78
110
|
r2 = _git_commit(message, cwd=cwd, no_verify=no_verify)
|
|
79
111
|
if r2.returncode == 0:
|
|
80
112
|
return True, None
|
|
@@ -108,8 +140,8 @@ def commit_and_push(
|
|
|
108
140
|
print(f" {'✓' if ok else '✗'} {label}", file=sys.stdout if ok else sys.stderr)
|
|
109
141
|
return ok
|
|
110
142
|
|
|
111
|
-
missing = [f for f in files if not
|
|
112
|
-
if not check(not missing, "files exist"):
|
|
143
|
+
missing = [f for f in files if not _present_or_staged_deletion(f, subrepos)]
|
|
144
|
+
if not check(not missing, "files exist (or are a staged deletion)"):
|
|
113
145
|
return CommitResult(
|
|
114
146
|
False, False, False, message, files,
|
|
115
147
|
error=f"File not found: {missing[0]} — did you typo the path? Run `git status` to see changed files",
|
|
@@ -177,7 +209,7 @@ def commit_and_push(
|
|
|
177
209
|
pr = _run(["git", "push"])
|
|
178
210
|
if pr.returncode != 0:
|
|
179
211
|
return CommitResult(
|
|
180
|
-
|
|
212
|
+
False, committed, False, message, files,
|
|
181
213
|
commit_sha=_sha(),
|
|
182
214
|
error=(pr.stdout + pr.stderr).strip(),
|
|
183
215
|
hint="Push rejected. Run `git pull --rebase`, resolve conflicts, then retry.",
|
|
@@ -11,13 +11,19 @@ import sys
|
|
|
11
11
|
import tomllib
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
REQUIRED_KEYS = ("webapp", "resource_group"
|
|
14
|
+
REQUIRED_KEYS = ("webapp", "resource_group")
|
|
15
15
|
|
|
16
16
|
CONFIG_EXAMPLE = """\
|
|
17
17
|
[tool.bdt.envs.prod]
|
|
18
18
|
webapp = "my-app"
|
|
19
19
|
resource_group = "my-app-rg"
|
|
20
|
-
slot
|
|
20
|
+
# slot is optional — omit it for an app's default/production slot,
|
|
21
|
+
# set it for a named deployment slot (e.g. "test", "stage").
|
|
22
|
+
|
|
23
|
+
[tool.bdt.envs.test]
|
|
24
|
+
webapp = "my-app"
|
|
25
|
+
resource_group = "my-app-rg"
|
|
26
|
+
slot = "test"
|
|
21
27
|
"""
|
|
22
28
|
|
|
23
29
|
|
|
@@ -10,7 +10,7 @@ packages = ["bmsdna"]
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "bmsdna-devtools"
|
|
13
|
-
version = "0.2.
|
|
13
|
+
version = "0.2.1"
|
|
14
14
|
description = "Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects"
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import zipfile
|
|
2
|
+
|
|
3
|
+
from bmsdna.devtools import app_service_logs
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_download_logs_omits_slot_flag_when_none(monkeypatch, tmp_path):
|
|
7
|
+
captured = {}
|
|
8
|
+
|
|
9
|
+
def fake_run_az(args):
|
|
10
|
+
captured["args"] = args
|
|
11
|
+
return ""
|
|
12
|
+
|
|
13
|
+
monkeypatch.setattr(app_service_logs, "run_az", fake_run_az)
|
|
14
|
+
app_service_logs.download_logs("my-app", "my-rg", None, tmp_path / "archive.zip")
|
|
15
|
+
assert "--slot" not in captured["args"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_download_logs_includes_slot_flag_when_given(monkeypatch, tmp_path):
|
|
19
|
+
captured = {}
|
|
20
|
+
|
|
21
|
+
def fake_run_az(args):
|
|
22
|
+
captured["args"] = args
|
|
23
|
+
return ""
|
|
24
|
+
|
|
25
|
+
monkeypatch.setattr(app_service_logs, "run_az", fake_run_az)
|
|
26
|
+
app_service_logs.download_logs("my-app", "my-rg", "test", tmp_path / "archive.zip")
|
|
27
|
+
assert "--slot" in captured["args"]
|
|
28
|
+
assert captured["args"][captured["args"].index("--slot") + 1] == "test"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_fetch_uses_production_label_when_slot_is_none(monkeypatch, tmp_path):
|
|
32
|
+
def fake_download(webapp, resource_group, slot, archive):
|
|
33
|
+
with zipfile.ZipFile(archive, "w"):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
monkeypatch.setattr(app_service_logs, "download_logs", fake_download)
|
|
37
|
+
error_file = app_service_logs.fetch("my-app", "my-rg", None, tmp_path, keep_archive=True)
|
|
38
|
+
assert error_file == tmp_path / "production_errors.log"
|
|
39
|
+
assert (tmp_path / "production_logs.zip").exists()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
from bmsdna.devtools.commit import commit_and_push
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def init_repo(path):
|
|
7
|
+
subprocess.run(["git", "init", "-q", "-b", "feature"], cwd=path, check=True)
|
|
8
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=path, check=True)
|
|
9
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=path, check=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_commit_and_push_fails_when_push_has_no_remote(tmp_path, monkeypatch):
|
|
13
|
+
init_repo(tmp_path)
|
|
14
|
+
(tmp_path / "a.txt").write_text("hello")
|
|
15
|
+
monkeypatch.chdir(tmp_path)
|
|
16
|
+
|
|
17
|
+
result = commit_and_push(
|
|
18
|
+
"feat(x): add a.txt",
|
|
19
|
+
["a.txt"],
|
|
20
|
+
require_message_quality=False,
|
|
21
|
+
require_feature_branch=False,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
assert result.committed is True
|
|
25
|
+
assert result.pushed is False
|
|
26
|
+
assert result.success is False
|
|
27
|
+
assert result.error
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_commit_and_push_allows_staged_deletion(tmp_path, monkeypatch):
|
|
31
|
+
init_repo(tmp_path)
|
|
32
|
+
(tmp_path / "a.txt").write_text("hello")
|
|
33
|
+
monkeypatch.chdir(tmp_path)
|
|
34
|
+
subprocess.run(["git", "add", "a.txt"], cwd=tmp_path, check=True)
|
|
35
|
+
subprocess.run(["git", "commit", "-q", "-m", "init"], cwd=tmp_path, check=True)
|
|
36
|
+
|
|
37
|
+
subprocess.run(["git", "rm", "-q", "a.txt"], cwd=tmp_path, check=True)
|
|
38
|
+
|
|
39
|
+
result = commit_and_push(
|
|
40
|
+
"feat(x): remove a.txt",
|
|
41
|
+
["a.txt"],
|
|
42
|
+
require_message_quality=False,
|
|
43
|
+
require_feature_branch=False,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
assert result.committed is True
|
|
47
|
+
assert result.error != "File not found: a.txt — did you typo the path? Run `git status` to see changed files"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_commit_and_push_rejects_unstaged_missing_file(tmp_path, monkeypatch):
|
|
51
|
+
"""A file that vanished from disk without git being told (e.g. a failed
|
|
52
|
+
write) must still be rejected -- only an already-*staged* deletion (via
|
|
53
|
+
`git rm`) is treated as an intentional removal."""
|
|
54
|
+
init_repo(tmp_path)
|
|
55
|
+
(tmp_path / "a.txt").write_text("hello")
|
|
56
|
+
monkeypatch.chdir(tmp_path)
|
|
57
|
+
subprocess.run(["git", "add", "a.txt"], cwd=tmp_path, check=True)
|
|
58
|
+
subprocess.run(["git", "commit", "-q", "-m", "init"], cwd=tmp_path, check=True)
|
|
59
|
+
|
|
60
|
+
(tmp_path / "a.txt").unlink()
|
|
61
|
+
|
|
62
|
+
result = commit_and_push(
|
|
63
|
+
"feat(x): update a.txt",
|
|
64
|
+
["a.txt"],
|
|
65
|
+
require_message_quality=False,
|
|
66
|
+
require_feature_branch=False,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assert result.committed is False
|
|
70
|
+
assert result.error == "File not found: a.txt — did you typo the path? Run `git status` to see changed files"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_commit_and_push_stages_modified_file_alongside_staged_deletion(tmp_path, monkeypatch):
|
|
74
|
+
"""Regression test: `git add <modified-file> <already-git-rm'd-file>` fails
|
|
75
|
+
its ENTIRE invocation (git errors "pathspec did not match any files" for
|
|
76
|
+
the already-removed path, staging nothing at all in that call) -- which
|
|
77
|
+
previously silently dropped the modified file's new content from the
|
|
78
|
+
commit, since _commit_with_retry didn't check git add's exit code."""
|
|
79
|
+
init_repo(tmp_path)
|
|
80
|
+
(tmp_path / "a.md").write_text("old")
|
|
81
|
+
(tmp_path / "gone.py").write_text("old")
|
|
82
|
+
subprocess.run(["git", "add", "a.md", "gone.py"], cwd=tmp_path, check=True)
|
|
83
|
+
subprocess.run(["git", "commit", "-q", "-m", "init"], cwd=tmp_path, check=True)
|
|
84
|
+
monkeypatch.chdir(tmp_path)
|
|
85
|
+
|
|
86
|
+
subprocess.run(["git", "rm", "-q", "gone.py"], cwd=tmp_path, check=True)
|
|
87
|
+
(tmp_path / "a.md").write_text("new content")
|
|
88
|
+
|
|
89
|
+
result = commit_and_push(
|
|
90
|
+
"feat(x): update a.md and remove gone.py",
|
|
91
|
+
["a.md", "gone.py"],
|
|
92
|
+
require_message_quality=False,
|
|
93
|
+
require_feature_branch=False,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
assert result.committed is True
|
|
97
|
+
committed_content = subprocess.run(
|
|
98
|
+
["git", "show", "HEAD:a.md"], cwd=tmp_path, capture_output=True, text=True, check=True
|
|
99
|
+
).stdout
|
|
100
|
+
assert committed_content == "new content"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_commit_and_push_allows_staged_deletion_in_subrepo(tmp_path, monkeypatch):
|
|
104
|
+
subrepo = tmp_path / "database"
|
|
105
|
+
subrepo.mkdir()
|
|
106
|
+
init_repo(subrepo)
|
|
107
|
+
(subrepo / "schema.sql").write_text("create table t();")
|
|
108
|
+
subprocess.run(["git", "add", "schema.sql"], cwd=subrepo, check=True)
|
|
109
|
+
subprocess.run(["git", "commit", "-q", "-m", "init"], cwd=subrepo, check=True)
|
|
110
|
+
|
|
111
|
+
init_repo(tmp_path)
|
|
112
|
+
monkeypatch.chdir(tmp_path)
|
|
113
|
+
subprocess.run(["git", "commit", "-q", "-m", "init", "--allow-empty"], cwd=tmp_path, check=True)
|
|
114
|
+
subprocess.run(["git", "rm", "-q", "schema.sql"], cwd=subrepo, check=True)
|
|
115
|
+
|
|
116
|
+
result = commit_and_push(
|
|
117
|
+
"feat(x): remove schema.sql",
|
|
118
|
+
["database/schema.sql"],
|
|
119
|
+
require_message_quality=False,
|
|
120
|
+
require_feature_branch=False,
|
|
121
|
+
subrepos=["database"],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
assert result.error != "File not found: database/schema.sql — did you typo the path? Run `git status` to see changed files"
|
|
@@ -93,3 +93,15 @@ slot = "production"
|
|
|
93
93
|
"resource_group": "my-app-rg",
|
|
94
94
|
"slot": "production",
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_resolve_env_allows_missing_slot(tmp_path):
|
|
99
|
+
write_pyproject(
|
|
100
|
+
tmp_path,
|
|
101
|
+
"""
|
|
102
|
+
[tool.bdt.envs.prod]
|
|
103
|
+
webapp = "my-app"
|
|
104
|
+
resource_group = "my-app-rg"
|
|
105
|
+
""",
|
|
106
|
+
)
|
|
107
|
+
assert resolve_env("prod", tmp_path) == {"webapp": "my-app", "resource_group": "my-app-rg"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|