chore: release 26.9-alpha
All checks were successful
Build ISO / build-iso (push) Successful in 20m49s
CI / lint (push) Successful in 1m13s
CI / test (push) Successful in 48s
CI / validate-json (push) Successful in 44s
CI / markdown-links (push) Successful in 16s
Release / release (push) Successful in 13m31s

Three small fixes surfaced by the 26.8 QA pass on fresh VM .161:

- Landing-page app tiles now open external `open_url` links in a new
  tab, matching /apps Open-button behaviour. Without this a Kuma click
  on the home screen replaced Furtka itself.
- `scripts/publish-release.sh` treats the ISO upload as best-effort;
  a Forgejo-proxy 504 no longer kills the whole release after tarball
  + sha + release.json are already uploaded.
- `furtka app list --json` now mirrors /api/apps — includes
  `description_long`, `open_url`, and `settings` that the previous
  slim projection dropped.
This commit is contained in:
Daniel Maksymilian Syrnicki 2026-04-20 18:51:30 +02:00
parent cf93ef44cb
commit c7e7c8b1e5
5 changed files with 73 additions and 7 deletions

View file

@ -7,6 +7,31 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r
## [Unreleased] ## [Unreleased]
## [26.9-alpha] - 2026-04-21
### Fixed
- Landing-page app tiles with an `open_url` now open in a new tab
(`target="_blank" rel="noopener"`), matching the Open button
behaviour on `/apps`. Without this, clicking "Uptime Kuma" on the
home screen replaced Furtka itself with the Kuma admin page.
Internal links (the `Manage →` fallback for apps without an
`open_url`) still open in the same tab.
- `scripts/publish-release.sh` no longer fails the whole release when
the ISO upload hits a Forgejo proxy 504. The core tarball + sha256 +
release.json (which running boxes need for self-update) are uploaded
first and the ISO is attempted last as a best-effort; a 504 now logs
a warning and exits 0 so the release page still publishes. Surfaced
by the 26.8-alpha cut: the tarball landed but the ~1 GB ISO upload
timed out at the Forgejo reverse proxy.
### Changed
- `furtka app list --json` now mirrors `/api/apps` field-for-field —
previously the CLI emitted a slim projection missing
`description_long`, `open_url`, and `settings`. Anyone piping the
CLI output into jq for automation was seeing an incomplete view.
## [26.8-alpha] - 2026-04-20 ## [26.8-alpha] - 2026-04-20
### Added ### Added
@ -130,7 +155,8 @@ First tagged snapshot. Pre-alpha — the installer does not yet boot, but the de
- **Containers:** Docker + Compose - **Containers:** Docker + Compose
- **License:** AGPL-3.0 - **License:** AGPL-3.0
[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka/compare/26.8-alpha...HEAD [Unreleased]: https://forgejo.sourcegate.online/daniel/furtka/compare/26.9-alpha...HEAD
[26.9-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.9-alpha
[26.8-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.8-alpha [26.8-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.8-alpha
[26.6-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.6-alpha [26.6-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.6-alpha
[26.5-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.5-alpha [26.5-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.5-alpha

View file

@ -100,9 +100,9 @@
// frontend fall back to /apps for management. // frontend fall back to /apps for management.
if (app.open_url) { if (app.open_url) {
const host = HOSTNAME || location.hostname; const host = HOSTNAME || location.hostname;
return { href: app.open_url.replace('{host}', host), label: 'Open' }; return { href: app.open_url.replace('{host}', host), label: 'Open', external: true };
} }
return { href: '/apps', label: 'Manage →' }; return { href: '/apps', label: 'Manage →', external: false };
} }
async function renderApps() { async function renderApps() {
@ -119,8 +119,9 @@
} }
target.innerHTML = apps.map(a => { target.innerHTML = apps.map(a => {
const icon = a.icon_svg || FALLBACK_ICON; const icon = a.icon_svg || FALLBACK_ICON;
const { href, label } = primaryAction(a); const { href, label, external } = primaryAction(a);
return `<a class="app-tile" href="${esc(href)}"> const tgt = external ? ' target="_blank" rel="noopener"' : '';
return `<a class="app-tile" href="${esc(href)}"${tgt}>
<div class="icon">${icon}</div> <div class="icon">${icon}</div>
<span class="name">${esc(a.display_name || a.name)}</span> <span class="name">${esc(a.display_name || a.name)}</span>
<span class="cta">${esc(label)}</span> <span class="cta">${esc(label)}</span>

View file

@ -21,9 +21,22 @@ def _cmd_app_list(args: argparse.Namespace) -> int:
"display_name": r.manifest.display_name, "display_name": r.manifest.display_name,
"version": r.manifest.version, "version": r.manifest.version,
"description": r.manifest.description, "description": r.manifest.description,
"description_long": r.manifest.description_long,
"volumes": list(r.manifest.volumes), "volumes": list(r.manifest.volumes),
"ports": list(r.manifest.ports), "ports": list(r.manifest.ports),
"icon": r.manifest.icon, "icon": r.manifest.icon,
"open_url": r.manifest.open_url,
"settings": [
{
"name": s.name,
"label": s.label,
"description": s.description,
"type": s.type,
"required": s.required,
"default": s.default,
}
for s in r.manifest.settings
],
} }
if r.manifest if r.manifest
else None, else None,

View file

@ -103,9 +103,16 @@ upload_asset "$RELEASE_JSON"
# exists. Release workflows that want this build the ISO via iso/build.sh # exists. Release workflows that want this build the ISO via iso/build.sh
# and move the output here before calling publish-release. Local runs # and move the output here before calling publish-release. Local runs
# that skip the ISO step still publish the core release successfully. # that skip the ISO step still publish the core release successfully.
#
# Soft-fail: the ISO is ~1 GB and Forgejo's reverse proxy has returned
# 504 on the upload even when the write eventually succeeds. The core
# tarball (which boxes need for self-update) is already uploaded above,
# so don't let an ISO transport hiccup fail the whole release.
ISO="$DIST_DIR/furtka-$VERSION.iso" ISO="$DIST_DIR/furtka-$VERSION.iso"
if [ -f "$ISO" ]; then if [ -f "$ISO" ]; then
upload_asset "$ISO" if ! upload_asset "$ISO"; then
echo "warning: ISO upload failed — release published without ISO asset" >&2
fi
fi fi
echo "Release $VERSION published: https://$HOST/$REPO/releases/tag/$VERSION" echo "Release $VERSION published: https://$HOST/$REPO/releases/tag/$VERSION"

View file

@ -32,9 +32,21 @@ def test_app_list_json_with_one_app(tmp_path, monkeypatch, capsys):
"display_name": "Network Files", "display_name": "Network Files",
"version": "0.1.0", "version": "0.1.0",
"description": "SMB", "description": "SMB",
"description_long": "Long description here.",
"volumes": ["files"], "volumes": ["files"],
"ports": [445], "ports": [445],
"icon": "icon.svg", "icon": "icon.svg",
"open_url": "smb://{host}/files",
"settings": [
{
"name": "SMB_USER",
"label": "User",
"description": "SMB user",
"type": "text",
"default": "furtka",
"required": True,
}
],
} }
) )
) )
@ -43,7 +55,14 @@ def test_app_list_json_with_one_app(tmp_path, monkeypatch, capsys):
data = json.loads(capsys.readouterr().out) data = json.loads(capsys.readouterr().out)
assert len(data) == 1 assert len(data) == 1
assert data[0]["ok"] is True assert data[0]["ok"] is True
assert data[0]["manifest"]["name"] == "fileshare" m = data[0]["manifest"]
assert m["name"] == "fileshare"
assert m["description_long"] == "Long description here."
assert m["open_url"] == "smb://{host}/files"
assert len(m["settings"]) == 1
assert m["settings"][0]["name"] == "SMB_USER"
assert m["settings"][0]["required"] is True
assert m["settings"][0]["default"] == "furtka"
def test_reconcile_dry_run_empty(tmp_path, monkeypatch, capsys): def test_reconcile_dry_run_empty(tmp_path, monkeypatch, capsys):