diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index efe227cb7..f07e9ba55 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -757,7 +757,7 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ runner.temp }}/podman-linux-amd64.tar.gz - key: ${{ runner.os }}-${{ runner.arch }}-podman-static-v5.8.4 + key: ${{ runner.os }}-${{ runner.arch }}-podman-static-v5.8.4-sha256-v2 - name: install podman (Linux) if: (matrix.install-podman == 'rootless' || matrix.install-podman == 'rootful') && runner.os == 'Linux' diff --git a/cmd/secrets/list.go b/cmd/secrets/list.go index 90ba14014..0d4bb7196 100644 --- a/cmd/secrets/list.go +++ b/cmd/secrets/list.go @@ -37,6 +37,7 @@ func NewListCmd(flags *flags.GlobalFlags) *cobra.Command { type secretEntry struct { Name string `json:"name"` Context string `json:"context"` + Backend string `json:"backend,omitempty"` Created string `json:"created,omitempty"` LastUsed string `json:"lastUsed,omitempty"` Orphaned bool `json:"orphaned,omitempty"` @@ -93,6 +94,7 @@ func renderJSON(metas []secrets.SecretMeta) error { entries = append(entries, secretEntry{ Name: m.Name, Context: m.Context, + Backend: string(m.Backend), Created: formatTime(m.Created), LastUsed: formatTime(m.LastUsed), Orphaned: m.Orphaned, @@ -117,7 +119,7 @@ func formatTime(t time.Time) string { func orphanLabel(orphaned bool) string { if orphaned { - return "orphaned" + return "missing value" } return "ok" } diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 3cd27d01a..5a6cd3a8a 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -42,6 +42,7 @@ interface SecretEntry { created?: string lastUsed?: string orphaned?: boolean + backend?: "keyring" | "file" } interface EnvEntry { diff --git a/desktop/src/renderer/src/lib/stores/secrets.test.ts b/desktop/src/renderer/src/lib/stores/secrets.test.ts index 6cff95c59..32f2bec13 100644 --- a/desktop/src/renderer/src/lib/stores/secrets.test.ts +++ b/desktop/src/renderer/src/lib/stores/secrets.test.ts @@ -6,6 +6,7 @@ import { initSecrets, refreshSecrets, secrets, + secretsError, secretsLoading, } from "./secrets.js" @@ -13,6 +14,7 @@ describe("secrets store", () => { beforeEach(() => { resetTauriMocks() secrets.set([]) + secretsError.set(null) secretsLoading.set(true) }) @@ -36,6 +38,19 @@ describe("secrets store", () => { expect(get(secretsLoading)).toBe(false) expect(get(secrets)).toEqual([]) + expect(get(secretsError)).toBe("IPC not available") + }) + + it("clears the init error after a successful retry", async () => { + mockInvoke + .mockRejectedValueOnce(new Error("IPC not available")) + .mockResolvedValueOnce([{ name: "TOKEN", context: "default" }]) + + await initSecrets() + expect(get(secretsError)).toBe("IPC not available") + + await initSecrets() + expect(get(secretsError)).toBeNull() }) it("refreshSecrets updates the store", async () => { @@ -46,4 +61,16 @@ describe("secrets store", () => { expect(get(secrets)).toHaveLength(1) expect(get(secrets)[0].name).toBe("TOKEN") }) + + it("clears the refresh error after a successful retry", async () => { + mockInvoke + .mockRejectedValueOnce(new Error("IPC not available")) + .mockResolvedValueOnce([{ name: "TOKEN", context: "default" }]) + + await refreshSecrets() + expect(get(secretsError)).toBe("IPC not available") + + await refreshSecrets() + expect(get(secretsError)).toBeNull() + }) }) diff --git a/desktop/src/renderer/src/lib/stores/secrets.ts b/desktop/src/renderer/src/lib/stores/secrets.ts index 14b5333d1..2b98afcbe 100644 --- a/desktop/src/renderer/src/lib/stores/secrets.ts +++ b/desktop/src/renderer/src/lib/stores/secrets.ts @@ -4,12 +4,14 @@ import type { Secret } from "$lib/types/index.js" export const secrets = writable([]) export const secretsLoading = writable(true) +export const secretsError = writable(null) export async function refreshSecrets(): Promise { try { secrets.set(await secretList()) - } catch { - // IPC not available + secretsError.set(null) + } catch (err) { + secretsError.set(err instanceof Error ? err.message : String(err)) } } @@ -17,8 +19,9 @@ export async function initSecrets(): Promise { secretsLoading.set(true) try { secrets.set(await secretList()) - } catch { - // IPC not available + secretsError.set(null) + } catch (err) { + secretsError.set(err instanceof Error ? err.message : String(err)) } finally { secretsLoading.set(false) } diff --git a/desktop/src/renderer/src/lib/types/index.ts b/desktop/src/renderer/src/lib/types/index.ts index 45f7a17a3..10c2b6f9b 100644 --- a/desktop/src/renderer/src/lib/types/index.ts +++ b/desktop/src/renderer/src/lib/types/index.ts @@ -151,6 +151,7 @@ export interface Secret { created?: string lastUsed?: string orphaned?: boolean + backend?: "keyring" | "file" } export interface EnvVar { diff --git a/desktop/src/renderer/src/pages/SecretsPage.svelte b/desktop/src/renderer/src/pages/SecretsPage.svelte index 19ce8eb75..75f4f0e6d 100644 --- a/desktop/src/renderer/src/pages/SecretsPage.svelte +++ b/desktop/src/renderer/src/pages/SecretsPage.svelte @@ -7,7 +7,7 @@ import { badgeVariants } from "$lib/components/ui/badge/index.js" import * as Dialog from "$lib/components/ui/dialog/index.js" import ConfirmDialog from "$lib/components/layout/ConfirmDialog.svelte" import CardSkeleton from "$lib/components/ui/skeleton/CardSkeleton.svelte" -import { secrets, secretsLoading, refreshSecrets } from "$lib/stores/secrets.js" +import { secrets, secretsError, secretsLoading, refreshSecrets } from "$lib/stores/secrets.js" import { secretSet, secretDelete } from "$lib/ipc/commands.js" import { toasts } from "$lib/stores/toasts.js" import { extractErrorMessage } from "$lib/utils/error.js" @@ -102,7 +102,7 @@ async function confirmDelete() { {nameExists ? "Replace Secret" : "Add Secret"} - Stored in your OS keyring and injected into workspaces on demand. + Stored securely in your configured Devsy secrets backend and injected into workspaces on demand.
{ e.preventDefault(); handleCreate() }} class="space-y-4"> @@ -158,6 +158,10 @@ async function confirmDelete() { {/each} + {:else if $secretsError} +
+ Unable to load secrets: {$secretsError} +
{:else if $secrets.length === 0}
@@ -189,7 +193,7 @@ async function confirmDelete() {
{#if secret.orphaned} - orphaned + Missing value {/if}