Added latest release management to rngit

This commit is contained in:
Mark Qvist
2026-05-14 14:13:42 +02:00
parent bdac57ec0b
commit d881c111f6
2 changed files with 154 additions and 35 deletions
+29 -18
View File
@@ -98,6 +98,10 @@ class NomadNetworkNode():
CLR_FILE = "`F66d"
CLR_DIM = "`F666"
CLR_DIM_H = "`F444"
CLR_OK_DIM = "`FT537855"
CLR_DIFF_A = "`F0a0"
CLR_DIFF_R = "`F900"
CLR_DIFF_P = "`F0aa"
# Yes, I'm being intentionally weird here. If you
# want to use tabs, three spaces is all you get.
@@ -434,7 +438,7 @@ class NomadNetworkNode():
# Get releases count
releases_path = f"{repo['path']}.releases"
releases_count = 0
releases = self.owner.releases_list_data(releases_path)
releases, latest_release = self.owner.releases_list_data(releases_path)
if releases: releases_count = len([r for r in releases if r.get("status") == "published"])
sep = self.icon("sep")
@@ -1174,7 +1178,7 @@ class NomadNetworkNode():
nav_content = "".join(nav_parts)
releases_path = f"{repo['path']}.releases"
releases = self.owner.releases_list_data(releases_path)
releases, latest_release = self.owner.releases_list_data(releases_path)
if not releases:
content_parts.append(self.m_heading("Releases", 2))
content_parts.append("\nNo releases available for this repository.\n")
@@ -1198,8 +1202,9 @@ class NomadNetworkNode():
link = self.m_link(tag, self.PATH_RELEASE, g=group_name, r=repo_name, t=tag)
sep = self.icon("sep")
latest_str = f" {sep} {self.CLR_OK_DIM}`*Latest`*`f" if tag == latest_release else ""
artifacts_str = f"`*{artifacts} artifact{'s' if artifacts != 1 else ''}`*"
content_parts.append(f"{link} {self.CLR_DIM}{date_str} {sep} {artifacts_str}`f\n")
content_parts.append(f"{link} {self.CLR_DIM}{date_str} {sep} {artifacts_str}{latest_str}`f\n")
if preview:
if rel_format == "markdown": content_parts.append(f"{self.mdc.format_block(preview)}\n")
elif rel_format == "micron": content_parts.append(f"{preview}\n")
@@ -1230,13 +1235,16 @@ class NomadNetworkNode():
releases_path = f"{repo['path']}.releases"
if tag == "latest":
releases = self.owner.releases_list_data(releases_path)
releases, latest_release = self.owner.releases_list_data(releases_path)
if not releases:
content = self.m_heading("Release Not Found", 2) + f"\nNo latest release exist.\n"
content = self.m_heading("Release Not Found", 2) + f"\nNo releases exist.\n"
return self.render_template(content, nav_content=nav_content, st=st)
recent_releases = sorted(releases, key=lambda x: x['created'], reverse=True)
tag = recent_releases[0]["tag"]
if not latest_release:
recent_releases = sorted(releases, key=lambda x: x['created'], reverse=True)
tag = recent_releases[0]["tag"]
else: tag = latest_release
content_parts = []
nav_parts = []
@@ -1561,10 +1569,13 @@ class NomadNetworkNode():
releases_path = f"{repo['path']}.releases"
if tag == "latest":
releases = self.owner.releases_list_data(releases_path)
releases, latest_release = self.owner.releases_list_data(releases_path)
if not releases: return None
recent_releases = sorted(releases, key=lambda x: x['created'], reverse=True)
tag = recent_releases[0]["tag"]
if not latest_release:
recent_releases = sorted(releases, key=lambda x: x['created'], reverse=True)
tag = recent_releases[0]["tag"]
else: tag = latest_release
release_dir = os.path.join(releases_path, tag)
artifacts_dir = os.path.join(release_dir, "artifacts")
@@ -2207,17 +2218,17 @@ class NomadNetworkNode():
for line in lines:
if line.startswith("+"):
if line.startswith("+++"): formatted_lines.append(self.m_escape(line))
else: formatted_lines.append(f"`F0a0{self.m_escape(line)}`f")
else: formatted_lines.append(f"{self.CLR_DIFF_A}{self.m_escape(line)}`f")
elif line.startswith("-"):
if line.startswith("---"): formatted_lines.append(self.m_escape(f"\\{line}"))
else: formatted_lines.append(f"`F900{self.m_escape(line)}`f")
else: formatted_lines.append(f"{self.CLR_DIFF_R}{self.m_escape(line)}`f")
elif line.startswith("@@"):
formatted_lines.append(f"`F0aa{self.m_escape(line)}`f")
formatted_lines.append(f"{self.CLR_DIFF_P}{self.m_escape(line)}`f")
elif line.startswith("diff ") or line.startswith("index ") or line.startswith("new file") or line.startswith("deleted file"):
if line.startswith("diff --git a"): formatted_lines.append("")
formatted_lines.append(f"`F666{self.m_escape(line)}`f")
formatted_lines.append(f"{self.CLR_DIM}{self.m_escape(line)}`f")
else: formatted_lines.append(self.m_escape(line))
@@ -2311,9 +2322,9 @@ class NomadNetworkNode():
final_label = f"{labels[-1][:12]:>12}"
middle_space = chart_width-len(first_label)-len(final_label)
label_line = f"{indent}`F666{first_label}`f"
label_line = f"{indent}{self.CLR_DIM}{first_label}`f"
label_line += " " * middle_space
label_line += f"`F666{final_label}`f\n"
label_line += f"{self.CLR_DIM}{final_label}`f\n"
chart_lines.append(label_line)
return "".join(chart_lines)
@@ -2363,9 +2374,9 @@ class NomadNetworkNode():
final_label = f"{labels[-1][:12]:>12}"
middle_space = chart_width-len(first_label)-len(final_label)
label_line = f"{indent}`F666{first_label}`f"
label_line = f"{indent}{self.CLR_DIM}{first_label}`f"
label_line += " " * middle_space
label_line += f"`F666{final_label}`f\n"
label_line += f"{self.CLR_DIM}{final_label}`f\n"
lines.append(label_line)
return "".join(lines)
+125 -17
View File
@@ -84,6 +84,7 @@ def program_setup(configdir, rnsconfigdir=None, verbosity=0, quietness=0, servic
elif operation == "view": git_client.view_release(remote=task["remote"], target=task["target"])
elif operation == "create": git_client.create_release(remote=task["remote"], target=task["target"])
elif operation == "delete": git_client.delete_release(remote=task["remote"], target=task["target"])
elif operation == "latest": git_client.latest_release(remote=task["remote"], target=task["target"])
else: print("Invalid operation"); exit(1)
elif command == "work":
@@ -414,8 +415,18 @@ class ReticulumGitClient():
error_msg = response[1:].decode("utf-8", errors="ignore")
self.abort(f"Server error: {error_msg}")
if len(response) > 1: releases = mp.unpackb(response[1:])
else: releases = []
if len(response) > 1: unpacked = mp.unpackb(response[1:])
else: unpacked = []
if type(unpacked) == list:
releases = unpacked
latest_release = None
elif type(unpacked) == dict:
releases = unpacked["releases"]
latest_release = unpacked["latest"]
else: self.abort("Invalid release data format from remote")
if not releases: print("No releases for this repository")
else:
@@ -429,7 +440,8 @@ class ReticulumGitClient():
artifacts = str(rel.get("artifacts", 0))
preview = rel.get("preview", "")[:34]
print(f"{tag:<10} {status:<10} {created:<17} {artifacts:<5} {preview}")
print()
if latest_release: print(f"\nThe latest release is: {latest_release}")
except Exception as e: self.abort(f"Error listing releases: {e}")
finally:
@@ -635,6 +647,49 @@ class ReticulumGitClient():
finally:
if self.link: self.link.teardown()
def latest_release(self, remote=None, target=None):
if not remote: print(f"No remote specified"); exit(1)
if not target: print(f"No target specified"); exit(1)
self.connect_remote(remote)
timeout = self.link_timeout
while not self.link_ready and not self.link_failed and timeout > 0:
time.sleep(0.5)
timeout -= 1
if not self.link_ready: self.abort("Failed to establish link")
print("\r \r", end="")
try:
destination_hash, group, repo = self.parse_remote_url(remote)
repo_path = f"{group}/{repo}"
print(f"Are you sure you want to set {target} as the latest release? [y/N]: ", end="")
try: confirm = input().strip().lower()
except EOFError: confirm = "n"
if confirm != "y":
print("Update cancelled")
return
request_data = { self.IDX_REPOSITORY: repo_path,
"operation": "latest", "tag": target }
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=30)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
if status_byte != 0:
error_msg = response[1:].decode("utf-8", errors="ignore")
self.abort(f"Remote error: {error_msg}")
print(f"Release {target} set as latest")
except Exception as e: self.abort(f"Error setting latest release: {e}")
finally:
if self.link: self.link.teardown()
########################
# Work Docs Management #
########################
@@ -2012,9 +2067,9 @@ class ReticulumGitNode():
if not read_access: return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Not found"
if operation in ["create", "delete"] and release_access and read_access: access = True
elif operation in ["list", "view"] and read_access: access = True
else: access = False
if operation in ["create", "delete", "latest"] and release_access and read_access: access = True
elif operation in ["list", "view"] and read_access: access = True
else: access = False
if not access: return self.RES_DISALLOWED.to_bytes(1, "big") + b"Not allowed"
else:
@@ -2026,6 +2081,7 @@ class ReticulumGitNode():
elif operation == "view" and read_access: return self._release_view(releases_path, data)
elif operation == "create" and release_access: return self._release_create(releases_path, repository_path, data, remote_identity)
elif operation == "delete" and release_access: return self._release_delete(releases_path, data)
elif operation == "latest" and release_access: return self._release_latest(releases_path, data)
else: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request"
except Exception as e:
@@ -2034,8 +2090,10 @@ class ReticulumGitNode():
def releases_list_data(self, releases_path):
try:
tags = {}
releases = []
if not os.path.isdir(releases_path): return releases
latest_release = None
if not os.path.isdir(releases_path): return releases, None
for entry in os.listdir(releases_path):
release_dir = os.path.join(releases_path, entry)
if not os.path.isdir(release_dir): continue
@@ -2045,14 +2103,16 @@ class ReticulumGitNode():
try:
meta = ConfigObj(meta_path)
release_info = { "tag": meta.get("tag", entry),
release_tag = meta.get("tag", entry)
release_status = meta.get("status", "unknown")
release_info = { "tag": release_tag,
"hash": meta.get("hash", ""),
"created": meta.as_int("created") if "created" in meta else 0,
"status": meta.get("status", "unknown"),
"status": release_status,
"created_by": meta.get("created_by", "") }
notes_preview = ""
notes_format = "markdown"
notes_format = "markdown"
for notes_file in ["RELEASE.md", "RELEASE.mu", "RELEASE.txt"]:
notes_path = os.path.join(release_dir, notes_file)
if os.path.isfile(notes_path):
@@ -2082,25 +2142,37 @@ class ReticulumGitNode():
else: release_info["artifacts"] = 0
releases.append(release_info)
tags[release_tag] = True if release_status == "published" else False
except Exception as e:
RNS.log(f"Error reading release metadata for {entry}: {e}", RNS.LOG_DEBUG)
continue
try:
latest_path = os.path.join(releases_path, "latest")
if os.path.isfile(latest_path):
with open(latest_path, "r") as fh: latest_tag = fh.read().strip()
if latest_tag in tags and tags[latest_tag] == True: latest_release = latest_tag
except Exception as e: RNS.log(f"Could not determine latest release for {releases_path}: {e}", RNS.LOG_ERROR)
releases.sort(key=lambda x: x.get("created", 0), reverse=True)
return releases
return releases, latest_release
except Exception as e:
RNS.log(f"Error listing releases for {releases_path}: {e}", RNS.LOG_ERROR)
return None
return None, None
def _release_list(self, releases_path):
if not os.path.isdir(releases_path): return b"\x00" + mp.packb([])
releases = self.releases_list_data(releases_path)
releases, latest_release = self.releases_list_data(releases_path)
if releases == None: return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Error listing releases"
release_data = {"releases": releases, "latest": latest_release}
return b"\x00" + mp.packb(releases)
return b"\x00" + mp.packb(release_data)
def release_data(self, release_dir, tag):
try:
@@ -2165,8 +2237,21 @@ class ReticulumGitNode():
if not tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid tag specified"
tag = os.path.basename(tag)
release_dir = os.path.join(releases_path, tag)
latest_release = None
if tag == "latest":
try:
latest_path = os.path.join(releases_path, "latest")
if os.path.isfile(latest_path):
with open(latest_path, "r") as fh: latest_tag = fh.read().strip()
latest_release = latest_tag
except Exception as e: RNS.log(f"Could not determine latest release for {releases_path}: {e}", RNS.LOG_ERROR)
if not latest_release: return self.RES_NOT_FOUND.to_bytes(1, "big") + b"No latest release found"
else: tag = latest_release
release_dir = os.path.join(releases_path, tag)
if not os.path.isdir(release_dir): return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Release not found"
release_info = self.release_data(release_dir, tag)
@@ -2311,11 +2396,34 @@ class ReticulumGitNode():
try:
shutil.rmtree(release_dir)
RNS.log(f"Deleted release {tag}", RNS.LOG_DEBUG)
RNS.log(f"Deleted release {tag} from {releases_path}", RNS.LOG_DEBUG)
return b"\x00"
except Exception as e:
RNS.log(f"Error deleting release: {e}", RNS.LOG_ERROR)
RNS.log(f"Error deleting release from {releases_path}: {e}", RNS.LOG_ERROR)
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
def _release_latest(self, releases_path, data):
tag = data.get("tag", "")
if not tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No tag specified"
if "/" in tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid tag specified"
tag = os.path.basename(tag)
release_dir = os.path.join(releases_path, tag)
if not os.path.isdir(release_dir): return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Release not found"
try:
latest_path = os.path.join(releases_path, "latest")
tmp_path = latest_path+".tmp"
with open(tmp_path, "w") as fh: fh.write(tag)
os.rename(tmp_path, latest_path)
RNS.log(f"Set {tag} as latest release for {releases_path}", RNS.LOG_DEBUG)
return b"\x00"
except Exception as e:
RNS.log(f"Error setting latest release for {releases_path}: {e}", RNS.LOG_ERROR)
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
#########################