autopilot-code 2.1.0 → 2.2.0
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.
- package/.autopilot/autopilot.json +1 -1
- package/dist/cli.js +10 -3
- package/package.json +2 -2
- package/scripts/issue_runner/github_state.py +95 -12
- package/scripts/issue_status.py +367 -0
package/dist/cli.js
CHANGED
|
@@ -538,9 +538,16 @@ program
|
|
|
538
538
|
});
|
|
539
539
|
program
|
|
540
540
|
.command("status")
|
|
541
|
-
.description("Show
|
|
542
|
-
.
|
|
543
|
-
|
|
541
|
+
.description("Show active issues and their current state")
|
|
542
|
+
.option("--service", "Show systemd service status instead of issues")
|
|
543
|
+
.action((opts) => {
|
|
544
|
+
if (opts.service) {
|
|
545
|
+
statusSystemdService();
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
const statusScriptPath = node_path_1.default.join(repoRoot, "scripts", "issue_status.py");
|
|
549
|
+
run("python3", [statusScriptPath]);
|
|
550
|
+
}
|
|
544
551
|
});
|
|
545
552
|
program
|
|
546
553
|
.command("logs")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autopilot-code",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Repo-issue–driven autopilot runner",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@changesets/cli": "^2.29.8",
|
|
31
|
-
"@types/node": "^22.
|
|
31
|
+
"@types/node": "^22.19.7",
|
|
32
32
|
"typescript": "^5.7.2"
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -183,11 +183,63 @@ class GitHubStateManager:
|
|
|
183
183
|
except (json.JSONDecodeError, KeyError, TypeError):
|
|
184
184
|
return None
|
|
185
185
|
|
|
186
|
+
def _create_label(self, label_name: str) -> bool:
|
|
187
|
+
"""
|
|
188
|
+
Create a label in the GitHub repository.
|
|
189
|
+
|
|
190
|
+
Returns True if created or already exists, False on failure.
|
|
191
|
+
"""
|
|
192
|
+
# Define standard labels with colors and descriptions
|
|
193
|
+
label_defs = {
|
|
194
|
+
"autopilot:todo": {"color": "7057ff", "description": "Ready to be picked up by autopilot"},
|
|
195
|
+
"autopilot:in-progress": {"color": "0075ca", "description": "Claimed by autopilot"},
|
|
196
|
+
"autopilot:blocked": {"color": "d93f0b", "description": "Needs human input or missing heartbeat"},
|
|
197
|
+
"autopilot:done": {"color": "3fb950", "description": "Completed"},
|
|
198
|
+
"autopilot:backlog": {"color": "d4c5f9", "description": "Captured, not ready"},
|
|
199
|
+
"autopilot:planning": {"color": "5319e7", "description": "Creating implementation plan"},
|
|
200
|
+
"autopilot:implementing": {"color": "1f883d", "description": "Writing code"},
|
|
201
|
+
"autopilot:pr-created": {"color": "fbca04", "description": "Pull request created"},
|
|
202
|
+
"autopilot:waiting-checks": {"color": "0e8a16", "description": "Waiting for CI checks"},
|
|
203
|
+
"autopilot:fixing-checks": {"color": "cfd3d7", "description": "Fixing failing CI checks"},
|
|
204
|
+
"autopilot:merging": {"color": "bfd4f2", "description": "Merging pull request"},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if label_name not in label_defs:
|
|
208
|
+
print(f"Warning: Unknown label '{label_name}', skipping auto-creation", flush=True)
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
label_info = label_defs[label_name]
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
self._run_gh([
|
|
215
|
+
"api",
|
|
216
|
+
"--method",
|
|
217
|
+
"POST",
|
|
218
|
+
"-H",
|
|
219
|
+
"Accept: application/vnd.github+json",
|
|
220
|
+
f"repos/{self.repo}/labels",
|
|
221
|
+
"-f",
|
|
222
|
+
f"name={label_name}",
|
|
223
|
+
"-f",
|
|
224
|
+
f"color={label_info['color']}",
|
|
225
|
+
"-f",
|
|
226
|
+
f"description={label_info['description']}",
|
|
227
|
+
])
|
|
228
|
+
print(f"✅ Auto-created missing label: {label_name}", flush=True)
|
|
229
|
+
return True
|
|
230
|
+
except RuntimeError as e:
|
|
231
|
+
# Label might already exist, which is fine
|
|
232
|
+
if "already exists" in str(e).lower() or "Label already exists" in str(e):
|
|
233
|
+
return True
|
|
234
|
+
print(f"⚠️ Failed to auto-create label '{label_name}': {e}", flush=True)
|
|
235
|
+
return False
|
|
236
|
+
|
|
186
237
|
def _set_step_label(self, issue_number: int, step: IssueStep) -> None:
|
|
187
238
|
"""
|
|
188
|
-
Update
|
|
239
|
+
Update autopilot step label.
|
|
189
240
|
|
|
190
241
|
Removes any existing autopilot:* step labels and adds the new one.
|
|
242
|
+
Auto-creates missing labels if needed.
|
|
191
243
|
"""
|
|
192
244
|
result = self._run_gh(
|
|
193
245
|
[
|
|
@@ -220,17 +272,48 @@ class GitHubStateManager:
|
|
|
220
272
|
)
|
|
221
273
|
|
|
222
274
|
if step in STEP_LABELS:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
275
|
+
new_label = STEP_LABELS[step]
|
|
276
|
+
try:
|
|
277
|
+
self._run_gh(
|
|
278
|
+
[
|
|
279
|
+
"issue",
|
|
280
|
+
"edit",
|
|
281
|
+
str(issue_number),
|
|
282
|
+
"--repo",
|
|
283
|
+
self.repo,
|
|
284
|
+
"--add-label",
|
|
285
|
+
new_label,
|
|
286
|
+
]
|
|
287
|
+
)
|
|
288
|
+
except RuntimeError as e:
|
|
289
|
+
# Check if it's a "label not found" error
|
|
290
|
+
error_msg = str(e)
|
|
291
|
+
if "not found" in error_msg.lower() or "label" in error_msg.lower():
|
|
292
|
+
print(f"⚠️ Label '{new_label}' not found in repo '{self.repo}'", flush=True)
|
|
293
|
+
print(f"🔧 Attempting to auto-create label...", flush=True)
|
|
294
|
+
|
|
295
|
+
if self._create_label(new_label):
|
|
296
|
+
# Retry adding the label
|
|
297
|
+
self._run_gh(
|
|
298
|
+
[
|
|
299
|
+
"issue",
|
|
300
|
+
"edit",
|
|
301
|
+
str(issue_number),
|
|
302
|
+
"--repo",
|
|
303
|
+
self.repo,
|
|
304
|
+
"--add-label",
|
|
305
|
+
new_label,
|
|
306
|
+
]
|
|
307
|
+
)
|
|
308
|
+
else:
|
|
309
|
+
# Auto-creation failed, provide helpful error
|
|
310
|
+
raise RuntimeError(
|
|
311
|
+
f"Required label '{new_label}' does not exist in repository '{self.repo}'. "
|
|
312
|
+
f"Please run: autopilot setup-labels --repo {self.repo}"
|
|
313
|
+
) from e
|
|
314
|
+
else:
|
|
315
|
+
# Some other error, re-raise
|
|
316
|
+
raise
|
|
234
317
|
|
|
235
318
|
def _run_gh(self, args: list[str]) -> str:
|
|
236
319
|
"""Run a gh CLI command and return stdout."""
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Show autopilot status for active issues."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
STATE_DIR = ".autopilot"
|
|
13
|
+
STATE_FILE = "state.json"
|
|
14
|
+
DEFAULT_MAX_DEPTH = 3
|
|
15
|
+
IGNORED_DIRS = {
|
|
16
|
+
"node_modules",
|
|
17
|
+
".venv",
|
|
18
|
+
"venv",
|
|
19
|
+
".env",
|
|
20
|
+
"env",
|
|
21
|
+
".git",
|
|
22
|
+
".idea",
|
|
23
|
+
".vscode",
|
|
24
|
+
"dist",
|
|
25
|
+
"build",
|
|
26
|
+
"target",
|
|
27
|
+
"bin",
|
|
28
|
+
"obj",
|
|
29
|
+
"__pycache__",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def sh(cmd: list[str], cwd: Path | None = None, check: bool = True) -> str:
|
|
34
|
+
p = subprocess.run(
|
|
35
|
+
cmd,
|
|
36
|
+
cwd=str(cwd) if cwd else None,
|
|
37
|
+
text=True,
|
|
38
|
+
stdout=subprocess.PIPE,
|
|
39
|
+
stderr=subprocess.STDOUT,
|
|
40
|
+
)
|
|
41
|
+
if check and p.returncode != 0:
|
|
42
|
+
raise RuntimeError(
|
|
43
|
+
f"command failed ({p.returncode}): {' '.join(cmd)}\n{p.stdout}"
|
|
44
|
+
)
|
|
45
|
+
return p.stdout
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_git_repo(dir_path: Path) -> bool:
|
|
49
|
+
return (dir_path / ".git").exists()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_repo_name(dir: Path) -> Optional[str]:
|
|
53
|
+
try:
|
|
54
|
+
res = subprocess.run(
|
|
55
|
+
["git", "remote", "get-url", "origin"],
|
|
56
|
+
cwd=str(dir),
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
check=True,
|
|
60
|
+
)
|
|
61
|
+
url = res.stdout.strip()
|
|
62
|
+
|
|
63
|
+
if "@github.com:" in url:
|
|
64
|
+
return url.split("@github.com:")[1].replace(".git", "")
|
|
65
|
+
|
|
66
|
+
if "github.com/" in url:
|
|
67
|
+
return url.split("github.com/")[1].replace(".git", "")
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
except Exception:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_config(repo_root: Path) -> Optional[dict[str, Any]]:
|
|
75
|
+
cfg_path = repo_root / ".autopilot" / "autopilot.json"
|
|
76
|
+
if not cfg_path.exists():
|
|
77
|
+
return None
|
|
78
|
+
data = json.loads(cfg_path.read_text(encoding="utf8"))
|
|
79
|
+
if not data.get("enabled", False):
|
|
80
|
+
return None
|
|
81
|
+
return data
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def discover_repos(
|
|
85
|
+
root: Path, max_depth: int = DEFAULT_MAX_DEPTH
|
|
86
|
+
) -> list[dict[str, Any]]:
|
|
87
|
+
out: list[dict[str, Any]] = []
|
|
88
|
+
_discover_repos_recursive(root, 0, max_depth, out)
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _discover_repos_recursive(
|
|
93
|
+
current_dir: Path, current_depth: int, max_depth: int, out: list[dict[str, Any]]
|
|
94
|
+
) -> None:
|
|
95
|
+
if current_depth > max_depth:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if (current_dir / ".git").exists():
|
|
99
|
+
if (current_dir / ".autopilot" / "autopilot.json").exists():
|
|
100
|
+
try:
|
|
101
|
+
cfg = load_config(current_dir)
|
|
102
|
+
if cfg:
|
|
103
|
+
out.append(
|
|
104
|
+
{"root": current_dir, "repo": cfg["repo"], "config": cfg}
|
|
105
|
+
)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"Error loading config in {current_dir}: {e}", file=sys.stderr)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
for child in sorted(current_dir.iterdir(), key=lambda p: p.name.lower()):
|
|
112
|
+
if not child.is_dir():
|
|
113
|
+
continue
|
|
114
|
+
if child.name in IGNORED_DIRS or child.name.startswith("."):
|
|
115
|
+
continue
|
|
116
|
+
_discover_repos_recursive(child, current_depth + 1, max_depth, out)
|
|
117
|
+
except (PermissionError, OSError):
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_state_comment(repo: str, issue_number: int) -> Optional[dict[str, Any]]:
|
|
122
|
+
try:
|
|
123
|
+
result = sh(
|
|
124
|
+
[
|
|
125
|
+
"gh",
|
|
126
|
+
"issue",
|
|
127
|
+
"view",
|
|
128
|
+
str(issue_number),
|
|
129
|
+
"--repo",
|
|
130
|
+
repo,
|
|
131
|
+
"--json",
|
|
132
|
+
"comments",
|
|
133
|
+
"--jq",
|
|
134
|
+
'.comments[] | select(.body | contains("<!-- autopilot-state"))',
|
|
135
|
+
]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if not result.strip():
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
comments = json.loads("[" + result + "]")
|
|
142
|
+
for comment in comments:
|
|
143
|
+
body = comment.get("body", "")
|
|
144
|
+
if "<!-- autopilot-state" in body:
|
|
145
|
+
import re
|
|
146
|
+
|
|
147
|
+
match = re.search(
|
|
148
|
+
r"<!-- autopilot-state\s*\n({.*?})\s*\n-->", body, re.DOTALL
|
|
149
|
+
)
|
|
150
|
+
if match:
|
|
151
|
+
return json.loads(match.group(1))
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_pr_info(repo: str, branch: str) -> Optional[dict[str, Any]]:
|
|
158
|
+
try:
|
|
159
|
+
cmd = [
|
|
160
|
+
"gh",
|
|
161
|
+
"pr",
|
|
162
|
+
"list",
|
|
163
|
+
"--repo",
|
|
164
|
+
repo,
|
|
165
|
+
"--head",
|
|
166
|
+
branch,
|
|
167
|
+
"--json",
|
|
168
|
+
"number,state,mergeable,mergeStateStatus,statusCheckRollup",
|
|
169
|
+
"--limit",
|
|
170
|
+
"1",
|
|
171
|
+
]
|
|
172
|
+
raw = sh(cmd, check=False)
|
|
173
|
+
prs = json.loads(raw)
|
|
174
|
+
if prs:
|
|
175
|
+
return prs[0]
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def format_duration(seconds: int) -> str:
|
|
182
|
+
hours = seconds // 3600
|
|
183
|
+
minutes = (seconds % 3600) // 60
|
|
184
|
+
secs = seconds % 60
|
|
185
|
+
|
|
186
|
+
if hours > 0:
|
|
187
|
+
return f"{hours}h {minutes}m {secs}s"
|
|
188
|
+
if minutes > 0:
|
|
189
|
+
return f"{minutes}m {secs}s"
|
|
190
|
+
return f"{secs}s"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_step_from_label(labels: list[str]) -> Optional[str]:
|
|
194
|
+
for label in labels:
|
|
195
|
+
if label.startswith("autopilot:") and label != "autopilot:in-progress":
|
|
196
|
+
return label
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def display_status(repo_statuses: dict[str, list[dict[str, Any]]]) -> None:
|
|
201
|
+
if not repo_statuses:
|
|
202
|
+
print("No active issues found.")
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
for repo, issues in repo_statuses.items():
|
|
206
|
+
print(f"\nStatus for {repo}")
|
|
207
|
+
|
|
208
|
+
if not issues:
|
|
209
|
+
print(" No active issues")
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
for issue in issues:
|
|
213
|
+
print(f"\n#{issue['number']} - {issue['title']}")
|
|
214
|
+
print(f" State: {issue.get('state', 'unknown')}")
|
|
215
|
+
|
|
216
|
+
step_label = issue.get("step_label")
|
|
217
|
+
if step_label:
|
|
218
|
+
step_name = step_label.replace("autopilot:", "")
|
|
219
|
+
step_name = step_name.replace("-", " ").title()
|
|
220
|
+
print(f" Step: {step_name}")
|
|
221
|
+
|
|
222
|
+
duration = issue.get("duration", 0)
|
|
223
|
+
if duration > 0:
|
|
224
|
+
print(f" Duration: {format_duration(duration)}")
|
|
225
|
+
|
|
226
|
+
branch = issue.get("branch")
|
|
227
|
+
if branch:
|
|
228
|
+
print(f" Branch: {branch}")
|
|
229
|
+
|
|
230
|
+
pr_info = issue.get("pr_info")
|
|
231
|
+
if pr_info:
|
|
232
|
+
pr_num = pr_info.get("number")
|
|
233
|
+
state = pr_info.get("state", "unknown")
|
|
234
|
+
mergeable = pr_info.get("mergeable", "unknown")
|
|
235
|
+
|
|
236
|
+
if mergeable == "CONFLICTING":
|
|
237
|
+
mergeable = "conflicts"
|
|
238
|
+
elif mergeable == "MERGEABLE":
|
|
239
|
+
mergeable = "ready"
|
|
240
|
+
|
|
241
|
+
checks = pr_info.get("statusCheckRollup", [])
|
|
242
|
+
if isinstance(checks, dict):
|
|
243
|
+
check_status = checks.get("state", "unknown")
|
|
244
|
+
elif checks:
|
|
245
|
+
passing = sum(1 for c in checks if c.get("conclusion") == "SUCCESS")
|
|
246
|
+
failing = sum(1 for c in checks if c.get("conclusion") == "FAILURE")
|
|
247
|
+
pending = sum(
|
|
248
|
+
1
|
|
249
|
+
for c in checks
|
|
250
|
+
if c.get("conclusion") in ("PENDING", "QUEUED", None)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if failing > 0:
|
|
254
|
+
check_status = f"{passing} passing, {failing} failing"
|
|
255
|
+
elif pending > 0:
|
|
256
|
+
check_status = f"{passing} passing, {pending} pending"
|
|
257
|
+
else:
|
|
258
|
+
check_status = f"{passing} passing"
|
|
259
|
+
else:
|
|
260
|
+
check_status = "none"
|
|
261
|
+
|
|
262
|
+
print(
|
|
263
|
+
f" PR: #{pr_num} (state: {state}, mergeable: {mergeable}, checks: {check_status})"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def main() -> int:
|
|
268
|
+
ap = argparse.ArgumentParser()
|
|
269
|
+
ap.add_argument("--root", nargs="+", default=None, help="Root folders to scan")
|
|
270
|
+
args = ap.parse_args()
|
|
271
|
+
|
|
272
|
+
cwd = Path.cwd()
|
|
273
|
+
repo_configs = []
|
|
274
|
+
|
|
275
|
+
if is_git_repo(cwd):
|
|
276
|
+
repo_name = get_repo_name(cwd)
|
|
277
|
+
if repo_name:
|
|
278
|
+
cfg = load_config(cwd)
|
|
279
|
+
if cfg:
|
|
280
|
+
repo_configs.append({"root": cwd, "repo": repo_name, "config": cfg})
|
|
281
|
+
print(f"Showing status for current repo: {repo_name}")
|
|
282
|
+
else:
|
|
283
|
+
root_paths = args.root if args.root else [Path.home()]
|
|
284
|
+
for root_str in root_paths:
|
|
285
|
+
root = Path(root_str).resolve()
|
|
286
|
+
if root.exists() and root.is_dir():
|
|
287
|
+
repo_configs.extend(discover_repos(root))
|
|
288
|
+
|
|
289
|
+
if not repo_configs:
|
|
290
|
+
print("No autopilot-enabled repos found.")
|
|
291
|
+
return 0
|
|
292
|
+
|
|
293
|
+
repo_statuses: dict[str, list[dict[str, Any]]] = {}
|
|
294
|
+
now = int(time.time())
|
|
295
|
+
|
|
296
|
+
for repo_cfg in repo_configs:
|
|
297
|
+
repo = repo_cfg["repo"]
|
|
298
|
+
repo_root = repo_cfg["root"]
|
|
299
|
+
config = repo_cfg.get("config", {})
|
|
300
|
+
|
|
301
|
+
labels_config = config.get("issueLabels", {})
|
|
302
|
+
in_progress_label = labels_config.get("inProgress", "autopilot:in-progress")
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
cmd = [
|
|
306
|
+
"gh",
|
|
307
|
+
"issue",
|
|
308
|
+
"list",
|
|
309
|
+
"--repo",
|
|
310
|
+
repo,
|
|
311
|
+
"--limit",
|
|
312
|
+
"50",
|
|
313
|
+
"--search",
|
|
314
|
+
f"is:open label:{in_progress_label}",
|
|
315
|
+
"--json",
|
|
316
|
+
"number,title,labels,updatedAt,createdAt",
|
|
317
|
+
]
|
|
318
|
+
raw = sh(cmd)
|
|
319
|
+
issues = json.loads(raw)
|
|
320
|
+
|
|
321
|
+
repo_issues = []
|
|
322
|
+
for issue in issues:
|
|
323
|
+
issue_num = issue["number"]
|
|
324
|
+
|
|
325
|
+
labels = [l["name"] for l in issue.get("labels", [])]
|
|
326
|
+
step_label = get_step_from_label(labels)
|
|
327
|
+
|
|
328
|
+
state_comment = get_state_comment(repo, issue_num)
|
|
329
|
+
branch = None
|
|
330
|
+
if state_comment:
|
|
331
|
+
branch = state_comment.get("branch")
|
|
332
|
+
|
|
333
|
+
pr_info = None
|
|
334
|
+
if branch:
|
|
335
|
+
pr_info = get_pr_info(repo, branch)
|
|
336
|
+
|
|
337
|
+
updated_at = issue.get("updatedAt", "")
|
|
338
|
+
if updated_at:
|
|
339
|
+
from datetime import datetime
|
|
340
|
+
|
|
341
|
+
dt = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
|
|
342
|
+
duration = now - int(dt.timestamp())
|
|
343
|
+
else:
|
|
344
|
+
duration = 0
|
|
345
|
+
|
|
346
|
+
repo_issues.append(
|
|
347
|
+
{
|
|
348
|
+
"number": issue_num,
|
|
349
|
+
"title": issue["title"],
|
|
350
|
+
"state": step_label or in_progress_label,
|
|
351
|
+
"step_label": step_label,
|
|
352
|
+
"duration": duration,
|
|
353
|
+
"branch": branch,
|
|
354
|
+
"pr_info": pr_info,
|
|
355
|
+
}
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
repo_statuses[repo] = repo_issues
|
|
359
|
+
except Exception as e:
|
|
360
|
+
print(f"Error fetching status for {repo}: {e}", file=sys.stderr)
|
|
361
|
+
|
|
362
|
+
display_status(repo_statuses)
|
|
363
|
+
return 0
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
if __name__ == "__main__":
|
|
367
|
+
raise SystemExit(main())
|