forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
a05a79eb83
commit
f8314e7de1
14 files changed
+529
-108
No files matched your search
+327
-72
@@ -1,4 +1,4 @@
|
||||
"""Visual release manager for Healer Man with explicit safety gates.
|
||||
"""Visual Git, TrueNAS, and optional APK release manager with safety gates.
|
||||
|
||||
The GUI intentionally leaves the TrueNAS portion as copyable shell commands:
|
||||
the app must be stopped and started from the TrueNAS Apps UI around that block.
|
||||
@@ -71,6 +71,8 @@ OVERRIDABLE_TEST_LABELS = frozenset(
|
||||
ANSI_ESCAPE_PATTERN = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
||||
FAILURE_OUTPUT_LINE_LIMIT = 160
|
||||
FAILURE_OUTPUT_CHARACTER_LIMIT = 20_000
|
||||
DEFAULT_GIT_TRUENAS_ONLY = True
|
||||
DEFAULT_RUN_TESTS = True
|
||||
|
||||
|
||||
class ReleaseError(RuntimeError):
|
||||
@@ -104,15 +106,27 @@ class ReleaseVersion:
|
||||
version_code: int
|
||||
|
||||
|
||||
def full_release_checks(npm: str) -> tuple[tuple[list[str], str], ...]:
|
||||
"""Commands that must succeed before a release can be staged."""
|
||||
return (
|
||||
def full_release_checks(
|
||||
npm: str,
|
||||
*,
|
||||
include_android: bool = True,
|
||||
include_tests: bool = True,
|
||||
) -> tuple[tuple[list[str], str], ...]:
|
||||
"""Commands that must succeed before an update can be staged."""
|
||||
test_checks = (
|
||||
([npm, "run", "content:test"], "Content tests"),
|
||||
([npm, "run", "server:test"], "Server tests"),
|
||||
([npm, "test"], "Application tests"),
|
||||
([npm, "run", "test:shelved"], "Shelved-mode application tests"),
|
||||
)
|
||||
required_checks = (
|
||||
([npm, "run", "content:publish"], "Publish verified 54-dungeon content manifest"),
|
||||
([npm, "run", "build"], "Production build"),
|
||||
)
|
||||
checks = (test_checks if include_tests else ()) + required_checks
|
||||
if not include_android:
|
||||
return checks
|
||||
return checks + (
|
||||
([npm, "run", "android:sync"], "Android web build and Capacitor sync"),
|
||||
)
|
||||
|
||||
@@ -645,7 +659,7 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
)
|
||||
ttk.Label(
|
||||
title,
|
||||
text="Validate → inspect → commit → push → publish signed APK",
|
||||
text="Validate → inspect → commit → push → deploy, with optional signed APK",
|
||||
style="Sub.TLabel",
|
||||
).grid(row=1, column=0, sticky="w", pady=(2, 0))
|
||||
self.refresh_button = ttk.Button(title, text="Refresh", command=self.refresh)
|
||||
@@ -665,6 +679,12 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
self.next_version_var = tk.StringVar()
|
||||
self.next_code_var = tk.StringVar()
|
||||
self.commit_message_var = tk.StringVar()
|
||||
self.git_truenas_only_var = tk.BooleanVar(
|
||||
value=DEFAULT_GIT_TRUENAS_ONLY
|
||||
)
|
||||
self.run_tests_var = tk.BooleanVar(value=DEFAULT_RUN_TESTS)
|
||||
self.release_mode_hint_var = tk.StringVar()
|
||||
self.test_mode_hint_var = tk.StringVar()
|
||||
|
||||
ttk.Label(release_panel, text="Current", style="Muted.Panel.TLabel").grid(
|
||||
row=1, column=0, sticky="w"
|
||||
@@ -682,25 +702,31 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
ttk.Label(release_panel, text="Bump", style="Muted.Panel.TLabel").grid(
|
||||
row=2, column=0, sticky="w", pady=(12, 0)
|
||||
)
|
||||
bump_box = ttk.Combobox(
|
||||
self.bump_box = ttk.Combobox(
|
||||
release_panel,
|
||||
textvariable=self.bump_var,
|
||||
values=("Patch", "Minor", "Major"),
|
||||
state="readonly",
|
||||
width=10,
|
||||
)
|
||||
bump_box.grid(row=2, column=1, sticky="w", padx=(8, 20), pady=(12, 0))
|
||||
bump_box.bind("<<ComboboxSelected>>", lambda _event: self._suggest_versions())
|
||||
self.bump_box.grid(row=2, column=1, sticky="w", padx=(8, 20), pady=(12, 0))
|
||||
self.bump_box.bind("<<ComboboxSelected>>", lambda _event: self._suggest_versions())
|
||||
ttk.Label(release_panel, text="Next version", style="Muted.Panel.TLabel").grid(
|
||||
row=3, column=0, sticky="w", pady=(9, 0)
|
||||
)
|
||||
ttk.Entry(release_panel, textvariable=self.next_version_var, width=15).grid(
|
||||
self.next_version_entry = ttk.Entry(
|
||||
release_panel, textvariable=self.next_version_var, width=15
|
||||
)
|
||||
self.next_version_entry.grid(
|
||||
row=3, column=1, sticky="ew", padx=(8, 20), pady=(9, 0)
|
||||
)
|
||||
ttk.Label(release_panel, text="Next code", style="Muted.Panel.TLabel").grid(
|
||||
row=3, column=2, sticky="w", pady=(9, 0)
|
||||
)
|
||||
ttk.Entry(release_panel, textvariable=self.next_code_var, width=12).grid(
|
||||
self.next_code_entry = ttk.Entry(
|
||||
release_panel, textvariable=self.next_code_var, width=12
|
||||
)
|
||||
self.next_code_entry.grid(
|
||||
row=3, column=3, sticky="ew", padx=(8, 0), pady=(9, 0)
|
||||
)
|
||||
ttk.Label(release_panel, text="Commit message", style="Muted.Panel.TLabel").grid(
|
||||
@@ -709,7 +735,7 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
ttk.Entry(release_panel, textvariable=self.commit_message_var).grid(
|
||||
row=4, column=1, columnspan=3, sticky="ew", padx=(8, 0), pady=(9, 0)
|
||||
)
|
||||
ttk.Label(release_panel, text="Release notes", style="Muted.Panel.TLabel").grid(
|
||||
ttk.Label(release_panel, text="Release notes (APK only)", style="Muted.Panel.TLabel").grid(
|
||||
row=5, column=0, sticky="nw", pady=(9, 0)
|
||||
)
|
||||
self.release_notes = tk.Text(
|
||||
@@ -727,12 +753,42 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
self.release_notes.grid(
|
||||
row=5, column=1, columnspan=3, sticky="nsew", padx=(8, 0), pady=(9, 0)
|
||||
)
|
||||
self.release_mode_toggle = ttk.Checkbutton(
|
||||
release_panel,
|
||||
text="Git + TrueNAS only (skip Android version bump and APK publication)",
|
||||
variable=self.git_truenas_only_var,
|
||||
command=self._update_release_mode,
|
||||
)
|
||||
self.release_mode_toggle.grid(
|
||||
row=6, column=0, columnspan=4, sticky="w", pady=(12, 0)
|
||||
)
|
||||
ttk.Label(
|
||||
release_panel,
|
||||
textvariable=self.release_mode_hint_var,
|
||||
style="Muted.Panel.TLabel",
|
||||
wraplength=610,
|
||||
).grid(row=7, column=0, columnspan=4, sticky="w", pady=(4, 0))
|
||||
self.run_tests_toggle = ttk.Checkbutton(
|
||||
release_panel,
|
||||
text="Run test suites (recommended)",
|
||||
variable=self.run_tests_var,
|
||||
command=self._update_test_mode,
|
||||
)
|
||||
self.run_tests_toggle.grid(
|
||||
row=8, column=0, columnspan=4, sticky="w", pady=(10, 0)
|
||||
)
|
||||
ttk.Label(
|
||||
release_panel,
|
||||
textvariable=self.test_mode_hint_var,
|
||||
style="Muted.Panel.TLabel",
|
||||
wraplength=610,
|
||||
).grid(row=9, column=0, columnspan=4, sticky="w", pady=(4, 0))
|
||||
|
||||
secrets_panel = ttk.Frame(shell, style="Panel.TFrame", padding=15)
|
||||
secrets_panel.grid(row=1, column=1, sticky="nsew", padx=(8, 0))
|
||||
secrets_panel.columnconfigure(1, weight=1)
|
||||
ttk.Label(
|
||||
secrets_panel, text="Signing and Gitea", style="Panel.TLabel",
|
||||
secrets_panel, text="Signing and Gitea (APK only)", style="Panel.TLabel",
|
||||
font=("Segoe UI Semibold", 12),
|
||||
).grid(row=0, column=0, columnspan=3, sticky="w")
|
||||
ttk.Label(
|
||||
@@ -869,6 +925,54 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
self.retry_button.grid(row=0, column=1, padx=7)
|
||||
self.cancel_button = ttk.Button(actions, text="Cancel", command=self.cancel, state="disabled")
|
||||
self.cancel_button.grid(row=0, column=2, padx=(7, 0))
|
||||
self._update_release_mode()
|
||||
self._update_test_mode()
|
||||
|
||||
def _update_release_mode(self) -> None:
|
||||
git_truenas_only = self.git_truenas_only_var.get()
|
||||
self.bump_box.configure(state="disabled" if git_truenas_only else "readonly")
|
||||
entry_state = "disabled" if git_truenas_only else "normal"
|
||||
self.next_version_entry.configure(state=entry_state)
|
||||
self.next_code_entry.configure(state=entry_state)
|
||||
if git_truenas_only:
|
||||
self.release_button.configure(text="Validate and Push Git + TrueNAS Update")
|
||||
self.release_mode_hint_var.set(
|
||||
"Default mode: keeps the current Android version, skips Android sync and APK "
|
||||
"publication, and prepares the pinned TrueNAS deployment commands after push."
|
||||
)
|
||||
else:
|
||||
self.release_button.configure(text="Validate, Push, and Publish APK")
|
||||
self.release_mode_hint_var.set(
|
||||
"APK mode: bumps the app and Android versions, runs Android sync, and publishes "
|
||||
"the signed APK after the Git push."
|
||||
)
|
||||
self._suggest_versions()
|
||||
current_notes = self.release_notes.get("1.0", "end").strip()
|
||||
server_default = "Healer Man Git and TrueNAS server update."
|
||||
apk_default = (
|
||||
f"Healer Man {self.next_version_var.get()} app and Android update."
|
||||
)
|
||||
generated_apk_notes = re.fullmatch(
|
||||
r"Healer Man \d+\.\d+\.\d+ app and Android update\.",
|
||||
current_notes,
|
||||
)
|
||||
if git_truenas_only and (not current_notes or generated_apk_notes):
|
||||
self._set_text(self.release_notes, server_default)
|
||||
elif not git_truenas_only and (
|
||||
not current_notes or current_notes == server_default
|
||||
):
|
||||
self._set_text(self.release_notes, apk_default)
|
||||
|
||||
def _update_test_mode(self) -> None:
|
||||
if self.run_tests_var.get():
|
||||
self.test_mode_hint_var.set(
|
||||
"Runs content, server, normal-application, and shelved-application tests."
|
||||
)
|
||||
else:
|
||||
self.test_mode_hint_var.set(
|
||||
"Skips those four test suites. Candidate safety checks, content publication, "
|
||||
"the production build, and staged-file review still run."
|
||||
)
|
||||
|
||||
def _command_text_widget(self, notebook: ttk.Notebook) -> tk.Text:
|
||||
frame = ttk.Frame(notebook)
|
||||
@@ -938,7 +1042,7 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
self.last_commit = current_commit()
|
||||
self.current_version_var.set(package_version)
|
||||
self.current_code_var.set(str(release_version.version_code))
|
||||
self._suggest_versions()
|
||||
self._update_release_mode()
|
||||
change_count = len(
|
||||
[line for line in status.splitlines() if not line.startswith("##")]
|
||||
)
|
||||
@@ -956,10 +1060,6 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
generate_truenas_verify_commands(),
|
||||
readonly=True,
|
||||
)
|
||||
if not self.release_notes.get("1.0", "end").strip():
|
||||
self.release_notes.insert(
|
||||
"1.0", f"Healer Man {self.next_version_var.get()} app and Android update."
|
||||
)
|
||||
self.status_var.set("Ready")
|
||||
except Exception as exc:
|
||||
self.status_var.set("Configuration error")
|
||||
@@ -973,7 +1073,10 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
next_code = suggest_version_code(next_version, release.version_code)
|
||||
self.next_version_var.set(next_version)
|
||||
self.next_code_var.set(str(next_code))
|
||||
self.commit_message_var.set(f"Release Healer Man {next_version}")
|
||||
if self.git_truenas_only_var.get():
|
||||
self.commit_message_var.set("Update Healer Man server")
|
||||
else:
|
||||
self.commit_message_var.set(f"Release Healer Man {next_version}")
|
||||
except Exception as exc:
|
||||
self.status_var.set(str(exc))
|
||||
|
||||
@@ -1037,32 +1140,84 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
def start_full_release(self) -> None:
|
||||
if self.busy:
|
||||
return
|
||||
publish_apk = not self.git_truenas_only_var.get()
|
||||
run_tests = self.run_tests_var.get()
|
||||
test_summary = (
|
||||
"The four test suites will run."
|
||||
if run_tests
|
||||
else "The four test suites will be skipped; safety checks and the production build still run."
|
||||
)
|
||||
try:
|
||||
version = self._requested_version()
|
||||
credentials = self._credentials()
|
||||
toolchain_ready, toolchain_detail = android_toolchain_status()
|
||||
if not toolchain_ready:
|
||||
raise ReleaseError(toolchain_detail)
|
||||
commit_message = self.commit_message_var.get().strip()
|
||||
notes = self.release_notes.get("1.0", "end").strip()
|
||||
if not commit_message:
|
||||
raise ReleaseError("Commit message is required.")
|
||||
if not notes:
|
||||
raise ReleaseError("Release notes are required.")
|
||||
version: ReleaseVersion | None = None
|
||||
credentials: dict[str, str] | None = None
|
||||
notes = ""
|
||||
if publish_apk:
|
||||
version = self._requested_version()
|
||||
credentials = self._credentials()
|
||||
toolchain_ready, toolchain_detail = android_toolchain_status()
|
||||
if not toolchain_ready:
|
||||
raise ReleaseError(toolchain_detail)
|
||||
notes = self.release_notes.get("1.0", "end").strip()
|
||||
if not notes:
|
||||
raise ReleaseError("Release notes are required.")
|
||||
except Exception as exc:
|
||||
messagebox.showerror("Cannot start release", str(exc), parent=self.root)
|
||||
return
|
||||
if not publish_apk:
|
||||
if not messagebox.askyesno(
|
||||
"Start Git + TrueNAS update?",
|
||||
"Run the server validation and production build, review and commit the exact "
|
||||
"changed files, then push main?\n\nThe Android version will not change and no "
|
||||
"APK will be built or published. The Deploy tab will be pinned to the new commit.\n\n"
|
||||
+ test_summary,
|
||||
parent=self.root,
|
||||
):
|
||||
return
|
||||
self._begin_worker(
|
||||
"Validating Git + TrueNAS update",
|
||||
lambda: self._full_release_worker(
|
||||
None,
|
||||
commit_message,
|
||||
"",
|
||||
None,
|
||||
publish_apk=False,
|
||||
run_tests=run_tests,
|
||||
),
|
||||
(
|
||||
"Git main is published without changing the Android version or publishing "
|
||||
"an APK.\n\nUse the TrueNAS Deploy tab after stopping the app, then the "
|
||||
"Verify tab after restarting it."
|
||||
),
|
||||
)
|
||||
return
|
||||
assert version is not None
|
||||
assert credentials is not None
|
||||
if not messagebox.askyesno(
|
||||
"Start full release?",
|
||||
f"Prepare Healer Man {version.version_name} (Android code {version.version_code})?\n\n"
|
||||
"The app will bump versions, run all release checks, then show the exact staged files "
|
||||
"before it commits, pushes main, and publishes the signed APK.",
|
||||
"before it commits, pushes main, and publishes the signed APK.\n\n"
|
||||
+ test_summary,
|
||||
parent=self.root,
|
||||
):
|
||||
return
|
||||
self._begin_worker(
|
||||
"Validating release",
|
||||
lambda: self._full_release_worker(version, commit_message, notes, credentials),
|
||||
lambda: self._full_release_worker(
|
||||
version,
|
||||
commit_message,
|
||||
notes,
|
||||
credentials,
|
||||
publish_apk=True,
|
||||
run_tests=run_tests,
|
||||
),
|
||||
(
|
||||
"Git main and the signed APK release are published.\n\nUse the TrueNAS Deploy "
|
||||
"tab after stopping the app, then the Verify tab after restarting it."
|
||||
),
|
||||
)
|
||||
|
||||
def start_apk_only(self) -> None:
|
||||
@@ -1095,9 +1250,18 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
self._begin_worker(
|
||||
"Publishing current APK",
|
||||
lambda: self._apk_only_worker(release_version, notes, credentials),
|
||||
(
|
||||
"The signed APK release is published for the current Git commit.\n\nUse the "
|
||||
"TrueNAS Deploy tab only if that commit is not already deployed."
|
||||
),
|
||||
)
|
||||
|
||||
def _begin_worker(self, label: str, target: Callable[[], None]) -> None:
|
||||
def _begin_worker(
|
||||
self,
|
||||
label: str,
|
||||
target: Callable[[], None],
|
||||
success_message: str,
|
||||
) -> None:
|
||||
self.busy = True
|
||||
self.cancel_event.clear()
|
||||
self.status_var.set(label)
|
||||
@@ -1105,11 +1269,21 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
self.release_button.configure(state="disabled")
|
||||
self.retry_button.configure(state="disabled")
|
||||
self.refresh_button.configure(state="disabled")
|
||||
self.release_mode_toggle.configure(state="disabled")
|
||||
self.run_tests_toggle.configure(state="disabled")
|
||||
self.cancel_button.configure(state="normal")
|
||||
self._append_log(f"\n=== {label} ===")
|
||||
threading.Thread(target=self._worker_wrapper, args=(target,), daemon=True).start()
|
||||
threading.Thread(
|
||||
target=self._worker_wrapper,
|
||||
args=(target, success_message),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def _worker_wrapper(self, target: Callable[[], None]) -> None:
|
||||
def _worker_wrapper(
|
||||
self,
|
||||
target: Callable[[], None],
|
||||
success_message: str,
|
||||
) -> None:
|
||||
try:
|
||||
target()
|
||||
except ReleaseCancelled as exc:
|
||||
@@ -1117,7 +1291,7 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
except Exception as exc:
|
||||
self.events.put(("done", "error", str(exc)))
|
||||
else:
|
||||
self.events.put(("done", "success", "Release workflow completed."))
|
||||
self.events.put(("done", "success", success_message))
|
||||
|
||||
def _preflight_git(self, *, require_clean: bool) -> None:
|
||||
if git_output("branch", "--show-current") != "main":
|
||||
@@ -1137,10 +1311,13 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
|
||||
def _full_release_worker(
|
||||
self,
|
||||
version: ReleaseVersion,
|
||||
version: ReleaseVersion | None,
|
||||
commit_message: str,
|
||||
notes: str,
|
||||
credentials: dict[str, str],
|
||||
credentials: dict[str, str] | None,
|
||||
*,
|
||||
publish_apk: bool,
|
||||
run_tests: bool,
|
||||
) -> None:
|
||||
self._preflight_git(require_clean=False)
|
||||
self._log_event(
|
||||
@@ -1153,11 +1330,21 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
f"({len(candidate_paths)} changed path(s), including untracked files)"
|
||||
)
|
||||
npm = shutil.which("npm")
|
||||
powershell = shutil.which("powershell.exe") or shutil.which("powershell")
|
||||
if not npm or not powershell:
|
||||
raise ReleaseError("npm and Windows PowerShell must be available on PATH.")
|
||||
if not npm:
|
||||
raise ReleaseError("npm must be available on PATH.")
|
||||
powershell = None
|
||||
if publish_apk:
|
||||
if version is None or credentials is None:
|
||||
raise ReleaseError("APK publication requires version and signing details.")
|
||||
powershell = shutil.which("powershell.exe") or shutil.which("powershell")
|
||||
if not powershell:
|
||||
raise ReleaseError("Windows PowerShell must be available on PATH.")
|
||||
|
||||
managed_files = (PACKAGE_FILE, PACKAGE_LOCK_FILE, RELEASE_VERSION_FILE)
|
||||
managed_files = (
|
||||
(PACKAGE_FILE, PACKAGE_LOCK_FILE, RELEASE_VERSION_FILE)
|
||||
if publish_apk
|
||||
else ()
|
||||
)
|
||||
backups = {
|
||||
path: path.read_bytes() if path.exists() else None for path in managed_files
|
||||
}
|
||||
@@ -1165,16 +1352,32 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
committed = False
|
||||
base_commit = current_commit()
|
||||
try:
|
||||
self._run_command(
|
||||
[npm, "version", version.version_name, "--no-git-tag-version"],
|
||||
"Bump package version",
|
||||
)
|
||||
write_release_version(version)
|
||||
self._log_event(
|
||||
f"Recorded Android version {version.version_name} / {version.version_code}."
|
||||
)
|
||||
if publish_apk:
|
||||
assert version is not None
|
||||
self._run_command(
|
||||
[npm, "version", version.version_name, "--no-git-tag-version"],
|
||||
"Bump package version",
|
||||
)
|
||||
write_release_version(version)
|
||||
self._log_event(
|
||||
f"Recorded Android version {version.version_name} / {version.version_code}."
|
||||
)
|
||||
else:
|
||||
self._log_event(
|
||||
"Git + TrueNAS mode: keeping the current Android version and "
|
||||
"skipping APK publication."
|
||||
)
|
||||
if not run_tests:
|
||||
self._log_event(
|
||||
"⚠ Test suites disabled: skipping content, server, normal-application, "
|
||||
"and shelved-application tests."
|
||||
)
|
||||
|
||||
for command, label in full_release_checks(npm):
|
||||
for command, label in full_release_checks(
|
||||
npm,
|
||||
include_android=publish_apk,
|
||||
include_tests=run_tests,
|
||||
):
|
||||
try:
|
||||
self._run_command(command, label)
|
||||
except ReleaseCommandError as failure:
|
||||
@@ -1221,15 +1424,49 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
if not staged_paths:
|
||||
raise ReleaseError("There are no staged changes to release.")
|
||||
stat = git_output("diff", "--cached", "--stat")
|
||||
if publish_apk:
|
||||
assert version is not None
|
||||
review_heading = (
|
||||
f"Version: {version.version_name}\n"
|
||||
f"Android code: {version.version_code}\n"
|
||||
)
|
||||
review_question = "Commit, push main, and publish the APK?"
|
||||
approval_title = "Review staged release"
|
||||
approval_heading = "Inspect every staged path before publishing"
|
||||
approve_label = "Commit, Push, and Publish"
|
||||
cancel_label = "Cancel release"
|
||||
else:
|
||||
current_android = read_release_version()
|
||||
review_heading = (
|
||||
"Mode: Git + TrueNAS only\n"
|
||||
f"Android version remains: {current_android.version_name} "
|
||||
f"(code {current_android.version_code})\n"
|
||||
)
|
||||
review_question = (
|
||||
"Commit and push main without publishing an APK? The TrueNAS Deploy tab "
|
||||
"will be pinned to the new commit."
|
||||
)
|
||||
approval_title = "Review staged Git + TrueNAS update"
|
||||
approval_heading = "Inspect every staged path before pushing"
|
||||
approve_label = "Commit and Push"
|
||||
cancel_label = "Cancel update"
|
||||
review_heading += (
|
||||
f"Test suites: {'run' if run_tests else 'SKIPPED'}\n"
|
||||
)
|
||||
review = (
|
||||
f"Version: {version.version_name}\n"
|
||||
f"Android code: {version.version_code}\n"
|
||||
f"Commit: {commit_message}\n\n"
|
||||
review_heading
|
||||
+ f"Commit: {commit_message}\n\n"
|
||||
f"Staged paths ({len(staged_paths)}):\n"
|
||||
+ "\n".join(staged_paths)
|
||||
+ f"\n\nDiff summary:\n{stat}\n\nCommit, push main, and publish the APK?"
|
||||
+ f"\n\nDiff summary:\n{stat}\n\n{review_question}"
|
||||
)
|
||||
approved = self._request_approval(
|
||||
review,
|
||||
title=approval_title,
|
||||
heading=approval_heading,
|
||||
approve_label=approve_label,
|
||||
cancel_label=cancel_label,
|
||||
)
|
||||
approved = self._request_approval(review)
|
||||
if not approved:
|
||||
raise ReleaseCancelled("Release stopped at the staged-file review.")
|
||||
|
||||
@@ -1244,7 +1481,11 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
f"Remote main is {remote_commit or 'unknown'}, expected {release_commit}."
|
||||
)
|
||||
self.events.put(("commit", release_commit))
|
||||
self._publish_apk(powershell, version, notes, credentials)
|
||||
if publish_apk:
|
||||
assert powershell is not None
|
||||
assert version is not None
|
||||
assert credentials is not None
|
||||
self._publish_apk(powershell, version, notes, credentials)
|
||||
except Exception:
|
||||
if current_commit() != base_commit:
|
||||
committed = True
|
||||
@@ -1257,18 +1498,31 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
for path, content in backups.items():
|
||||
if content is None:
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
else:
|
||||
path.write_bytes(content)
|
||||
self._log_event("Restored version files because no release commit was created.")
|
||||
if publish_apk:
|
||||
for path, content in backups.items():
|
||||
if content is None:
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
else:
|
||||
path.write_bytes(content)
|
||||
self._log_event(
|
||||
"Restored version files because no release commit was created."
|
||||
)
|
||||
else:
|
||||
self._log_event(
|
||||
"No update commit was created; reviewed files were left in the working tree."
|
||||
)
|
||||
else:
|
||||
self._log_event(
|
||||
"The release commit exists. Version files were kept; use "
|
||||
"'Publish Current APK Only' after correcting the publication problem."
|
||||
)
|
||||
if publish_apk:
|
||||
self._log_event(
|
||||
"The release commit exists. Version files were kept; use "
|
||||
"'Publish Current APK Only' after correcting the publication problem."
|
||||
)
|
||||
else:
|
||||
self._log_event(
|
||||
"The update commit exists locally; resolve the push problem without "
|
||||
"rewriting the commit."
|
||||
)
|
||||
raise
|
||||
|
||||
def _apk_only_worker(
|
||||
@@ -1519,27 +1773,28 @@ def launch_gui(*, smoke_test: bool = False) -> int:
|
||||
self.release_button.configure(state="normal")
|
||||
self.retry_button.configure(state="normal")
|
||||
self.refresh_button.configure(state="normal")
|
||||
self.release_mode_toggle.configure(state="normal")
|
||||
self.run_tests_toggle.configure(state="normal")
|
||||
self.cancel_button.configure(state="disabled")
|
||||
self.keystore_password_var.set("")
|
||||
self.key_password_var.set("")
|
||||
if self.remember_token_var.get() and SAVED_GITEA_TOKEN_FILE.is_file():
|
||||
self.token_var.set(read_saved_gitea_token())
|
||||
if result == "success":
|
||||
self.status_var.set("Release completed")
|
||||
self._append_log("\n✓ Release workflow completed successfully.")
|
||||
self.status_var.set("Workflow completed")
|
||||
self._append_log("\n✓ Workflow completed successfully.")
|
||||
messagebox.showinfo(
|
||||
"Release complete",
|
||||
"Git main and the signed APK release are published.\n\n"
|
||||
"Use the TrueNAS Deploy tab after stopping the app, then the Verify tab after restarting it.",
|
||||
"Workflow complete",
|
||||
detail,
|
||||
parent=self.root,
|
||||
)
|
||||
elif result == "cancelled":
|
||||
self.status_var.set("Release cancelled")
|
||||
self.status_var.set("Workflow cancelled")
|
||||
self._append_log(f"\nCancelled: {detail}")
|
||||
else:
|
||||
self.status_var.set("Release failed")
|
||||
self.status_var.set("Workflow failed")
|
||||
self._append_log(f"\nERROR: {detail}")
|
||||
messagebox.showerror("Release failed", detail, parent=self.root)
|
||||
messagebox.showerror("Workflow failed", detail, parent=self.root)
|
||||
self.refresh()
|
||||
|
||||
def _close(self) -> None:
|
||||
|
||||
Reference in new issue
Block a user