Skip to content

Commit 65998b3

Browse files
authored
Merge branch 'develop' into web/task/display-name-validation
2 parents 79e6af4 + f1b0487 commit 65998b3

681 files changed

Lines changed: 18996 additions & 6597 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/tools/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__/

.claude/tools/ci_job_log.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Fetch and display the log output of a GitLab CI job."""
2+
3+
import argparse
4+
import re
5+
import sys
6+
7+
import httpx
8+
9+
10+
def clean_log(raw: str) -> str:
11+
"""Strip ANSI escape codes, GitLab section/timestamp markers, and carriage returns."""
12+
# Strip ANSI escape codes
13+
text = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", raw)
14+
15+
# Strip GitLab section markers
16+
text = re.sub(r"section_start:\d+:[a-zA-Z_\d]+\r?\n?", "", text)
17+
text = re.sub(r"section_end:\d+:[a-zA-Z_\d]+\r?\n?", "", text)
18+
19+
# Strip GitLab raw log timestamp+stream prefixes (e.g. "2026-02-16T06:23:12.723565Z 00O ")
20+
# These can appear with a space or + after the stream marker, and can be concatenated
21+
text = re.sub(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z \d{2}[OE][+ ]?", "", text)
22+
23+
# Strip carriage returns
24+
text = text.replace("\r", "")
25+
26+
# Remove leading blank lines
27+
text = text.lstrip("\n")
28+
29+
# Remove trailing + characters on otherwise empty lines (section marker artifacts)
30+
text = re.sub(r"^\+$", "", text, flags=re.MULTILINE)
31+
32+
# Collapse multiple consecutive blank lines into one
33+
text = re.sub(r"\n{3,}", "\n\n", text)
34+
35+
return text
36+
37+
38+
def main():
39+
parser = argparse.ArgumentParser(description="Fetch GitLab CI job log")
40+
parser.add_argument("job_id", help="GitLab job ID (from ci-status output)")
41+
parser.add_argument("--full", action="store_true", help="Show full log output (default: last 200 lines)")
42+
args = parser.parse_args()
43+
44+
url = f"https://gitlab.com/couchers/couchers/-/jobs/{args.job_id}/raw"
45+
46+
with httpx.Client(timeout=60, follow_redirects=True) as client:
47+
resp = client.get(url)
48+
if resp.status_code != 200:
49+
print(f"Failed to fetch job log (HTTP {resp.status_code})", file=sys.stderr)
50+
sys.exit(1)
51+
52+
log = clean_log(resp.text)
53+
lines = log.splitlines()
54+
total = len(lines)
55+
56+
if args.full or total <= 200:
57+
print(log)
58+
else:
59+
skipped = total - 200
60+
print(f"... ({skipped} lines skipped, use --full to see all {total} lines) ...")
61+
print()
62+
print("\n".join(lines[-200:]))
63+
64+
65+
if __name__ == "__main__":
66+
main()

.claude/tools/ci_status.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Look up GitLab CI pipeline status for a given PR, branch, or commit."""
2+
3+
import argparse
4+
import json
5+
import subprocess
6+
import sys
7+
8+
import httpx
9+
10+
GITLAB_PROJECT = "couchers%2Fcouchers"
11+
GITLAB_API = f"https://gitlab.com/api/v4/projects/{GITLAB_PROJECT}"
12+
13+
14+
def get_pr_info(pr_number: str) -> tuple[str, str]:
15+
"""Get SHA and branch name from a GitHub PR number."""
16+
result = subprocess.run(
17+
["gh", "pr", "view", pr_number, "--json", "headRefOid,headRefName"],
18+
capture_output=True,
19+
text=True,
20+
check=True,
21+
)
22+
data = json.loads(result.stdout)
23+
return data["headRefOid"], data["headRefName"]
24+
25+
26+
def get_branch_sha(client: httpx.Client, branch: str) -> str:
27+
"""Get the latest pipeline SHA for a branch from GitLab."""
28+
resp = client.get(
29+
f"{GITLAB_API}/pipelines",
30+
params={"ref": branch, "per_page": 1, "order_by": "updated_at", "sort": "desc"},
31+
)
32+
resp.raise_for_status()
33+
pipelines = resp.json()
34+
if not pipelines:
35+
print(f"No pipelines found for branch: {branch}", file=sys.stderr)
36+
sys.exit(1)
37+
return pipelines[0]["sha"]
38+
39+
40+
def find_pipeline(client: httpx.Client, sha: str) -> dict:
41+
"""Find the best pipeline for a given SHA."""
42+
resp = client.get(f"{GITLAB_API}/pipelines", params={"sha": sha, "per_page": 5})
43+
resp.raise_for_status()
44+
pipelines = resp.json()
45+
46+
if not pipelines:
47+
print(f"No pipelines found for SHA: {sha}", file=sys.stderr)
48+
sys.exit(1)
49+
50+
# Prefer external_pull_request_event source
51+
for p in pipelines:
52+
if p.get("source") == "external_pull_request_event":
53+
return p
54+
55+
return pipelines[0]
56+
57+
58+
def get_jobs(client: httpx.Client, pipeline_id: int) -> list[dict]:
59+
"""Fetch all jobs for a pipeline."""
60+
resp = client.get(f"{GITLAB_API}/pipelines/{pipeline_id}/jobs", params={"per_page": 100})
61+
resp.raise_for_status()
62+
return resp.json()
63+
64+
65+
def format_duration(seconds: float | None) -> str:
66+
if seconds is None:
67+
return "-"
68+
return f"{seconds:.0f}s"
69+
70+
71+
def main():
72+
parser = argparse.ArgumentParser(description="Look up GitLab CI pipeline status")
73+
group = parser.add_mutually_exclusive_group(required=True)
74+
group.add_argument("--pr", help="GitHub PR number")
75+
group.add_argument("--branch", help="Git branch name")
76+
group.add_argument("--sha", help="Commit SHA")
77+
args = parser.parse_args()
78+
79+
sha = ""
80+
branch = ""
81+
pr = ""
82+
83+
with httpx.Client(timeout=30) as client:
84+
if args.pr:
85+
pr = args.pr
86+
sha, branch = get_pr_info(pr)
87+
elif args.branch:
88+
branch = args.branch
89+
sha = get_branch_sha(client, branch)
90+
elif args.sha:
91+
# Resolve short SHAs to full 40-char via GitLab API (works in shallow clones)
92+
if len(args.sha) < 40:
93+
resp = client.get(f"{GITLAB_API}/repository/commits/{args.sha}")
94+
resp.raise_for_status()
95+
sha = resp.json()["id"]
96+
else:
97+
sha = args.sha
98+
99+
pipeline = find_pipeline(client, sha)
100+
pipeline_id = pipeline["id"]
101+
pipeline_status = pipeline["status"]
102+
103+
if not branch:
104+
branch = pipeline["ref"]
105+
106+
jobs = get_jobs(client, pipeline_id)
107+
108+
# Print summary header
109+
print(f"Pipeline: #{pipeline_id} ({pipeline_status})")
110+
print(f"Branch: {branch}")
111+
print(f"Commit: {sha}")
112+
if pr:
113+
print(f"PR: #{pr}")
114+
print(f"URL: https://gitlab.com/couchers/couchers/-/pipelines/{pipeline_id}")
115+
print()
116+
117+
# Sort jobs by stage then name
118+
jobs.sort(key=lambda j: (j.get("stage", ""), j.get("name", "")))
119+
120+
# Print jobs table
121+
print("Jobs:")
122+
print(f"{'STAGE':<16} {'JOB':<30} {'STATUS':<10} {'DURATION':<10} ID")
123+
124+
for job in jobs:
125+
stage = job.get("stage", "")
126+
name = job.get("name", "")
127+
status = job.get("status", "")
128+
duration = format_duration(job.get("duration"))
129+
job_id = job.get("id", "")
130+
131+
status_display = status.upper() if status == "failed" else status
132+
133+
print(f"{stage:<16} {name:<30} {status_display:<10} {duration:<10} {job_id}")
134+
135+
if status == "failed":
136+
reason = job.get("failure_reason", "")
137+
if reason:
138+
print(f"{'':16} ^ failure reason: {reason}")
139+
140+
141+
if __name__ == "__main__":
142+
main()

.claude/tools/pyproject.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[project]
2+
name = "claude-tools"
3+
version = "0.0.1"
4+
requires-python = ">=3.11"
5+
dependencies = [
6+
"httpx",
7+
]
8+
9+
[project.scripts]
10+
ci-status = "ci_status:main"
11+
ci-job-log = "ci_job_log:main"
12+
13+
[tool.uv]
14+
package = true
15+
16+
[tool.hatch.build.targets.wheel]
17+
packages = ["."]
18+
19+
[build-system]
20+
requires = ["hatchling"]
21+
build-backend = "hatchling.build"

.claude/tools/uv.lock

Lines changed: 91 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/dependabot.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ updates:
2525
- "1.topic frontend"
2626
- "dependencies"
2727

28+
- package-ecosystem: "npm"
29+
directory: "/app/mobile"
30+
schedule:
31+
interval: "weekly"
32+
labels:
33+
- "mobile app"
34+
- "dependencies"
35+
2836
- package-ecosystem: "docker"
2937
directories:
3038
- "/app/backend"

0 commit comments

Comments
 (0)