205 lines
9.6 KiB
Python
205 lines
9.6 KiB
Python
import os
|
|
import sys
|
|
import json
|
|
import argparse
|
|
import urllib.request
|
|
import urllib.parse
|
|
import mimetypes
|
|
import package_dist
|
|
|
|
import time
|
|
|
|
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, max_retries=3):
|
|
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()
|
|
|
|
for attempt in range(1, max_retries + 1):
|
|
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"[!] Attempt {attempt}/{max_retries} - Error uploading {filename}: HTTP {e.code} - {err}")
|
|
if attempt < max_retries:
|
|
time.sleep(2 * attempt)
|
|
else:
|
|
return None
|
|
except Exception as ex:
|
|
print(f"[!] Attempt {attempt}/{max_retries} - Exception uploading {filename}: {ex}")
|
|
if attempt < max_retries:
|
|
time.sleep(2 * attempt)
|
|
else:
|
|
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")
|
|
try:
|
|
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"]
|
|
except urllib.error.HTTPError as e:
|
|
print(f"[*] Release creation returned HTTP {e.code}. Checking if peer runner created it concurrently...")
|
|
for attempt in range(1, 6):
|
|
time.sleep(2)
|
|
try:
|
|
with urllib.request.urlopen(check_req) as resp:
|
|
existing = json.loads(resp.read().decode("utf-8"))
|
|
print(f"[+] Retrieved peer-created release for tag {tag} (ID: {existing['id']})")
|
|
return existing["id"]
|
|
except Exception:
|
|
pass
|
|
raise
|
|
|
|
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"
|
|
"- **Dual Platform Gitea Release Automation**: Added dedicated Windows (`release-windows.yml`) and Linux (`release-linux.yml`) Gitea Actions to compile native executables and publish assets concurrently.\n"
|
|
"- **Dedicated SHA-256 Checksums**: Release assets now include dedicated checksum files matching `[win/linux]_[client/agent]_sha256sum` (e.g., `win_client_sha256sum`, `win_agent_sha256sum`, `win_server_sha256sum`, `linux_client_sha256sum`, `linux_agent_sha256sum`, `linux_server_sha256sum`).\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.\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()
|