Compare commits

...

12 Commits

Author SHA1 Message Date
garage-ui-release-bot[bot] b28e975a56 chore(main): release 0.8.5 (#86)
Co-authored-by: garage-ui-release-bot[bot] <285030457+garage-ui-release-bot[bot]@users.noreply.github.com>
2026-07-03 11:13:32 +02:00
Noste 430d1a5d68 fix(frontend): add downloadObject function for downloading files from a bucket 2026-07-03 11:10:33 +02:00
dependabot[bot] 460793632e chore(deps): bump esbuild, @vitejs/plugin-react and vite in /frontend (#79)
Removes [esbuild](https://github.com/evanw/esbuild). It's no longer used after updating ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies need to be updated together.


Removes `esbuild`

Updates `@vitejs/plugin-react` from 5.1.2 to 5.2.0
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/plugin-react@5.2.0/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.2.0/packages/plugin-react)

Updates `vite` from 7.3.2 to 8.0.16
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version:
  dependency-type: indirect
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.2.0
  dependency-type: direct:development
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 16:33:57 +02:00
dependabot[bot] f888b7c77c chore(deps): bump js-yaml from 4.1.1 to 4.2.0 in /frontend (#83)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 16:33:41 +02:00
dependabot[bot] ef118dc3a7 chore(deps): bump form-data from 4.0.5 to 4.0.6 in /frontend (#82)
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.5 to 4.0.6.
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.5...v4.0.6)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 16:33:20 +02:00
garage-ui-release-bot[bot] 34ba8ef851 chore(main): release 0.8.4 (#84)
Co-authored-by: garage-ui-release-bot[bot] <285030457+garage-ui-release-bot[bot]@users.noreply.github.com>
2026-06-22 15:32:55 +02:00
Noste 1ffdfe83c8 fix(helm): track appVersion in release-please and fix badges 2026-06-22 15:28:41 +02:00
garage-ui-release-bot[bot] e0cd59dcdf chore(main): release 0.8.3 (#81)
Co-authored-by: garage-ui-release-bot[bot] <285030457+garage-ui-release-bot[bot]@users.noreply.github.com>
2026-06-22 12:23:53 +02:00
Noooste 1c1e4d4b2b chore: update Helm chart version to 0.8.2 and adjust README badges
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
2026-06-21 23:20:44 +02:00
Noooste ae245c8a31 Merge remote-tracking branch 'origin/main' 2026-06-21 22:58:38 +02:00
Noooste 28c186f3eb fix(helm): update version badges in README for Garage UI
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
2026-06-21 22:58:32 +02:00
Noste 46aa3752c8 fix(backend): improve API version detection with retry logic for health probes
Signed-off-by: Noste <83548733+Noooste@users.noreply.github.com>
2026-06-17 19:14:06 +02:00
14 changed files with 779 additions and 976 deletions
+2 -1
View File
@@ -3,7 +3,8 @@ name: chart-release
on:
push:
tags:
- 'garage-ui-chart-v*'
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-*'
workflow_dispatch:
permissions:
-84
View File
@@ -25,87 +25,3 @@ jobs:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
token: ${{ steps.app-token.outputs.token }}
- name: Sync Chart.yaml appVersion on release PR
if: ${{ steps.rp.outputs.prs_created == 'true' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PRS: ${{ steps.rp.outputs.prs }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
echo "$PRS" | jq -c '.[]' | while read -r pr; do
branch=$(echo "$pr" | jq -r '.headBranchName')
workdir=$(mktemp -d)
git clone --branch "$branch" --depth 1 \
"https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "$workdir"
cd "$workdir"
git config user.email "garage-ui-release-bot[bot]@users.noreply.github.com"
git config user.name "garage-ui-release-bot[bot]"
version=$(jq -r '."."' .release-please-manifest.json)
target="v${version}"
sed -i -E "s|^appVersion:.*|appVersion: ${target}|" helm/garage-ui/Chart.yaml
if ! git diff --quiet helm/garage-ui/Chart.yaml; then
git add helm/garage-ui/Chart.yaml
git commit -m "chore: sync Chart.yaml appVersion to ${target}"
git push origin "$branch"
fi
cd -
rm -rf "$workdir"
done
- name: Cut chart patch when backend released without chart
if: ${{ steps.rp.outputs.releases_created == 'true' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
BACKEND_RELEASED: ${{ steps.rp.outputs.release_created }}
CHART_RELEASED: ${{ steps.rp.outputs['helm/garage-ui--release_created'] }}
BACKEND_TAG: ${{ steps.rp.outputs.tag_name }}
BACKEND_VERSION: ${{ steps.rp.outputs.version }}
run: |
set -euo pipefail
if [ "$BACKEND_RELEASED" != "true" ] || [ "$CHART_RELEASED" = "true" ]; then
echo "Backend not released, or chart already released this cycle. Nothing to do."
exit 0
fi
workdir=$(mktemp -d)
git clone \
"https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "$workdir"
cd "$workdir"
git config user.email "garage-ui-release-bot[bot]@users.noreply.github.com"
git config user.name "garage-ui-release-bot[bot]"
chart_old=$(jq -r '."helm/garage-ui"' .release-please-manifest.json)
chart_new=$(echo "$chart_old" | awk -F. '{printf "%d.%d.%d", $1, $2, $3+1}')
chart_tag="garage-ui-chart-v${chart_new}"
jq --arg v "$chart_new" '."helm/garage-ui" = $v' \
.release-please-manifest.json > .release-please-manifest.json.tmp
mv .release-please-manifest.json.tmp .release-please-manifest.json
sed -i -E "s|^version:.*|version: ${chart_new}|" helm/garage-ui/Chart.yaml
sed -i -E "s|^appVersion:.*|appVersion: ${BACKEND_TAG}|" helm/garage-ui/Chart.yaml
today=$(date -u +%Y-%m-%d)
compare_url="https://github.com/${REPO}/compare/garage-ui-chart-v${chart_old}...${chart_tag}"
entry=$(printf '## [%s](%s) (%s)\n\n\n### Miscellaneous Chores\n\n* sync chart appVersion to %s\n' \
"$chart_new" "$compare_url" "$today" "$BACKEND_TAG")
awk -v entry="$entry" 'NR==1 && /^# Changelog/ { print; print ""; print entry; next } { print }' \
helm/garage-ui/CHANGELOG.md > helm/garage-ui/CHANGELOG.md.tmp
mv helm/garage-ui/CHANGELOG.md.tmp helm/garage-ui/CHANGELOG.md
git add .release-please-manifest.json helm/garage-ui/Chart.yaml helm/garage-ui/CHANGELOG.md
git commit -m "chore: bump chart to ${chart_new} for backend ${BACKEND_TAG}"
git push origin main
git tag "$chart_tag"
git push origin "$chart_tag"
gh release create "$chart_tag" \
--repo "$REPO" \
--title "$chart_tag" \
--notes "Chart patch synced to backend ${BACKEND_TAG}. No chart template changes."
+1 -2
View File
@@ -1,4 +1,3 @@
{
".": "0.8.2",
"helm/garage-ui": "0.6.1"
".": "0.8.5"
}
+22
View File
@@ -1,5 +1,27 @@
# Changelog
## [0.8.5](https://github.com/Noooste/garage-ui/compare/v0.8.4...v0.8.5) (2026-07-03)
### Bug Fixes
* **frontend:** add downloadObject function for downloading files from a bucket ([430d1a5](https://github.com/Noooste/garage-ui/commit/430d1a5d68e4f9915a2951d5b31c1b5ea56cb24c))
## [0.8.4](https://github.com/Noooste/garage-ui/compare/v0.8.3...v0.8.4) (2026-06-22)
### Bug Fixes
* **helm:** track appVersion in release-please and fix badges ([1ffdfe8](https://github.com/Noooste/garage-ui/commit/1ffdfe83c884dc5e39d071343d5269164746c536))
## [0.8.3](https://github.com/Noooste/garage-ui/compare/v0.8.2...v0.8.3) (2026-06-21)
### Bug Fixes
* **backend:** improve API version detection with retry logic for health probes ([46aa375](https://github.com/Noooste/garage-ui/commit/46aa3752c81788787388d1c67e29cef786bdabff))
* **helm:** update version badges in README for Garage UI ([28c186f](https://github.com/Noooste/garage-ui/commit/28c186f3eba1a1c111100712f1eaec22a5d18eb2))
## [0.8.2](https://github.com/Noooste/garage-ui/compare/v0.8.1...v0.8.2) (2026-06-07)
+41 -2
View File
@@ -39,8 +39,14 @@ type AdminServiceResult struct {
APIVersion string
}
// errProbeNotFound means the probed route returned 404. Garage v2.x also serves
// /v1/health, so a 404 on /v2 is the only reliable "this is a v1 server" signal.
var errProbeNotFound = errors.New("probe endpoint not found")
func NewAdminService(cfg *config.GarageConfig, logLevel string) (*AdminServiceResult, error) {
if err := probeEndpoint(cfg, "/v2/GetClusterHealth"); err == nil {
// retry so a startup fails doesn't lock us to a v1 client.
err := probeEndpointWithRetry(cfg, "/v2/GetClusterHealth")
if err == nil {
logger.Info().Str("api_version", "v2").Msg("Detected Garage admin API v2")
svc := NewGarageV2AdminService(cfg, logLevel)
return &AdminServiceResult{
@@ -50,7 +56,17 @@ func NewAdminService(cfg *config.GarageConfig, logLevel string) (*AdminServiceRe
}, nil
}
if err := probeEndpoint(cfg, "/v1/health"); err == nil {
// only fall back to v1 on a real 404
// other errors mean the server is up but the probe failed transiently; picking v1 against
// a v2.x server breaks /v1/status with "v1/ endpoint is no longer supported" (issue #78).
if !errors.Is(err, errProbeNotFound) {
return nil, fmt.Errorf(
"could not detect Garage admin API version at %s: %w. Ensure Garage v1.1+ is running and the admin API is reachable",
cfg.AdminEndpoint, err,
)
}
if err := probeEndpointWithRetry(cfg, "/v1/health"); err == nil {
logger.Info().
Str("api_version", "v1").
Msg("Detected Garage admin API v1 — cluster statistics and per-node details will be unavailable")
@@ -68,6 +84,26 @@ func NewAdminService(cfg *config.GarageConfig, logLevel string) (*AdminServiceRe
)
}
const probeAttempts = 4
// probeEndpointWithRetry retries transient failures with backoff, but returns a
// 404 immediately, a missing route won't appear on a retry.
func probeEndpointWithRetry(cfg *config.GarageConfig, path string) error {
var err error
backoff := 250 * time.Millisecond
for attempt := range probeAttempts {
err = probeEndpoint(cfg, path)
if err == nil || errors.Is(err, errProbeNotFound) {
return err
}
if attempt < probeAttempts-1 {
time.Sleep(backoff)
backoff *= 2
}
}
return err
}
func probeEndpoint(cfg *config.GarageConfig, path string) error {
session := azuretls.NewSession()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -86,6 +122,9 @@ func probeEndpoint(cfg *config.GarageConfig, path string) error {
}
defer resp.RawBody.Close()
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("probe %s returned status 404: %w", path, errProbeNotFound)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("probe %s returned status %d", path, resp.StatusCode)
}
@@ -89,3 +89,61 @@ func TestDetectVersion_Unreachable(t *testing.T) {
t.Fatal("expected error for unreachable server")
}
}
// Garage v2.x serves /v1/health too, so a transient failure of the /v2 probe
// must not cause a permanent downgrade to the (broken on v2.x) v1 client.
// Regression test for https://github.com/Noooste/garage-ui/issues/78
func TestDetectVersion_V2_TransientProbeFailure(t *testing.T) {
var v2Hits int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v2/GetClusterHealth":
v2Hits++
if v2Hits < 3 { // fail the first two attempts, then recover
w.WriteHeader(http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"healthy"}`))
case "/v1/health": // v2.x still answers this
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"healthy"}`))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
t.Cleanup(srv.Close)
cfg := &config.GarageConfig{AdminEndpoint: srv.URL, AdminToken: "tok"}
result, err := NewAdminService(cfg, "")
if err != nil {
t.Fatal(err)
}
if result.APIVersion != "v2" {
t.Fatalf("expected v2 after transient probe failure, got %s", result.APIVersion)
}
}
// A server that answers /v1/health but returns a server error (not 404) for
// /v2/GetClusterHealth must NOT be detected as v1, because v2.x servers also
// answer /v1/health. Falling through to v1 here is the issue #78 misdetection.
func TestDetectVersion_DoesNotDowngradeOnV2ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v2/GetClusterHealth":
w.WriteHeader(http.StatusServiceUnavailable)
case "/v1/health":
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"healthy"}`))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
t.Cleanup(srv.Close)
cfg := &config.GarageConfig{AdminEndpoint: srv.URL, AdminToken: "tok"}
result, err := NewAdminService(cfg, "")
if err == nil && result.APIVersion == "v1" {
t.Fatal("must not downgrade to v1 when /v2 returns a server error; v2.x also serves /v1/health")
}
}
+617 -850
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -36,7 +36,7 @@
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react": "^5.2.0",
"autoprefixer": "^10.4.22",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
@@ -46,6 +46,6 @@
"tailwindcss": "^4.1.17",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.3.2"
"vite": "^8.0.16"
}
}
@@ -8,7 +8,7 @@ import { IconTile } from '@/components/ui/icon-tile';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { ArrowLeft, ChevronRight, Copy, Download, File, Loader2, Trash2 } from 'lucide-react';
import { toast } from 'sonner';
import { formatBytes } from '@/lib/file-utils';
import { downloadObject, formatBytes } from '@/lib/file-utils';
import { formatDate } from '@/lib/utils';
function CardSection({ title, children }: { title: string; children: React.ReactNode }) {
@@ -73,22 +73,9 @@ export function ObjectDetailsView() {
toast.success(label);
};
const handleDownload = async () => {
const handleDownload = () => {
if (!bucketName || !objectKey) return;
try {
const blob = await objectsApi.get(bucketName, objectKey);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName || 'download';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success('Download started');
} catch {
// error toast handled by axios interceptor
}
downloadObject(bucketName, objectKey);
};
const handleDelete = async () => {
@@ -14,7 +14,7 @@ import {
} from '@/components/ui/dropdown-menu';
import {ChevronLeft, ChevronRight, Download, Eye, FileIcon, FolderIcon, Loader2, MoreVertical, Trash2} from 'lucide-react';
import {Select, SelectOption} from '@/components/ui/select';
import {formatBytes, formatRelativeTime} from '@/lib/file-utils';
import {downloadObject, formatBytes, formatRelativeTime} from '@/lib/file-utils';
import type {S3Object} from '@/types';
interface ObjectsTableProps {
@@ -353,7 +353,7 @@ export function ObjectsTable({
<Eye className="h-4 w-4" />
View Details
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem onClick={() => downloadObject(bucketName, obj.key)}>
<Download className="h-4 w-4" />
Download
</DropdownMenuItem>
+24
View File
@@ -1,3 +1,27 @@
import { objectsApi } from './api';
import { toast } from 'sonner';
/**
* Download an object from a bucket by fetching it as a blob and clicking a
* temporary anchor element. Errors are surfaced by the axios interceptor.
*/
export async function downloadObject(bucket: string, key: string): Promise<void> {
try {
const blob = await objectsApi.get(bucket, key);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = key.split('/').pop() || 'download';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success('Download started');
} catch {
// error toast handled by axios interceptor
}
}
/**
* Get the file type based on file extension
*/
+2 -2
View File
@@ -3,8 +3,8 @@ name: garage-ui
description: A Helm chart for Garage UI - Web interface for Garage S3 object storage
icon: https://helm.noste.dev/garage.png
type: application
version: 0.6.1
appVersion: v0.8.2
version: 0.8.5 # x-release-please-version
appVersion: v0.8.5 # x-release-please-version
keywords:
- garage
- s3
+2 -2
View File
@@ -2,8 +2,8 @@
A Helm chart for deploying [Garage UI](https://github.com/Noooste/garage-ui), a modern web interface for managing [Garage](https://garagehq.deuxfleurs.fr/) distributed object storage systems.
[![Version](https://img.shields.io/badge/version-0.3.0-blue.svg)](Chart.yaml)
[![App Version](https://img.shields.io/badge/app%20version-v0.5.0-green.svg)](Chart.yaml)
[![Version](https://img.shields.io/badge/version-0.8.5?color=blue)](Chart.yaml) <!-- x-release-please-version -->
[![App Version](https://img.shields.io/badge/app%20version-v0.8.5?color=green)](Chart.yaml) <!-- x-release-please-version -->
## Table of Contents
+3 -13
View File
@@ -2,26 +2,16 @@
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": false,
"separate-pull-requests": false,
"packages": {
".": {
"release-type": "go",
"component": "garage-ui",
"include-component-in-tag": false,
"changelog-path": "CHANGELOG.md",
"exclude-paths": [
"helm/garage-ui",
".github",
"docs",
"scripts"
"extra-files": [
{ "type": "generic", "path": "helm/garage-ui/Chart.yaml" },
"helm/garage-ui/README.md"
]
},
"helm/garage-ui": {
"release-type": "helm",
"component": "garage-ui-chart",
"package-name": "garage-ui",
"include-component-in-tag": true,
"changelog-path": "CHANGELOG.md"
}
}
}