Add ready branch to Docker workflow and refactor self-update to use preloaded version selector
- Add ready branch to Docker publish workflow alongside testing and main - Replace tag search/autocomplete UI with standard select element preloaded with current major version tags - Add get_selector_tag_options helper that filters tags to current major line and returns list of higher major versions available - Show attention banner with Docker update guide link when newer major versions exist on selected
frdel committed
Mar 26, 2026 at 08:45 UTC
27829350aeb8120542cce84631700cfa2931639d
8 files changed
+219
-241
.github/workflows/docker-publish.yml
+2
-1
@@ -4,6 +4,7 @@ on:
4
push:
5
branches:
6
- "testing"
7
+ - "ready"
8
- "main"
9
tags:
10
- "v*"
@@ -16,7 +17,7 @@ on:
17
18
env:
19
# Non-main branches publish a Docker tag with the same name as the branch.
19
- ALLOWED_BRANCHES: "testing main"
20
+ ALLOWED_BRANCHES: "testing ready main"
21
MAIN_BRANCH: "main"
22
RELEASE_TAG_REGEX: "^v([0-9]+)\\.([0-9]+)$"
23
MIN_RELEASE_MAJOR: "1"
api/self_update_tags.py
+7
-8
@@ -7,29 +7,28 @@ from helpers import self_update
7
class SelfUpdateTags(ApiHandler):
8
async def process(self, input: dict, request: Request) -> dict | Response:
9
branch = str(input.get("branch", "")).strip().lower()
10
- query = str(input.get("query", ""))
10
current_branch = self_update.get_repo_version_info().get("branch", "").strip().lower()
11
default_branch = current_branch if current_branch in self_update.SUPPORTED_BRANCHES else "main"
12
+ resolved_branch = branch or default_branch
13
14
try:
15
- tags, error = self_update.get_available_tags(
16
- branch or None,
17
- query=query,
15
+ tags, higher_major_versions, error = self_update.get_selector_tag_options(
16
+ resolved_branch,
17
)
18
return {
19
"success": True,
20
"supported": runtime.is_dockerized(),
22
- "branch": branch or default_branch,
23
- "query": query,
21
+ "branch": resolved_branch,
22
"tags": tags,
23
+ "higher_major_versions": higher_major_versions,
24
"error": error,
25
}
26
except Exception as e:
27
return {
28
"success": False,
29
"supported": runtime.is_dockerized(),
31
- "branch": branch or "main",
32
- "query": query,
30
+ "branch": resolved_branch,
31
"tags": [],
32
+ "higher_major_versions": [],
33
"error": str(e),
34
}
docs/guides/self-update.md
+4
-2
@@ -33,7 +33,9 @@ The updater can create a zip backup of `/a0/usr` before replacing repository fil
33
34
## Version selection
35
36
-The WebUI fetches repository version tags for the selected branch and lets you enter any exact version manually, including downgrades.
36
+The WebUI preloads repository version tags for the selected branch into a standard selector.
37
+
38
+Only tags from the current major release line are listed in the selector. If newer major lines are available on the selected branch, the UI shows an attention banner that links to the Docker update guide.
39
40
Agent Zero version tags follow this format:
41
@@ -50,7 +52,7 @@ Tags below `v1.0` are ignored by the selector and rejected by the self-update re
52
53
Self-update is intentionally limited to changes within the same major line.
54
53
-If the requested version changes the first version number, the UI blocks the update and shows a warning. Those upgrades require downloading a new Docker image because they can include operating system level changes or other breaking changes outside the repository checkout.
55
+If a newer major line exists, the UI points you to the Docker setup guide because those upgrades require downloading a new Docker image. They can include operating system level changes or other breaking changes outside the repository checkout.
56
57
## Safety notes
58
helpers/self_update.py
+44
-1
@@ -286,6 +286,13 @@ def is_valid_selector_tag(tag: str) -> bool:
286
return _parse_selector_version(tag) is not None
287
288
289
+def _parse_major_version(tag: str) -> int | None:
290
+ match = re.fullmatch(r"v(\d+)(?:[.-].*)?", tag.strip())
291
+ if not match:
292
+ return None
293
+ return int(match.group(1))
294
+
295
+
296
def get_available_tags(
297
branch: str | None = None,
298
*,
@@ -311,13 +318,48 @@ def get_available_tags(
318
return tags, ""
319
320
321
+def get_selector_tag_options(
322
+ branch: str | None = None,
323
+ *,
324
+ repo_dir: str | Path | None = None,
325
+ current_version: str | None = None,
326
+) -> tuple[list[str], list[int], str]:
327
+ repository = get_repo_dir(repo_dir)
328
+ tags, error = get_available_tags(branch, repo_dir=repository)
329
+ if error:
330
+ return [], [], error
331
+
332
+ current_major = _parse_major_version(
333
+ current_version or get_repo_version_info(repository)["short_tag"]
334
+ )
335
+ if current_major is None:
336
+ return tags, [], ""
337
+
338
+ same_major_tags: list[str] = []
339
+ higher_major_versions: set[int] = set()
340
+ for tag in tags:
341
+ tag_major = _parse_major_version(tag)
342
+ if tag_major is None:
343
+ continue
344
+ if tag_major == current_major:
345
+ same_major_tags.append(tag)
346
+ elif tag_major > current_major:
347
+ higher_major_versions.add(tag_major)
348
+
349
+ return same_major_tags, sorted(higher_major_versions), ""
350
+
351
+
352
def get_update_info(repo_dir: str | Path | None = None) -> dict[str, Any]:
353
repository = get_repo_dir(repo_dir)
354
version_info = get_repo_version_info(repository)
355
current_version = version_info["short_tag"]
356
current_branch = version_info.get("branch", "").strip().lower()
357
default_branch = current_branch if current_branch in SUPPORTED_BRANCHES else "main"
320
- tags, tags_error = get_available_tags(default_branch, repo_dir=repository)
358
+ tags, higher_major_versions, tags_error = get_selector_tag_options(
359
+ default_branch,
360
+ repo_dir=repository,
361
+ current_version=current_version,
362
+ )
363
return {
364
"repo_dir": str(repository),
365
"current": version_info,
@@ -326,6 +368,7 @@ def get_update_info(repo_dir: str | Path | None = None) -> dict[str, Any]:
368
"branches": BRANCH_OPTIONS,
369
"available_tags": tags,
370
"available_tags_error": tags_error,
371
+ "available_higher_major_versions": higher_major_versions,
372
"paths": {
373
"update_file": str(get_update_file_path()),
374
"status_file": str(get_status_file_path()),
tests/test_self_update_tag_filter.py
+41
-7
@@ -66,7 +66,27 @@ def test_self_update_branch_filter_prefers_remote_branch_tags(monkeypatch):
66
assert tags == ["v1.1", "v1.0"]
67
68
69
-def test_self_update_frontend_filters_old_tag_suggestions():
69
+def test_self_update_selector_tag_options_filter_to_current_major(monkeypatch):
70
+ monkeypatch.setattr(
71
+ self_update,
72
+ "get_available_tags",
73
+ lambda branch, *, repo_dir=None, query="": (
74
+ ["v3.0", "v2.1", "v1.4", "v1.2"],
75
+ "",
76
+ ),
77
+ )
78
+
79
+ tags, higher_major_versions, error = self_update.get_selector_tag_options(
80
+ "main",
81
+ current_version="v1.2",
82
+ )
83
+
84
+ assert error == ""
85
+ assert tags == ["v1.4", "v1.2"]
86
+ assert higher_major_versions == [2, 3]
87
+
88
+
89
+def test_self_update_frontend_uses_preloaded_select():
90
store_path = (
91
PROJECT_ROOT
92
/ "webui"
@@ -77,20 +97,29 @@ def test_self_update_frontend_filters_old_tag_suggestions():
97
)
98
content = store_path.read_text(encoding="utf-8")
99
100
+ assert 'const SELF_UPDATE_MANUAL_BACKUP_MODAL_PATH = "settings/backup/backup_restore.html";' in content
101
assert "const MIN_SELECTOR_VERSION = [1, 0];" in content
81
- assert "response.tags.filter((tag) => this.isSupportedSuggestionTag(tag))" in content
102
+ assert "availableTags: []" in content
103
+ assert "higherMajorVersions: []" in content
104
+ assert "this.applyAvailableTags({" in content
105
+ assert "response.available_tags" in content
106
+ assert "response.available_higher_major_versions" in content
107
+ assert "response.higher_major_versions" in content
108
+ assert "await this.fetchTags();" in content
109
assert "Release tag must use the format vX.Y." in content
110
assert "Release tag must be v1.0 or newer." in content
111
assert "this.info?.defaults?.branch ||" in content
85
- assert "Version ${tag} does not exist on branch" in content
112
+ assert "Version ${this.trimmedTag} does not exist on branch" in content
113
assert "this.selectedTagExistsOnBranch" in content
114
assert 'const response = await fetch("/api/health"' in content
115
assert "if (response.ok && observedBackendUnavailable)" in content
116
assert "Waiting for Agent Zero to disconnect before reloading the page." in content
117
assert "/api/csrf_token" not in content
118
+ assert "tagSuggestions" not in content
119
+ assert "tagDropdownOpen" not in content
120
121
93
-def test_self_update_modal_validates_exact_tag_on_blur():
122
+def test_self_update_modal_uses_standard_select_and_manual_backup():
123
modal_path = (
124
PROJECT_ROOT
125
/ "webui"
@@ -101,10 +130,15 @@ def test_self_update_modal_validates_exact_tag_on_blur():
130
)
131
content = modal_path.read_text(encoding="utf-8")
132
104
- assert '@blur="$store.selfUpdateStore.onTagBlur()"' in content
105
- assert '@mousedown.prevent="$store.selfUpdateStore.selectTag(tag)"' in content
106
- assert "$store.selfUpdateStore.tagExistenceWarning" in content
133
+ assert 'x-model="$store.selfUpdateStore.form.tag"' in content
134
+ assert "$store.selfUpdateStore.versionSelectPlaceholder" in content
135
+ assert "$store.selfUpdateStore.higherMajorVersionMessage" in content
136
+ assert "Docker update guide" in content
137
+ assert "https://www.agent-zero.ai/p/docs/get-started/" in content
138
+ assert "Manual backup" in content
139
assert 'type="button"' in content
140
+ assert "@blur" not in content
141
+ assert "selectTag(tag)" not in content
142
143
144
def test_self_update_schedule_rejects_missing_tag_on_branch(monkeypatch, tmp_path):
webui/components/settings/backup/backup_restore.html
+2
-2
@@ -21,7 +21,7 @@
21
<div class="field-control">
22
<button
23
class="btn btn-field"
24
- @click="$store.settings.handleFieldButton({ id: 'backup_create' })"
24
+ @click="window.openModal('settings/backup/backup.html')"
25
>
26
Create Backup
27
</button>
@@ -38,7 +38,7 @@
38
<div class="field-control">
39
<button
40
class="btn btn-field"
41
- @click="$store.settings.handleFieldButton({ id: 'backup_restore' })"
41
+ @click="window.openModal('settings/backup/restore.html')"
42
>
43
Restore Backup
44
</button>
webui/components/settings/external/self-update-modal.html
+52
-114
@@ -125,79 +125,42 @@
125
<div class="field-label">
126
<div class="field-title">Version</div>
127
<div class="field-description">
128
- Enter a version tag from the
128
+ Choose a preloaded version tag from the
129
<a href="https://github.com/agent0ai/agent-zero" target="_blank" rel="noreferrer">Agent Zero repository</a>.
130
- Start typing to filter, or use the search button to fetch versions from the selected branch. Only <code>vX.Y</code> tags at <code>v1.0</code> or newer are supported. Exact text entry is still allowed, including downgrades.
130
+ Only tags from the current major release line are listed here. Newer major lines require a Docker image update first.
131
</div>
132
<template x-if="$store.selfUpdateStore.tagsError">
133
<div class="field-description">
134
- Tag lookup failed:
134
+ Version lookup failed:
135
<span x-text="$store.selfUpdateStore.tagsError"></span>
136
</div>
137
</template>
138
</div>
139
- <div class="field-control self-update-tag-search" @click.outside="$store.selfUpdateStore.closeTagDropdown()">
140
- <input
141
- type="text"
139
+ <template x-if="$store.selfUpdateStore.higherMajorVersionMessage">
140
+ <div class="self-update-warning-banner">
141
+ <div x-text="$store.selfUpdateStore.higherMajorVersionMessage"></div>
142
+ <a
143
+ href="https://www.agent-zero.ai/p/docs/get-started/"
144
+ target="_blank"
145
+ rel="noreferrer"
146
+ >
147
+ Docker update guide
148
+ </a>
149
+ </div>
150
+ </template>
151
+ <div class="field-control">
152
+ <select
153
x-model="$store.selfUpdateStore.form.tag"
143
- @input="$store.selfUpdateStore.onTagInput()"
144
- @focus="$store.selfUpdateStore.openTagDropdown()"
145
- @blur="$store.selfUpdateStore.onTagBlur()"
146
- @keydown.enter.prevent="$store.selfUpdateStore.searchTags()"
147
- placeholder="v1.0"
148
- :disabled="$store.selfUpdateStore.isBusy"
149
- style="padding-right: 32px;"
150
- />
151
- <span
152
- class="material-symbols-outlined self-update-tag-search-btn"
153
- :class="{ 'spinning': $store.selfUpdateStore.tagsLoading }"
154
- @click="if (!$store.selfUpdateStore.isBusy) $store.selfUpdateStore.searchTags()"
155
- title="Search tags from selected branch"
156
- x-text="$store.selfUpdateStore.tagsLoading ? 'progress_activity' : 'search'"
157
- ></span>
158
- <div
159
- class="self-update-tag-results"
160
- x-show="$store.selfUpdateStore.tagDropdownOpen"
161
- x-transition.opacity
154
+ :disabled="$store.selfUpdateStore.isBusy || $store.selfUpdateStore.tagsLoading || !$store.selfUpdateStore.hasAvailableTags"
155
>
163
- <template x-if="$store.selfUpdateStore.tagsLoading">
164
- <div class="self-update-tag-item disabled">Loading tags...</div>
165
- </template>
166
- <template x-if="!$store.selfUpdateStore.tagsLoading && $store.selfUpdateStore.tagSuggestions.length === 0">
167
- <div class="self-update-tag-item disabled">No matching tags found</div>
168
- </template>
169
- <template x-for="tag in $store.selfUpdateStore.groupedTagSuggestions.matched" :key="`match-${tag}`">
170
- <div
171
- class="self-update-tag-item"
172
- @mousedown.prevent="$store.selfUpdateStore.selectTag(tag)"
173
- x-text="tag"
174
- ></div>
175
- </template>
176
- <div
177
- class="self-update-tag-separator"
178
- x-show="$store.selfUpdateStore.groupedTagSuggestions.matched.length > 0 && $store.selfUpdateStore.groupedTagSuggestions.rest.length > 0"
179
- >
180
- Other versions
181
- </div>
182
- <template x-for="tag in $store.selfUpdateStore.groupedTagSuggestions.rest" :key="`rest-${tag}`">
183
- <div
184
- class="self-update-tag-item"
185
- @mousedown.prevent="$store.selfUpdateStore.selectTag(tag)"
186
- x-text="tag"
187
- ></div>
156
+ <option value="" x-text="$store.selfUpdateStore.versionSelectPlaceholder"></option>
157
+ <template x-for="tag in $store.selfUpdateStore.availableTags" :key="tag">
158
+ <option :value="tag" x-text="tag"></option>
159
</template>
189
- </div>
160
+ </select>
161
</div>
162
</div>
163
193
- <div class="self-update-warning-banner" x-show="$store.selfUpdateStore.versionCompatibilityWarning">
194
- <span x-text="$store.selfUpdateStore.versionCompatibilityWarning"></span>
195
- </div>
196
-
197
- <div class="self-update-warning-banner" x-show="$store.selfUpdateStore.tagExistenceWarning">
198
- <span x-text="$store.selfUpdateStore.tagExistenceWarning"></span>
199
- </div>
200
-
164
<div class="field">
165
<div class="field-label">
166
<div class="field-title">Back up <code>/a0/usr</code> first</div>
@@ -234,6 +197,25 @@
197
</div>
198
</div>
199
200
+ <div class="field">
201
+ <div class="field-label">
202
+ <div class="field-title">Manual backup</div>
203
+ <div class="field-description">
204
+ Open the existing backup and restore modal if you want to create a backup before scheduling the update.
205
+ </div>
206
+ </div>
207
+ <div class="field-control">
208
+ <button
209
+ type="button"
210
+ class="btn btn-field"
211
+ @click="$store.selfUpdateStore.openManualBackupModal()"
212
+ :disabled="$store.selfUpdateStore.isBusy"
213
+ >
214
+ Manual backup
215
+ </button>
216
+ </div>
217
+ </div>
218
+
219
<div class="field">
220
<div class="field-label">
221
<div class="field-title">Backup filename</div>
@@ -331,61 +313,6 @@
313
gap: 1rem;
314
}
315
334
- .self-update-tag-search {
335
- position: relative;
336
- }
337
-
338
- .self-update-tag-search-btn {
339
- position: absolute;
340
- right: 0.5rem;
341
- top: 0.55rem;
342
- cursor: pointer;
343
- color: var(--color-text-muted);
344
- display: inline-flex;
345
- align-items: center;
346
- justify-content: center;
347
- }
348
-
349
- .self-update-tag-results {
350
- position: absolute;
351
- inset: calc(100% + 0.35rem) 0 auto 0;
352
- max-height: 14rem;
353
- overflow-y: auto;
354
- z-index: 20;
355
- border: 1px solid var(--color-border);
356
- border-radius: 0.75rem;
357
- background: var(--color-panel);
358
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
359
- }
360
-
361
- .self-update-tag-item {
362
- padding: 0.65rem 0.85rem;
363
- cursor: pointer;
364
- color: var(--color-text);
365
- }
366
-
367
- .self-update-tag-item:hover {
368
- background: var(--color-background-hover);
369
- }
370
-
371
- .self-update-tag-item.disabled {
372
- cursor: default;
373
- color: var(--color-text-muted);
374
- }
375
-
376
- .self-update-tag-item.disabled:hover {
377
- background: transparent;
378
- }
379
-
380
- .self-update-tag-separator {
381
- padding: 0.45rem 0.85rem;
382
- border-top: 1px solid var(--color-border);
383
- color: var(--color-text-muted);
384
- font-size: 0.85rem;
385
- text-transform: uppercase;
386
- letter-spacing: 0.04em;
387
- }
388
-
316
.self-update-copy {
317
color: var(--color-text-muted);
318
line-height: 1.5;
@@ -459,6 +386,11 @@
386
}
387
388
.self-update-warning-banner {
389
+ display: flex;
390
+ flex-wrap: wrap;
391
+ align-items: flex-start;
392
+ justify-content: space-between;
393
+ gap: 0.75rem;
394
padding: 0.85rem 0.95rem;
395
border: 1px solid var(--color-warning-text);
396
border-radius: 0.8rem;
@@ -467,6 +399,12 @@
399
line-height: 1.5;
400
}
401
402
+ .self-update-warning-banner a {
403
+ color: inherit;
404
+ font-weight: 700;
405
+ text-decoration: underline;
406
+ }
407
+
408
.self-update-summary-grid {
409
display: grid;
410
gap: 0.75rem;
webui/components/settings/external/self-update-store.js
+67
-106
@@ -8,6 +8,7 @@ const HEALTH_WAIT_BUFFER_MS = 30000;
8
const SELF_UPDATE_RETURN_URL_KEY = "a0:self-update:return-url";
9
const SELF_UPDATE_OVERLAY_ID = "self-update-progress-overlay";
10
const SELF_UPDATE_MODAL_PATH = "settings/external/self-update-modal.html";
11
+const SELF_UPDATE_MANUAL_BACKUP_MODAL_PATH = "settings/backup/backup_restore.html";
12
const MIN_SELECTOR_VERSION = [1, 0];
13
14
const model = {
@@ -18,9 +19,8 @@ const model = {
19
error: "",
20
tagsError: "",
21
info: null,
21
- tagSuggestions: [],
22
- tagDropdownOpen: false,
23
- tagExistenceChecked: false,
22
+ availableTags: [],
23
+ higherMajorVersions: [],
24
restartStatusText: "",
25
restartDetailText: "",
26
form: {
@@ -50,63 +50,33 @@ const model = {
50
return this.info?.current?.branch || "";
51
},
52
53
- get groupedTagSuggestions() {
54
- const tags = Array.isArray(this.tagSuggestions) ? this.tagSuggestions : [];
55
- const query = (this.form.tag || "").trim().toLowerCase();
56
- if (!query) {
57
- return { matched: [], rest: tags };
58
- }
59
-
60
- const matched = [];
61
- const rest = [];
62
- for (const tag of tags) {
63
- if ((tag || "").toLowerCase().includes(query)) {
64
- matched.push(tag);
65
- } else {
66
- rest.push(tag);
67
- }
68
- }
69
- return { matched, rest };
70
- },
71
-
53
get trimmedTag() {
54
return (this.form.tag || "").trim();
55
},
56
57
+ get hasAvailableTags() {
58
+ return this.availableTags.length > 0;
59
+ },
60
+
61
get selectedTagExistsOnBranch() {
62
const tag = this.trimmedTag;
78
- return Boolean(tag) && this.tagSuggestions.includes(tag);
63
+ return Boolean(tag) && this.availableTags.includes(tag);
64
},
65
81
- get versionCompatibilityWarning() {
82
- const current = this.parseCompatibilityTag(this.currentVersion);
83
- const target = this.parseCompatibilityTag(this.form.tag);
84
- if (!current || !target) return "";
85
- if (current.major !== target.major) {
86
- return (
87
- "Changing the first version number requires downloading a new Docker image, " +
88
- "because those releases can include operating system level changes or other breaking changes."
89
- );
90
- }
91
- return "";
66
+ get higherMajorVersionMessage() {
67
+ if (!this.higherMajorVersions.length) return "";
68
+ const versionLabels = this.higherMajorVersions.map((major) => `v${major}.x`);
69
+ const versionText =
70
+ versionLabels.length === 1
71
+ ? versionLabels[0]
72
+ : `${versionLabels.slice(0, -1).join(", ")} and ${versionLabels[versionLabels.length - 1]}`;
73
+ return `A newer major release line is available on this branch (${versionText}). Major upgrades require downloading a newer Docker image before using self-update.`;
74
},
75
94
- get tagExistenceWarning() {
95
- const tag = this.trimmedTag;
96
- if (
97
- !this.tagExistenceChecked ||
98
- this.tagsLoading ||
99
- this.tagsError ||
100
- !tag ||
101
- !this.parseSelectorTag(tag) ||
102
- !this.isSupportedSelectorTag(tag)
103
- ) {
104
- return "";
105
- }
106
- if (!this.selectedTagExistsOnBranch) {
107
- return `Version ${tag} does not exist on branch ${this.form.branch || "main"}.`;
108
- }
109
- return "";
76
+ get versionSelectPlaceholder() {
77
+ if (this.tagsLoading) return "Loading versions...";
78
+ if (!this.hasAvailableTags) return "No versions available";
79
+ return "Select a version";
80
},
81
82
get canScheduleUpdate() {
@@ -114,9 +84,9 @@ const model = {
84
this.isSupported &&
85
!this.isBusy &&
86
!this.tagsLoading &&
87
+ this.hasAvailableTags &&
88
this.isSupportedSelectorTag(this.form.tag) &&
118
- this.selectedTagExistsOnBranch &&
119
- !this.versionCompatibilityWarning
89
+ this.selectedTagExistsOnBranch
90
);
91
},
92
@@ -132,8 +102,8 @@ const model = {
102
this.saving = false;
103
this.restarting = false;
104
this.tagsLoading = false;
135
- this.tagDropdownOpen = false;
136
- this.tagExistenceChecked = false;
105
+ this.availableTags = [];
106
+ this.higherMajorVersions = [];
107
this.restartStatusText = "";
108
this.restartDetailText = "";
109
this.removeProgressOverlay();
@@ -301,8 +271,11 @@ const model = {
271
}
272
this.info = response;
273
this.applyFormState(response.pending || response.defaults || {});
304
- this.tagExistenceChecked = Boolean((this.form.tag || "").trim());
305
- await this.searchTags({ openResults: false });
274
+ this.applyAvailableTags({
275
+ tags: response.available_tags,
276
+ higherMajorVersions: response.available_higher_major_versions,
277
+ error: response.available_tags_error,
278
+ });
279
} catch (error) {
280
console.error("Failed to load self-update info:", error);
281
this.error = error.message || "Failed to load self-update info.";
@@ -313,12 +286,14 @@ const model = {
286
287
applyFormState(source) {
288
this.form.branch =
289
+ source?.branch ||
290
this.info?.defaults?.branch ||
291
this.currentBranch ||
318
- source?.branch ||
292
"main";
293
this.form.tag =
321
- typeof source?.tag === "string" ? source.tag : this.currentVersion;
294
+ typeof source?.tag === "string"
295
+ ? source.tag
296
+ : this.info?.defaults?.tag || this.currentVersion;
297
this.form.backup_usr =
298
typeof source?.backup_usr === "boolean" ? source.backup_usr : true;
299
this.form.backup_path = source?.backup_path || "";
@@ -327,35 +302,43 @@ const model = {
302
source?.backup_conflict_policy || "rename";
303
},
304
330
- closeTagDropdown() {
331
- this.tagDropdownOpen = false;
305
+ applyAvailableTags({ tags = [], higherMajorVersions = [], error = "" } = {}) {
306
+ this.availableTags = Array.isArray(tags) ? tags : [];
307
+ this.higherMajorVersions = Array.isArray(higherMajorVersions)
308
+ ? higherMajorVersions
309
+ : [];
310
+ this.tagsError = error || "";
311
+
312
+ if (!this.availableTags.length) {
313
+ this.form.tag = "";
314
+ return;
315
+ }
316
+
317
+ const preferredTag = this.trimmedTag;
318
+ if (preferredTag && this.availableTags.includes(preferredTag)) {
319
+ return;
320
+ }
321
+
322
+ const defaultTag = (this.info?.defaults?.tag || "").trim();
323
+ this.form.tag =
324
+ defaultTag && this.availableTags.includes(defaultTag)
325
+ ? defaultTag
326
+ : this.availableTags[0];
327
},
328
329
async openModal() {
330
await openModal(SELF_UPDATE_MODAL_PATH);
331
},
332
338
- openTagDropdown() {
339
- this.tagDropdownOpen = true;
333
+ async openManualBackupModal() {
334
+ await openModal(SELF_UPDATE_MANUAL_BACKUP_MODAL_PATH);
335
},
336
337
async onBranchChanged() {
343
- this.openTagDropdown();
344
- this.tagExistenceChecked = Boolean(this.trimmedTag);
345
- await this.searchTags();
338
+ await this.fetchTags();
339
},
340
348
- onTagInput() {
349
- this.tagExistenceChecked = false;
350
- this.openTagDropdown();
351
- },
352
-
353
- async onTagBlur() {
354
- this.tagExistenceChecked = Boolean(this.trimmedTag);
355
- await this.searchTags({ openResults: false });
356
- },
357
-
358
- async searchTags({ openResults = true } = {}) {
341
+ async fetchTags() {
342
const requestId = ++this._tagRequestId;
343
this.tagsLoading = true;
344
this.tagsError = "";
@@ -363,7 +346,6 @@ const model = {
346
try {
347
const response = await API.callJsonApi("self_update_tags", {
348
branch: this.form.branch,
366
- query: "",
349
});
350
if (!response?.success) {
351
throw new Error(response?.error || "Failed to fetch release tags.");
@@ -371,19 +353,18 @@ const model = {
353
if (requestId !== this._tagRequestId) {
354
return;
355
}
374
- this.tagSuggestions = Array.isArray(response?.tags)
375
- ? response.tags.filter((tag) => this.isSupportedSuggestionTag(tag))
376
- : [];
377
- this.tagsError = response?.error || "";
378
- this.tagDropdownOpen = openResults;
356
+ this.applyAvailableTags({
357
+ tags: response.tags,
358
+ higherMajorVersions: response.higher_major_versions,
359
+ error: response.error,
360
+ });
361
} catch (error) {
362
console.error("Failed to fetch self-update tags:", error);
363
if (requestId !== this._tagRequestId) {
364
return;
365
}
384
- this.tagSuggestions = [];
366
+ this.applyAvailableTags();
367
this.tagsError = error.message || "Failed to fetch release tags.";
386
- this.tagDropdownOpen = openResults;
368
} finally {
369
if (requestId === this._tagRequestId) {
370
this.tagsLoading = false;
@@ -391,12 +372,6 @@ const model = {
372
}
373
},
374
394
- selectTag(tag) {
395
- this.form.tag = tag;
396
- this.tagExistenceChecked = true;
397
- this.tagDropdownOpen = false;
398
- },
399
-
375
parseSelectorTag(value) {
376
const match = /^v(\d+)\.(\d+)$/.exec((value || "").trim());
377
if (!match) return null;
@@ -406,10 +381,6 @@ const model = {
381
];
382
},
383
409
- isSupportedSuggestionTag(value) {
410
- return this.isSupportedSelectorTag(value);
411
- },
412
-
384
isSupportedSelectorTag(value) {
385
const parsed = this.parseSelectorTag(value);
386
if (!parsed) return false;
@@ -420,15 +391,6 @@ const model = {
391
return true;
392
},
393
423
- parseCompatibilityTag(value) {
424
- const match = /^v(\d+)\.(\d+)(?:\..+)?$/.exec((value || "").trim());
425
- if (!match) return null;
426
- return {
427
- major: Number.parseInt(match[1], 10),
428
- minor: Number.parseInt(match[2], 10),
429
- };
430
- },
431
-
394
async scheduleUpdate() {
395
if (!this.form.branch?.trim()) {
396
this.error = "Choose a branch.";
@@ -436,7 +398,7 @@ const model = {
398
}
399
400
if (!this.form.tag?.trim()) {
439
- this.error = "Enter a release tag to schedule.";
401
+ this.error = "Choose a version from the list.";
402
return;
403
}
404
@@ -451,8 +413,7 @@ const model = {
413
}
414
415
if (!this.selectedTagExistsOnBranch) {
454
- this.tagExistenceChecked = true;
455
- await this.searchTags({ openResults: false });
416
+ await this.fetchTags();
417
if (!this.selectedTagExistsOnBranch) {
418
this.error = `Version ${this.trimmedTag} does not exist on branch ${this.form.branch || "main"}.`;
419
return;