diff --git a/config.yaml b/config.yaml index 764fbc2..8672124 100644 --- a/config.yaml +++ b/config.yaml @@ -4,6 +4,8 @@ gitflic: # Базовый URL GitFlic (облачный или self-hosted) + # Для облачной версии: https://api.gitflic.ru + # Для self-hosted: http://:/rest-api base_url: "https://gitflic.ru" # API-токен пользователя (read-only, scope: api, read_repository) api_token: "" # required diff --git a/main.py b/main.py index 3810a66..0f05b49 100644 --- a/main.py +++ b/main.py @@ -13,7 +13,7 @@ import time from copy import deepcopy from datetime import datetime, timezone from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple import requests import yaml @@ -100,21 +100,21 @@ def save_state(state: dict, path: str) -> None: logger.error("Ошибка сохранения состояния в %s: %s", path, exc) -def make_release_key(project_id: int, release_id: int, +def make_release_key(project_id: str, release_id: str, tag: str, version: str) -> str: """Сформировать ключ для идентификации релиза.""" return f"{project_id}:{release_id}:{tag}:{version}" -def is_release_sent(state: dict, project_id: int, release_id: int, +def is_release_sent(state: dict, project_id: str, release_id: str, tag: str, version: str) -> bool: """Проверить, был ли релиз уже отправлен.""" key = make_release_key(project_id, release_id, tag, version) return key in state.get("sent_releases", {}) -def mark_release_sent(state: dict, project_id: int, project_name: str, - release_id: int, tag: str, version: str, +def mark_release_sent(state: dict, project_id: str, project_name: str, + release_id: str, tag: str, version: str, files: List[Dict[str, Any]]) -> None: """Пометить релиз как отправленный.""" key = make_release_key(project_id, release_id, tag, version) @@ -153,7 +153,7 @@ class GitFlicClient: self._session = requests.Session() self._session.headers.update({ - "Authorization": f"Bearer {self.api_token}", + "Authorization": f"token {self.api_token}", "Accept": "application/json", "User-Agent": "Sandbox-GitFlic-PoC/1.0", }) @@ -176,43 +176,48 @@ class GitFlicClient: logger.error("Ошибка запроса %s %s: %s", method, url, exc) raise - def _paginated_get(self, path: str, params: Optional[dict] = None) -> List[dict]: - """Получить все страницы пагинированного GET-запроса.""" + def _paginated_get(self, path: str, params: Optional[dict] = None, + list_key: Optional[str] = None) -> List[dict]: + """ + Получить все страницы пагинированного GET-запроса. + GitFlic использует: size (не per_page), page начиная с 0, + и ответ в формате {"_embedded": {"": [...]}, "page": {...}}. + """ if params is None: params = {} - params.setdefault("per_page", self.per_page) + params.setdefault("size", self.per_page) - page = 1 + page = 0 all_items: List[dict] = [] while True: params["page"] = page resp = self._request("GET", path, params=params) - items = resp.json() - if not isinstance(items, list): - all_items = items - break + data = resp.json() + + if list_key: + items = data.get("_embedded", {}).get(list_key, []) + else: + embedded = data.get("_embedded", {}) + items = next(iter(embedded.values())) if embedded else [] all_items.extend(items) - total_pages = resp.headers.get("X-Total-Pages") - if total_pages is not None: - if page >= int(total_pages): - break - else: - if not items: - break + page_info = data.get("page", {}) + total_pages = page_info.get("totalPages", 0) + if page >= total_pages - 1: + break page += 1 return all_items - def get_projects(self) -> List[dict]: - """GET /api/v1/projects.""" - return self._paginated_get("/api/v1/projects", {"membership": True}) + def get_my_projects(self) -> List[dict]: + """GET /project/my — собственные проекты пользователя.""" + return self._paginated_get("/project/my", list_key="projectList") - def get_company_projects(self, company_id: int) -> List[dict]: - """GET /api/v1/companies/{id}/projects.""" - return self._paginated_get(f"/api/v1/companies/{company_id}/projects") + def get_shared_projects(self) -> List[dict]: + """GET /project/shared — проекты с участием пользователя.""" + return self._paginated_get("/project/shared", list_key="projectList") def resolve_projects(self, cfg: dict) -> List[dict]: """Определить список проектов для сканирования.""" @@ -234,7 +239,7 @@ class GitFlicClient: if proj: projects.append(proj) else: - logger.warning("Проект не найден по ID: %d", pid) + logger.warning("Проект не найден по ID: %s", pid) return projects if source == "company": @@ -251,45 +256,47 @@ class GitFlicClient: return [] return self.get_team_projects(tid) - return self.get_projects() + return self.get_shared_projects() - def get_project(self, project_id: int) -> Optional[dict]: + def get_project(self, project_id: str) -> Optional[dict]: + """GET /project/{projectUuid}.""" try: - resp = self._request("GET", f"/api/v1/projects/{project_id}") + resp = self._request("GET", f"/project/{project_id}") return resp.json() except requests.RequestException: return None def get_project_by_path(self, path: str) -> Optional[dict]: + """ + GET /project/{ownerAlias}/{projectAlias}. + path — строка вида "owner/project-name". + """ try: - resp = self._request("GET", f"/api/v1/projects/{path}") + resp = self._request("GET", f"/project/{path}") return resp.json() except requests.RequestException: return None - def get_releases(self, project_id: int) -> List[dict]: - """GET /api/v1/projects/{id}/releases.""" + def get_releases(self, owner_alias: str, project_alias: str) -> List[dict]: + """GET /project/{ownerAlias}/{projectAlias}/release.""" return self._paginated_get( - f"/api/v1/projects/{project_id}/releases", - {"per_page": self.per_page}, + f"/project/{owner_alias}/{project_alias}/release", + list_key="releaseTagModelList", ) - def get_release_assets(self, project_id: int, release_id: int) -> List[dict]: - """Получить assets релиза.""" - try: - resp = self._request( - "GET", - f"/api/v1/projects/{project_id}/releases/{release_id}/assets", - ) - items = resp.json() - if isinstance(items, list): - return items - if isinstance(items, dict): - return items.get("assets", items.get("links", [])) - return [] - except requests.RequestException: - logger.debug("Assets не получены: %s/%s", project_id, release_id) - return [] + def get_company_projects(self, company_id: str) -> List[dict]: + """GET /company/{id}/project.""" + return self._paginated_get( + f"/company/{company_id}/project", + list_key="projectList", + ) + + def get_team_projects(self, team_id: str) -> List[dict]: + """GET /team/{id}/project.""" + return self._paginated_get( + f"/team/{team_id}/project", + list_key="projectList", + ) def download_file(self, url: str, local_path: Path) -> Path: """Скачать файл по ссылке.""" @@ -298,7 +305,7 @@ class GitFlicClient: headers = {} if url.startswith(self.base_url): - headers["Authorization"] = f"Bearer {self.api_token}" + headers["Authorization"] = f"token {self.api_token}" logger.info("Скачивание: %s -> %s", url, local_path) resp = self._session.get(url, headers=headers, stream=True, @@ -314,19 +321,6 @@ class GitFlicClient: local_path, local_path.stat().st_size) return local_path - def get_release_source_archive_url(self, project_id: int, - tag_name: str) -> str: - """URL архива исходного кода по тегу релиза.""" - return (f"{self.base_url}/api/v1/projects/{project_id}" - f"/repository/archive.zip?ref={tag_name}") - - def get_release_asset_download_url(self, project_id: int, - release_id: int, - asset_filename: str) -> str: - """URL для скачивания прикреплённого файла (asset).""" - return (f"{self.base_url}/api/v1/projects/{project_id}" - f"/releases/{release_id}/downloads/{asset_filename}") - # ───────────────────────────────────────────────────────── # Клиент PT Sandbox @@ -422,7 +416,7 @@ def scan_new_releases(gf: GitFlicClient, sb: PTSandboxClient, 1. Получить проекты 2. Для каждого проекта релизы 3. Отфильтровать отправленные - 4. Скачать файлы (архив + assets) + 4. Скачать файлы (attachmentFiles из релиза) 5. Отправить в Sandbox 6. Обновить состояние """ @@ -437,25 +431,32 @@ def scan_new_releases(gf: GitFlicClient, sb: PTSandboxClient, for project in projects: pid = project.get("id") - pname = project.get("path_with_namespace") or project.get("name", str(pid)) - logger.info("Обработка проекта: %s (id=%s)", pname, pid) + owner_alias = project.get("owner", {}).get("alias", "") + project_alias = project.get("alias", "") + pname = f"{owner_alias}/{project_alias}" + logger.info("Обработка проекта: %s (uuid=%s)", pname, pid) + + if not owner_alias or not project_alias: + logger.warning("Пропускаем проект %s: нет owner/project alias", pid) + continue try: - releases = gf.get_releases(pid) + releases = gf.get_releases(owner_alias, project_alias) except requests.RequestException: logger.error("Пропускаем проект %s - ошибка получения релизов", pname) continue if not releases: + logger.info("Нет релизов в проекте %s", pname) continue - releases.sort(key=lambda r: r.get("created_at", ""), reverse=True) + releases.sort(key=lambda r: r.get("createdAt", ""), reverse=True) if scan_mode == "latest": rel = releases[0] rel_id = rel.get("id") - rel_tag = rel.get("tag_name", "") - rel_version = rel.get("name", "") + rel_tag = rel.get("tagName", "") + rel_version = rel.get("title", "") if is_release_sent(state, pid, rel_id, rel_tag, rel_version): logger.info("Последний релиз уже отправлен. Пропускаем.") @@ -465,7 +466,7 @@ def scan_new_releases(gf: GitFlicClient, sb: PTSandboxClient, releases_to_process = [ r for r in releases if not is_release_sent(state, pid, r.get("id"), - r.get("tag_name", ""), r.get("name", "")) + r.get("tagName", ""), r.get("title", "")) ] if not releases_to_process: logger.info("Нет новых релизов в проекте %s", pname) @@ -473,32 +474,26 @@ def scan_new_releases(gf: GitFlicClient, sb: PTSandboxClient, for rel in releases_to_process: rel_id = rel.get("id") - rel_tag = rel.get("tag_name", "") - rel_version = rel.get("name", "") + rel_tag = rel.get("tagName", "") + rel_version = rel.get("title", "") rel_key = make_release_key(pid, rel_id, rel_tag, rel_version) - logger.info("Новый релиз: %s (tag=%s, version=%s)", + logger.info("Новый релиз: %s (tag=%s, title=%s)", rel_key, rel_tag, rel_version) - # Собираем файлы для скачивания + # Собираем файлы для скачивания (attachmentFiles из релиза) files_to_download: List[Dict[str, Any]] = [] local_files: List[Dict[str, Any]] = [] - # 1. Архив исходного кода по тегу - source_url = gf.get_release_source_archive_url(pid, rel_tag) - sf = f"source_{pname.replace('/', '_')}_{rel_tag}.zip" - files_to_download.append({"type": "source_archive", "url": source_url, "filename": sf}) + assets = rel.get("attachmentFiles", []) + for asset in assets: + aname = asset.get("name", "") + aurl = asset.get("link", "") + if not aname or not aurl: + continue + files_to_download.append({"type": "asset", "url": aurl, "filename": aname}) - # 2. Assets (прикреплённые файлы) - try: - assets = gf.get_release_assets(pid, rel_id) - for asset in assets: - aname = asset.get("name") or asset.get("filename", "") - if not aname: - continue - aurl = gf.get_release_asset_download_url(pid, rel_id, aname) - files_to_download.append({"type": "asset", "url": aurl, "filename": aname}) - except requests.RequestException: - logger.debug("Assets для %s не получены", rel_key) + if not files_to_download: + logger.info("Нет файлов для скачивания в релизе %s", rel_key) # Скачиваем все файлы for fi in files_to_download: