release-status 0.2.0__py3-none-any.whl
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.
- release_status/__init__.py +0 -0
- release_status/cache.py +53 -0
- release_status/cli.py +495 -0
- release_status/config.py +175 -0
- release_status/models.py +69 -0
- release_status/providers.py +366 -0
- release_status/resolvers.py +90 -0
- release_status/version.py +109 -0
- release_status/views.py +195 -0
- release_status-0.2.0.dist-info/METADATA +317 -0
- release_status-0.2.0.dist-info/RECORD +13 -0
- release_status-0.2.0.dist-info/WHEEL +4 -0
- release_status-0.2.0.dist-info/entry_points.txt +3 -0
|
File without changes
|
release_status/cache.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
class Cache:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
cache_dir: Path,
|
|
13
|
+
ttl: timedelta,
|
|
14
|
+
) -> None:
|
|
15
|
+
self.cache_dir = cache_dir
|
|
16
|
+
self.ttl = ttl
|
|
17
|
+
self.enabled = True
|
|
18
|
+
|
|
19
|
+
def _key_path(self, key: str) -> Path:
|
|
20
|
+
h = hashlib.sha256(key.encode()).hexdigest()[:16]
|
|
21
|
+
return self.cache_dir / f"{h}.json"
|
|
22
|
+
|
|
23
|
+
def get(self, key: str) -> Any | None:
|
|
24
|
+
if not self.enabled:
|
|
25
|
+
return None
|
|
26
|
+
path = self._key_path(key)
|
|
27
|
+
if not path.exists():
|
|
28
|
+
return None
|
|
29
|
+
entry = json.loads(path.read_text())
|
|
30
|
+
cached_at = datetime.fromisoformat(entry["cached_at"])
|
|
31
|
+
if datetime.now(timezone.utc) - cached_at > self.ttl:
|
|
32
|
+
path.unlink()
|
|
33
|
+
return None
|
|
34
|
+
return entry["data"]
|
|
35
|
+
|
|
36
|
+
def set(self, key: str, data: Any) -> None:
|
|
37
|
+
if not self.enabled:
|
|
38
|
+
return
|
|
39
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
entry = {
|
|
41
|
+
"cached_at": datetime.now(timezone.utc).isoformat(),
|
|
42
|
+
"data": data,
|
|
43
|
+
}
|
|
44
|
+
self._key_path(key).write_text(json.dumps(entry))
|
|
45
|
+
|
|
46
|
+
def clear(self) -> int:
|
|
47
|
+
if not self.cache_dir.exists():
|
|
48
|
+
return 0
|
|
49
|
+
count = 0
|
|
50
|
+
for f in self.cache_dir.glob("*.json"):
|
|
51
|
+
f.unlink()
|
|
52
|
+
count += 1
|
|
53
|
+
return count
|
release_status/cli.py
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated, Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from importlib.metadata import version as pkg_version
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from release_status.cache import Cache
|
|
16
|
+
from release_status.config import (
|
|
17
|
+
AppConfig,
|
|
18
|
+
ProjectConfig,
|
|
19
|
+
generate_schema,
|
|
20
|
+
load_config,
|
|
21
|
+
resolve_config_path,
|
|
22
|
+
)
|
|
23
|
+
from release_status.models import Commit, EnvironmentStatus, ProviderError, ToolNotFoundError
|
|
24
|
+
from release_status.providers import check_cli_tools, get_provider
|
|
25
|
+
from release_status.resolvers import resolve_environment
|
|
26
|
+
from release_status.version import check_for_update, check_for_update_strict, clear_update_cache, get_current_version, PACKAGE_NAME
|
|
27
|
+
from release_status.views import render_commits, render_environments, render_projects
|
|
28
|
+
|
|
29
|
+
app = typer.Typer(
|
|
30
|
+
name="release-status",
|
|
31
|
+
help="Show release/deployment status across multiple projects.",
|
|
32
|
+
no_args_is_help=True,
|
|
33
|
+
)
|
|
34
|
+
console = Console()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _State:
|
|
38
|
+
config_path: Path | None = None
|
|
39
|
+
no_cache: bool = False
|
|
40
|
+
since_days: int | None = None
|
|
41
|
+
branch: str | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_state = _State()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _complete_project(incomplete: str) -> list[str]:
|
|
48
|
+
try:
|
|
49
|
+
path = resolve_config_path(_state.config_path)
|
|
50
|
+
config = load_config(path)
|
|
51
|
+
return [p.name for p in config.projects if p.name.lower().startswith(incomplete.lower())]
|
|
52
|
+
except Exception:
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _version_callback(value: bool) -> None:
|
|
57
|
+
if value:
|
|
58
|
+
console.print(pkg_version("release-status"))
|
|
59
|
+
raise typer.Exit()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.callback()
|
|
63
|
+
def main(
|
|
64
|
+
_version: Annotated[
|
|
65
|
+
bool, typer.Option("--version", "-v", help="Show version", callback=_version_callback, is_eager=True)
|
|
66
|
+
] = False,
|
|
67
|
+
config: Annotated[
|
|
68
|
+
Optional[Path], typer.Option("--config", "-c", help="Path to config file")
|
|
69
|
+
] = None,
|
|
70
|
+
no_cache: Annotated[
|
|
71
|
+
bool, typer.Option("--no-cache", help="Disable cache")
|
|
72
|
+
] = False,
|
|
73
|
+
since: Annotated[
|
|
74
|
+
Optional[int], typer.Option("--since", help="Override days to look back for commits")
|
|
75
|
+
] = None,
|
|
76
|
+
branch: Annotated[
|
|
77
|
+
Optional[str], typer.Option("--branch", "-b", help="Override branch to fetch commits from")
|
|
78
|
+
] = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Release Status — deployment tracking across projects."""
|
|
81
|
+
_state.config_path = config
|
|
82
|
+
_state.no_cache = no_cache
|
|
83
|
+
_state.since_days = since
|
|
84
|
+
_state.branch = branch
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command()
|
|
88
|
+
def commits(
|
|
89
|
+
project: Annotated[str, typer.Argument(help="Project name", autocompletion=_complete_project)],
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Show recent commits with environment deployment markers."""
|
|
92
|
+
cfg = _load_config()
|
|
93
|
+
proj = _apply_branch_override(_find_project(cfg, project))
|
|
94
|
+
cache = _make_cache(cfg)
|
|
95
|
+
since = _since_days(cfg)
|
|
96
|
+
cache_ttl = 0 if _state.no_cache else cfg.cache_ttl_minutes
|
|
97
|
+
current_version = get_current_version()
|
|
98
|
+
|
|
99
|
+
with console.status("Fetching commits..."):
|
|
100
|
+
update_available = check_for_update(current_version)
|
|
101
|
+
commit_list = _fetch_commits(proj, cache, since)
|
|
102
|
+
|
|
103
|
+
with console.status("Checking environments..."):
|
|
104
|
+
env_statuses = _fetch_environments(proj, cache)
|
|
105
|
+
|
|
106
|
+
_fetch_missing_commits(commit_list, env_statuses, proj, cache)
|
|
107
|
+
render_commits(proj, commit_list, env_statuses, since, cache_ttl, console, current_version, update_available)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command()
|
|
111
|
+
def envs(
|
|
112
|
+
project: Annotated[str, typer.Argument(help="Project name", autocompletion=_complete_project)],
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Show environment deployment status."""
|
|
115
|
+
cfg = _load_config()
|
|
116
|
+
proj = _apply_branch_override(_find_project(cfg, project))
|
|
117
|
+
cache = _make_cache(cfg)
|
|
118
|
+
since = _since_days(cfg)
|
|
119
|
+
cache_ttl = 0 if _state.no_cache else cfg.cache_ttl_minutes
|
|
120
|
+
current_version = get_current_version()
|
|
121
|
+
|
|
122
|
+
with console.status("Fetching data..."):
|
|
123
|
+
update_available = check_for_update(current_version)
|
|
124
|
+
commit_list = _fetch_commits(proj, cache, since)
|
|
125
|
+
env_statuses = _fetch_environments(proj, cache)
|
|
126
|
+
|
|
127
|
+
_fetch_missing_commits(commit_list, env_statuses, proj, cache)
|
|
128
|
+
render_environments(proj, commit_list, env_statuses, since, cache_ttl, console, current_version, update_available)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@app.command()
|
|
132
|
+
def projects() -> None:
|
|
133
|
+
"""List all configured projects."""
|
|
134
|
+
cfg = _load_config()
|
|
135
|
+
render_projects(cfg, console)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.command()
|
|
139
|
+
def check() -> None:
|
|
140
|
+
"""Validate configuration and check CLI tool availability."""
|
|
141
|
+
path = resolve_config_path(_state.config_path)
|
|
142
|
+
cfg = _load_config()
|
|
143
|
+
console.print(f"[green]Config is valid:[/green] {path}")
|
|
144
|
+
console.print(f" Projects: {len(cfg.projects)}")
|
|
145
|
+
|
|
146
|
+
has_issues = False
|
|
147
|
+
for proj in cfg.projects:
|
|
148
|
+
issue = check_cli_tools(proj.repository.provider)
|
|
149
|
+
if issue:
|
|
150
|
+
console.print(f" [red]WARN[/red] {proj.name}: {issue}")
|
|
151
|
+
has_issues = True
|
|
152
|
+
else:
|
|
153
|
+
console.print(f" [green]OK[/green] {proj.name}: {proj.repository.provider.type}")
|
|
154
|
+
|
|
155
|
+
if not has_issues:
|
|
156
|
+
console.print("[green]All checks passed.[/green]")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.command()
|
|
160
|
+
def schema() -> None:
|
|
161
|
+
"""Print JSON Schema for the config file."""
|
|
162
|
+
console.print(json.dumps(generate_schema(), indent=2))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.command(name="clear-cache")
|
|
166
|
+
def clear_cache() -> None:
|
|
167
|
+
"""Clear all cached data."""
|
|
168
|
+
cfg = _load_config()
|
|
169
|
+
count = _make_cache(cfg).clear()
|
|
170
|
+
console.print(f"Cleared {count} cache entries.")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@app.command()
|
|
174
|
+
def update() -> None:
|
|
175
|
+
"""Update release-status to the latest version."""
|
|
176
|
+
current = get_current_version()
|
|
177
|
+
update_version, check_ok = check_for_update_strict(current)
|
|
178
|
+
|
|
179
|
+
if not update_version:
|
|
180
|
+
if check_ok:
|
|
181
|
+
console.print(f"Already up to date (v{current}).")
|
|
182
|
+
else:
|
|
183
|
+
console.print(f"[red]Could not check for updates.[/red] Current version: v{current}")
|
|
184
|
+
raise typer.Exit()
|
|
185
|
+
|
|
186
|
+
if not shutil.which("uv"):
|
|
187
|
+
console.print("[red]uv not found.[/red] Install it from https://docs.astral.sh/uv/")
|
|
188
|
+
console.print(f"Or update manually: pip install --upgrade {PACKAGE_NAME}")
|
|
189
|
+
raise typer.Exit(1)
|
|
190
|
+
|
|
191
|
+
console.print(f"Updating: v{current} → v{update_version}")
|
|
192
|
+
result = subprocess.run(
|
|
193
|
+
["uv", "tool", "install", PACKAGE_NAME, "--force", "--reinstall"],
|
|
194
|
+
capture_output=True,
|
|
195
|
+
text=True,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if result.returncode == 0:
|
|
199
|
+
clear_update_cache()
|
|
200
|
+
console.print(f"[green]Updated to v{update_version}.[/green]")
|
|
201
|
+
else:
|
|
202
|
+
console.print(f"[red]Update failed:[/red]\n{result.stderr.strip()}")
|
|
203
|
+
raise typer.Exit(1)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@app.command()
|
|
207
|
+
def init() -> None:
|
|
208
|
+
"""Create a starter config file."""
|
|
209
|
+
path = resolve_config_path(_state.config_path)
|
|
210
|
+
if path.exists():
|
|
211
|
+
console.print(f"Config already exists: {path}")
|
|
212
|
+
raise typer.Exit(1)
|
|
213
|
+
|
|
214
|
+
starter = {
|
|
215
|
+
"cache_dir": str(Path.home() / ".cache" / "release-status"),
|
|
216
|
+
"cache_ttl_minutes": 5,
|
|
217
|
+
"since_days": 180,
|
|
218
|
+
"projects": [
|
|
219
|
+
{
|
|
220
|
+
"name": "project-github-cli",
|
|
221
|
+
"repository": {
|
|
222
|
+
"url": "https://github.com/org/repo.git",
|
|
223
|
+
"branch": "main",
|
|
224
|
+
"provider": {"type": "github-cli"},
|
|
225
|
+
},
|
|
226
|
+
"environments": [
|
|
227
|
+
{
|
|
228
|
+
"name": "dev",
|
|
229
|
+
"url": "https://dev.example.com/build.json",
|
|
230
|
+
"source": {
|
|
231
|
+
"type": "json",
|
|
232
|
+
"fields": {"version": "$.version"},
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "project-github-api",
|
|
239
|
+
"repository": {
|
|
240
|
+
"url": "https://github.com/org/repo.git",
|
|
241
|
+
"branch": "main",
|
|
242
|
+
"provider": {
|
|
243
|
+
"type": "github-api",
|
|
244
|
+
"token_env": "GITHUB_TOKEN",
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
"environments": [
|
|
248
|
+
{
|
|
249
|
+
"name": "prod",
|
|
250
|
+
"url": "https://prod.example.com/build.json",
|
|
251
|
+
"source": {
|
|
252
|
+
"type": "json",
|
|
253
|
+
"fields": {"version": "$.version"},
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"name": "project-gitlab-cli",
|
|
260
|
+
"repository": {
|
|
261
|
+
"url": "https://gitlab.com/org/repo.git",
|
|
262
|
+
"branch": "main",
|
|
263
|
+
"provider": {"type": "gitlab-cli"},
|
|
264
|
+
},
|
|
265
|
+
"environments": [
|
|
266
|
+
{
|
|
267
|
+
"name": "dev",
|
|
268
|
+
"url": "https://dev.example.com/build.html",
|
|
269
|
+
"source": {
|
|
270
|
+
"type": "regex",
|
|
271
|
+
"pattern": r"(.*)\t(?P<commit_time>.*)\n(?P<version>.*)\t(?P<pipeline_time>.*)",
|
|
272
|
+
"fields": {"version": "version"},
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"name": "project-gitlab-api",
|
|
279
|
+
"repository": {
|
|
280
|
+
"url": "https://gitlab.com/org/repo.git",
|
|
281
|
+
"branch": "main",
|
|
282
|
+
"provider": {
|
|
283
|
+
"type": "gitlab-api",
|
|
284
|
+
"token_env": "GITLAB_API_TOKEN",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
"environments": [
|
|
288
|
+
{
|
|
289
|
+
"name": "prod",
|
|
290
|
+
"url": "https://prod.example.com/build.json",
|
|
291
|
+
"source": {
|
|
292
|
+
"type": "json",
|
|
293
|
+
"fields": {"version": "$.version"},
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
302
|
+
path.write_text(json.dumps(starter, indent=2) + "\n")
|
|
303
|
+
console.print(f"Created config: {path}")
|
|
304
|
+
console.print("Edit it with your projects, then run: release-status check")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# --- Helpers ---
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _load_config() -> AppConfig:
|
|
311
|
+
path = resolve_config_path(_state.config_path)
|
|
312
|
+
try:
|
|
313
|
+
return load_config(path)
|
|
314
|
+
except FileNotFoundError:
|
|
315
|
+
console.print(f"[red]Config file not found:[/red] {path}")
|
|
316
|
+
console.print(
|
|
317
|
+
f"Create one with: release-status init"
|
|
318
|
+
)
|
|
319
|
+
raise typer.Exit(1)
|
|
320
|
+
except ValidationError as e:
|
|
321
|
+
console.print(f"[red]Config validation error:[/red] {path}")
|
|
322
|
+
for err in e.errors():
|
|
323
|
+
loc = " → ".join(str(x) for x in err["loc"])
|
|
324
|
+
console.print(f" {loc}: {err['msg']}")
|
|
325
|
+
raise typer.Exit(1)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _find_project(config: AppConfig, name: str) -> ProjectConfig:
|
|
329
|
+
for p in config.projects:
|
|
330
|
+
if p.name.lower() == name.lower():
|
|
331
|
+
return p
|
|
332
|
+
available = ", ".join(p.name for p in config.projects)
|
|
333
|
+
console.print(f"[red]Project '{name}' not found.[/red] Available: {available}")
|
|
334
|
+
raise typer.Exit(1)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _make_cache(config: AppConfig) -> Cache:
|
|
338
|
+
cache = Cache(
|
|
339
|
+
cache_dir=config.cache_dir,
|
|
340
|
+
ttl=timedelta(minutes=config.cache_ttl_minutes),
|
|
341
|
+
)
|
|
342
|
+
cache.enabled = not _state.no_cache and config.cache_ttl_minutes > 0
|
|
343
|
+
return cache
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _apply_branch_override(proj: ProjectConfig) -> ProjectConfig:
|
|
347
|
+
# Uses Pydantic model_copy to create a modified config — all downstream code
|
|
348
|
+
# (providers, cache keys, views) reads repo.branch and gets the override automatically
|
|
349
|
+
if _state.branch:
|
|
350
|
+
repo = proj.repository.model_copy(update={"branch": _state.branch})
|
|
351
|
+
return proj.model_copy(update={"repository": repo})
|
|
352
|
+
return proj
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _since_days(config: AppConfig) -> int:
|
|
356
|
+
return _state.since_days if _state.since_days is not None else config.since_days
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _cache_key_commits(proj: ProjectConfig, since_days: int) -> str:
|
|
360
|
+
return (
|
|
361
|
+
f"commits:{proj.repository.provider.type}"
|
|
362
|
+
f":{proj.repository.url}:{proj.repository.branch}"
|
|
363
|
+
f":{since_days}"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _fetch_commits(proj: ProjectConfig, cache: Cache, since_days: int) -> list[Commit]:
|
|
368
|
+
key = _cache_key_commits(proj, since_days)
|
|
369
|
+
cached = cache.get(key)
|
|
370
|
+
if cached is not None:
|
|
371
|
+
return [
|
|
372
|
+
Commit(
|
|
373
|
+
sha=c["sha"],
|
|
374
|
+
short_sha=c["short_sha"],
|
|
375
|
+
message=c["message"],
|
|
376
|
+
author=c["author"],
|
|
377
|
+
date=datetime.fromisoformat(c["date"]),
|
|
378
|
+
)
|
|
379
|
+
for c in cached
|
|
380
|
+
]
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
provider = get_provider(proj.repository.provider)
|
|
384
|
+
commits = provider.fetch_commits(proj.repository, since_days)
|
|
385
|
+
except (ProviderError, ToolNotFoundError) as e:
|
|
386
|
+
console.print(f"[red]Error fetching commits:[/red] {e}")
|
|
387
|
+
raise typer.Exit(1)
|
|
388
|
+
|
|
389
|
+
cache.set(
|
|
390
|
+
key,
|
|
391
|
+
[
|
|
392
|
+
{
|
|
393
|
+
"sha": c.sha,
|
|
394
|
+
"short_sha": c.short_sha,
|
|
395
|
+
"message": c.message,
|
|
396
|
+
"author": c.author,
|
|
397
|
+
"date": c.date.isoformat(),
|
|
398
|
+
}
|
|
399
|
+
for c in commits
|
|
400
|
+
],
|
|
401
|
+
)
|
|
402
|
+
return commits
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _fetch_environments(
|
|
406
|
+
proj: ProjectConfig, cache: Cache
|
|
407
|
+
) -> list[EnvironmentStatus]:
|
|
408
|
+
results: list[EnvironmentStatus] = []
|
|
409
|
+
for env_config in proj.environments:
|
|
410
|
+
key = f"env:{env_config.url}"
|
|
411
|
+
cached = cache.get(key)
|
|
412
|
+
if cached is not None:
|
|
413
|
+
results.append(
|
|
414
|
+
EnvironmentStatus(
|
|
415
|
+
name=cached["name"],
|
|
416
|
+
fields=cached["fields"],
|
|
417
|
+
error=cached["error"],
|
|
418
|
+
url=cached["url"],
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
status = resolve_environment(env_config)
|
|
424
|
+
cache.set(
|
|
425
|
+
key,
|
|
426
|
+
{
|
|
427
|
+
"name": status.name,
|
|
428
|
+
"fields": status.fields,
|
|
429
|
+
"error": status.error,
|
|
430
|
+
"url": status.url,
|
|
431
|
+
},
|
|
432
|
+
)
|
|
433
|
+
results.append(status)
|
|
434
|
+
|
|
435
|
+
return results
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _fetch_missing_commits(
|
|
439
|
+
commit_list: list[Commit],
|
|
440
|
+
env_statuses: list[EnvironmentStatus],
|
|
441
|
+
proj: ProjectConfig,
|
|
442
|
+
cache: Cache,
|
|
443
|
+
) -> None:
|
|
444
|
+
"""Fetch individual commits for deployed SHAs not in the commit list.
|
|
445
|
+
|
|
446
|
+
This happens when a deployed version is older than since_days or on a
|
|
447
|
+
different branch. Fetched commits are marked with fetched=True and sorted
|
|
448
|
+
into the list by datetime.
|
|
449
|
+
"""
|
|
450
|
+
for env in env_statuses:
|
|
451
|
+
if not env.version or any(c.sha_matches(env.version) for c in commit_list):
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
key = f"commit:{env.version}"
|
|
455
|
+
cached = cache.get(key)
|
|
456
|
+
if cached is not None:
|
|
457
|
+
commit_list.append(
|
|
458
|
+
Commit(
|
|
459
|
+
sha=cached["sha"],
|
|
460
|
+
short_sha=cached["short_sha"],
|
|
461
|
+
message=cached["message"],
|
|
462
|
+
author=cached["author"],
|
|
463
|
+
date=datetime.fromisoformat(cached["date"]),
|
|
464
|
+
fetched=True,
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
continue
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
provider = get_provider(proj.repository.provider)
|
|
471
|
+
commit = provider.fetch_commit(proj.repository, env.version)
|
|
472
|
+
except (ProviderError, ToolNotFoundError):
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
cache.set(
|
|
476
|
+
key,
|
|
477
|
+
{
|
|
478
|
+
"sha": commit.sha,
|
|
479
|
+
"short_sha": commit.short_sha,
|
|
480
|
+
"message": commit.message,
|
|
481
|
+
"author": commit.author,
|
|
482
|
+
"date": commit.date.isoformat(),
|
|
483
|
+
},
|
|
484
|
+
)
|
|
485
|
+
commit_list.append(
|
|
486
|
+
Commit.from_raw(
|
|
487
|
+
sha=commit.sha,
|
|
488
|
+
message=commit.message,
|
|
489
|
+
author=commit.author,
|
|
490
|
+
date=commit.date,
|
|
491
|
+
fetched=True,
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
commit_list.sort(key=lambda c: c.date, reverse=True)
|