import os import sys import json import argparse import urllib.request import urllib.parse import mimetypes import package_dist DEFAULT_GITEA_URL = os.environ.get("GITEA_SERVER_URL", "https://gitea.eibl.tech") DEFAULT_REPO = os.environ.get("GITEA_REPOSITORY", "me0nline/LOGAR") def get_existing_assets(base_url, repo, release_id, token): url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets" req = urllib.request.Request(url, headers={"Authorization": f"token {token}", "Accept": "application/json"}) try: with urllib.request.urlopen(req) as resp: return json.loads(resp.read().decode("utf-8")) except Exception: return [] def delete_asset(base_url, repo, release_id, asset_id, token): url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets/{asset_id}" req = urllib.request.Request(url, method="DELETE", headers={"Authorization": f"token {token}"}) try: with urllib.request.urlopen(req) as resp: pass except Exception: pass def upload_file_to_release(base_url, repo, release_id, token, file_path): filename = os.path.basename(file_path) # Clean up existing asset with same name if already present existing_assets = get_existing_assets(base_url, repo, release_id, token) for asset in existing_assets: if asset.get("name") == filename: print(f"[*] Removing existing asset '{filename}' (ID: {asset['id']})...") delete_asset(base_url, repo, release_id, asset["id"], token) url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets?name={urllib.parse.quote(filename)}" with open(file_path, "rb") as f: file_bytes = f.read() req = urllib.request.Request(url, data=file_bytes, method="POST") req.add_header("Authorization", f"token {token}") req.add_header("Content-Type", "application/octet-stream") req.add_header("Accept", "application/json") try: with urllib.request.urlopen(req) as resp: data = json.loads(resp.read().decode("utf-8")) print(f"[+] Attached {filename} ({len(file_bytes)} bytes) to release.") return data except urllib.error.HTTPError as e: err = e.read().decode("utf-8", errors="ignore") print(f"[!] Error uploading {filename}: HTTP {e.code} - {err}") return None def create_or_get_release(base_url, repo, tag, token, title=None, notes=None): url = f"{base_url}/api/v1/repos/{repo}/releases" headers = { "Authorization": f"token {token}", "Content-Type": "application/json", "Accept": "application/json" } # Check if release exists check_url = f"{base_url}/api/v1/repos/{repo}/releases/tags/{urllib.parse.quote(tag)}" check_req = urllib.request.Request(check_url, headers={"Authorization": f"token {token}"}) try: with urllib.request.urlopen(check_req) as resp: existing = json.loads(resp.read().decode("utf-8")) print(f"[*] Found existing release for tag {tag} (ID: {existing['id']})") if notes or title: patch_url = f"{base_url}/api/v1/repos/{repo}/releases/{existing['id']}" patch_payload = {} if title: patch_payload["name"] = title if notes: patch_payload["body"] = notes patch_req = urllib.request.Request( patch_url, data=json.dumps(patch_payload).encode("utf-8"), headers=headers, method="PATCH" ) try: with urllib.request.urlopen(patch_req) as p_resp: print(f"[+] Updated release description for tag {tag}") except Exception as e: print(f"[!] Warning: Could not update existing release description: {e}") return existing["id"] except urllib.error.HTTPError: pass # Create new release payload = { "tag_name": tag, "name": title or f"LOGAR Release {tag}", "body": notes or f"Automated binary release for {tag}.", "draft": False, "prerelease": False } req = urllib.request.Request(url, data=json.dumps(payload).encode("utf-8"), headers=headers, method="POST") with urllib.request.urlopen(req) as resp: created = json.loads(resp.read().decode("utf-8")) print(f"[+] Created release {tag} (ID: {created['id']})") return created["id"] def main(): parser = argparse.ArgumentParser(description="Upload LOGAR compiled binaries directly to Gitea Release") parser.add_argument("--tag", default=os.environ.get("GITEA_REF_NAME"), help="Release tag name (e.g. v1.0.1)") parser.add_argument("--token", default=os.environ.get("GITEA_TOKEN"), help="Gitea Personal Access Token (or set GITEA_TOKEN env var)") parser.add_argument("--url", default=DEFAULT_GITEA_URL, help="Base Gitea instance URL") parser.add_argument("--repo", default=DEFAULT_REPO, help="Repository owner/name") parser.add_argument("--title", default=os.environ.get("RELEASE_TITLE"), help="Release title") parser.add_argument("--notes", default=os.environ.get("RELEASE_NOTES"), help="Release description / notes") parser.add_argument("--notes-file", default=None, help="Path to markdown file with release notes") parser.add_argument("--skip-build", action="store_true", help="Skip running package_dist.py before upload") args = parser.parse_args() tag = args.tag if not tag: tag = input("Enter tag name (e.g. v1.0.1): ").strip() token = args.token if not token: token = input("Enter Gitea Token: ").strip() if not tag or not token: print("[!] Tag and Token are required.") sys.exit(1) # Resolve release notes notes = args.notes if not notes and args.notes_file and os.path.exists(args.notes_file): with open(args.notes_file, "r", encoding="utf-8") as nf: notes = nf.read() elif not notes and os.path.exists("RELEASE_NOTES.md"): with open("RELEASE_NOTES.md", "r", encoding="utf-8") as nf: notes = nf.read() elif not notes: notes = ( f"## LOGAR Release {tag}\n\n" "### Changes in this Release:\n" "- **Removed Client Filter Logic**: Removed restrictive source-level noise filtering on edge forwarders. Clients now stream all candidate events from `INFO` up to `ERROR` across the lookback window instead of discarding them at the source.\n" "- **State Tracking & Deduplication**: Added persistent state tracking (`client_state.json`) with cursor and record number deduplication so previously transmitted events are never resent.\n" "- **24-Hour Lookback Window**: Forwarders now scan and upload events from the last 24 hours, skipping older entries.\n" "- **Lightweight Distribution Structure**: Cleaned `out/` to strictly contain deployment documentation and sample configurations.\n" "- **Automated Gitea CI/CD**: Integrated push testing workflow (`ci.yml`) and automated release asset packaging (`release.yml`).\n" ) title = args.title or f"LOGAR Release {tag}" if not args.skip_build: print("[*] Assembling compiled binaries...") package_dist.main() dist_dir = os.path.abspath("dist") if not os.path.exists(dist_dir) or not os.listdir(dist_dir): print("[!] No binaries found in dist/. Run package_dist.py first.") sys.exit(1) print(f"[*] Connecting to Gitea: {args.url} (repo: {args.repo})...") release_id = create_or_get_release(args.url, args.repo, tag, token, title=title, notes=notes) print(f"[*] Uploading binary assets from '{dist_dir}'...") for f in sorted(os.listdir(dist_dir)): fpath = os.path.join(dist_dir, f) if os.path.isfile(fpath): upload_file_to_release(args.url, args.repo, release_id, token, fpath) print(f"\n[+] Release successfully published with binary assets: {args.url}/{args.repo}/releases/tag/{tag}") if __name__ == "__main__": main()