Compare commits

...

6 commits
v0.3.1 ... main

Author SHA1 Message Date
e7b2326521 Synchronize local branch after publishing
Some checks are pending
Validate Decky / web-client (push) Waiting to run
2026-07-20 09:35:51 -07:00
a0b46497c1 Add Decky agent guidance
Some checks are pending
Validate Decky / web-client (push) Waiting to run
2026-07-20 09:33:34 -07:00
134a0ea05b chore(release): publish Decky 0.3.2 updater metadata
Some checks are pending
Validate Decky / web-client (push) Waiting to run
2026-07-20 09:16:08 -07:00
0055fb196b Release Decky 0.3.2
Some checks are pending
Validate Decky / web-client (push) Waiting to run
2026-07-20 09:15:52 -07:00
c5287352fd Simplify refresh status layout
Some checks are pending
Validate Decky / web-client (push) Waiting to run
2026-07-20 09:11:03 -07:00
42ed790956 chore(release): publish Decky 0.3.1 updater metadata
Some checks are pending
Validate Decky / web-client (push) Waiting to run
2026-07-20 09:01:07 -07:00
11 changed files with 138 additions and 54 deletions

54
AGENTS.md Normal file
View file

@ -0,0 +1,54 @@
# Decky agent notes
## Project
- The active app is `app/`: Tauri 2 + Svelte 5 + TypeScript. The older .NET
projects under `src/` and `tests/` are reference code, not the product.
- Decky is a compact, Nextcloud-inspired desktop client for Nextcloud Deck.
Preserve its dark native title bar, horizontal/vertical views, drag-and-drop,
compact cards, collapsible sidebar, and footer-free layout.
- Background sync begins after 10 seconds idle and repeats every 30 seconds. It
must pause during edits, drags, loading, modals, and while the window is hidden.
- Never store or commit Nextcloud credentials. Use an app password and the OS
credential store. Live-instance tests require the user to enter credentials.
## Nextcloud API compatibility
- Client code and tests are in `app/src/lib/deck-api.ts` and
`app/src/lib/deck-api.test.ts`.
- Support both documented `index.php/apps/deck/api/v1.0/` and rewrite-friendly
`apps/deck/api/v1.0/` paths.
- Preserve complete card fields on updates, including color and object/string
owner handling. Keep the tested cross-stack card-move fallback.
- Stack reorder uses Deck's OCS reorder endpoint and verifies persisted order.
- Respect board permissions before exposing edit/manage/delete controls.
## Validation
From `app/`, run `npm ci`, `npm run check`, `npm test`, and `npm run build`.
Build Windows installers on this Windows machine; the Unraid Forgejo Docker
runner only validates the web client via `.forgejo/workflows/validate.yml`.
## Versions and releases
- Installed apps update from signed Forgejo Release assets, never from Git.
- A released version/tag is immutable. Always increment the semantic version;
never republish or replace assets for an existing version.
- Use `scripts/Set-Version.ps1` to synchronize npm, Cargo, Tauri, and the browser
fallback. Commit and push source to `main` before publishing the release.
- Build/sign with `scripts/Prepare-Release.ps1`. Generated files live under
`artifacts/release/vX.Y.Z/` and stay out of Git.
- Publish by double-clicking `scripts/Publish-DeckyRelease.cmd` or by invoking
`scripts/Publish-ForgejoRelease.ps1`. The publisher creates the tag/release,
uploads the installer/signature, commits `releases/latest.json` to `main`, and
automatically fast-forwards the local checkout to that metadata commit.
- The publisher requires a clean `main` that exactly matches `origin/main`, so
commit and push source first. LF-to-CRLF messages on Windows are warnings,
not failures.
- Forgejo: `https://git.elijahkuntz.com/Elijah/Decky`. A publishing PAT needs
`write:repository`; never commit or print it.
- The permanent Tauri updater key is outside the repository at
`%USERPROFILE%\.tauri\decky.key` with its password beside it. Never expose,
replace, or regenerate this key after releases have shipped.
See `app/UPDATES.md` for the complete release procedure.

View file

@ -73,7 +73,6 @@ current PowerShell process and run:
$env:FORGEJO_PAT = 'paste-token-for-this-session'
powershell -ExecutionPolicy Bypass -File .\scripts\Publish-ForgejoRelease.ps1 -Version 0.3.0 -Notes "Board and stack management"
Remove-Item Env:FORGEJO_PAT
git pull --ff-only
```
For an interactive, double-clickable option, run:
@ -86,11 +85,12 @@ It bypasses PowerShell's script policy only for the publisher process, prompts
for the version and a hidden personal access token, and leaves the window open
so any error can be read. The token is not saved.
The publisher creates the `v0.3.0` Forgejo release/tag at `main`, uploads the
installer, signature, and metadata, then commits the tracked
`releases/latest.json` through the Forgejo API. The final pull synchronizes that
small metadata commit locally. Existing Decky installations then discover the
release through Settings.
The publisher first verifies that the working tree is clean and local `main`
exactly matches `origin/main`. It then creates the `v0.3.0` Forgejo release/tag,
uploads the installer, signature, and metadata, and commits the tracked
`releases/latest.json` through the Forgejo API. Finally, it automatically
fast-forwards local `main` to that metadata commit. Existing Decky installations
then discover the release through Settings.
## Forgejo Actions

4
app/package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "decky",
"version": "0.3.1",
"version": "0.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "decky",
"version": "0.3.1",
"version": "0.3.2",
"dependencies": {
"@lucide/svelte": "^0.545.0",
"@tauri-apps/api": "^2.0.0",

View file

@ -1,7 +1,7 @@
{
"name": "decky",
"private": true,
"version": "0.3.1",
"version": "0.3.2",
"type": "module",
"scripts": {
"dev": "vite --configLoader runner",

View file

@ -590,7 +590,7 @@ dependencies = [
[[package]]
name = "decky"
version = "0.3.1"
version = "0.3.2"
dependencies = [
"keyring",
"serde",

View file

@ -1,6 +1,6 @@
[package]
name = "decky"
version = "0.3.1"
version = "0.3.2"
description = "A focused cross-platform client for Nextcloud Deck"
authors = ["Elijah Kuntz"]
edition = "2021"

View file

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Decky",
"version": "0.3.1",
"version": "0.3.2",
"identifier": "com.elijahkuntz.decky",
"build": {
"beforeDevCommand": "npm run dev",
@ -34,7 +34,7 @@
"category": "Productivity",
"shortDescription": "Nextcloud Deck on your desktop",
"longDescription": "A focused cross-platform desktop client for viewing and editing Nextcloud Deck boards.",
"copyright": "Copyright © 2026 Elijah Kuntz",
"copyright": "Copyright (c) 2026 Elijah Kuntz",
"windows": {
"webviewInstallMode": {
"type": "downloadBootstrapper"

View file

@ -14,7 +14,6 @@
ChevronRight,
Columns3,
LayoutList,
LoaderCircle,
Minus,
Pencil,
Plus,
@ -53,7 +52,7 @@
let dragOriginStacks: Stack[] | null = null;
let dragGhost: { kind: 'card' | 'stack'; title: string; color?: string | null; x: number; y: number; width: number; offsetX: number; offsetY: number } | null = null;
let reduceMotion = false;
let currentVersion = '0.3.1';
let currentVersion = '0.3.2';
let pendingUpdate: Update | null = null;
let updateState: 'idle' | 'checking' | 'available' | 'current' | 'downloading' | 'installing' | 'error' = 'idle';
let updateMessage = 'Updates are checked securely against releases from git.elijahkuntz.com.';
@ -113,7 +112,7 @@
{ id: 106, stackId: 13, title: 'Patho Concept Map', type: 'plain', owner: 'admin', order: 2, color: 'E69A88', done: null }
]}
];
status = 'Homework · 6 cards · Preview data';
status = 'Preview data';
return;
}
if (!serverUrl || !username) return;
@ -149,7 +148,7 @@
return;
}
updateState = 'checking';
updateMessage = 'Checking Forgejo for a newer signed release…';
updateMessage = 'Checking Forgejo for a newer signed release...';
updateProgress = 0;
pendingUpdate = null;
try {
@ -170,7 +169,7 @@
async function installUpdate() {
if (!pendingUpdate) return;
updateState = 'downloading';
updateMessage = `Downloading Decky ${pendingUpdate.version}…`;
updateMessage = `Downloading Decky ${pendingUpdate.version}...`;
let downloaded = 0;
let total = 0;
try {
@ -184,7 +183,7 @@
} else if (event.event === 'Finished') {
updateState = 'installing';
updateProgress = 100;
updateMessage = 'Download verified. Installing and restarting Decky…';
updateMessage = 'Download verified. Installing and restarting Decky...';
}
});
await relaunch();
@ -196,7 +195,7 @@
async function connect(silent = false) {
busy = true;
status = 'Connecting to Nextcloud…';
status = 'Connecting...';
try {
client = await DeckApiClient.connect({ serverUrl, username, appPassword });
boards = (await client.getBoards()).filter((board) => !board.archived && !board.deletedAt);
@ -228,13 +227,12 @@
async function refreshBoard() {
if (!client || !selectedBoard) return;
busy = true;
status = `Loading ${selectedBoard.title}…`;
status = 'Refreshing...';
try {
stacks = (await client.getStacks(selectedBoard.id))
.filter((stack) => !stack.deletedAt)
.sort((a, b) => a.order - b.order);
const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((card) => !card.archived).length ?? 0), 0);
status = `${selectedBoard.title} · ${count} card${count === 1 ? '' : 's'} · Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
status = `Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
} catch (error) {
status = friendlyError(error);
} finally {
@ -249,7 +247,7 @@
const interactionAtStart = lastInteractionAt;
if (!background) {
busy = true;
status = selectedBoard ? `Loading ${selectedBoard.title}…` : 'Refreshing boardsââ¬Â¦';
status = 'Refreshing...';
}
try {
const refreshedBoards = (await activeClient.getBoards()).filter((board) => !board.archived && !board.deletedAt);
@ -273,8 +271,7 @@
.sort((a, b) => a.order - b.order);
if (client !== activeClient || selectedBoard?.id !== board.id || (background && (lastInteractionAt !== interactionAtStart || document.visibilityState !== 'visible'))) return;
stacks = refreshedStacks;
const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((card) => !card.archived).length ?? 0), 0);
status = `${board.title} · ${count} card${count === 1 ? '' : 's'} · Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
status = `Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
} catch (error) {
status = `${background ? 'Background refresh failed' : 'Refresh failed'}: ${friendlyError(error)}`;
} finally {
@ -503,7 +500,7 @@
}
async function deleteCard(card: Card) {
if (!client || !selectedBoard || !confirm(`Delete “${card.title}”?`)) return;
if (!client || !selectedBoard || !confirm(`Delete "${card.title}"?`)) return;
busy = true;
try {
await client.deleteCard(selectedBoard.id, card.stackId, card.id);
@ -675,8 +672,7 @@
} else if (stackId !== null) {
const targetIndex = stacks.findIndex((stack) => stack.id === stackId);
stacks = await client.reorderStack(board.id, stackId, targetIndex);
const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((item) => !item.archived).length ?? 0), 0);
status = `${board.title} · ${count} card${count === 1 ? '' : 's'} · Stack order saved`;
status = `Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
}
} catch (error) {
stacks = origin;
@ -711,7 +707,7 @@
}
</script>
<svelte:head><title>{selectedBoard ? `${selectedBoard.title} · Decky` : 'Decky'}</title></svelte:head>
<svelte:head><title>{selectedBoard ? `${selectedBoard.title} - Decky` : 'Decky'}</title></svelte:head>
<div class:sidebar-collapsed={settings.sidebarCollapsed} class="app-shell">
<header class="window-titlebar" role="presentation" on:mousedown={dragWindow} on:dblclick={() => runWindowAction('maximize')}>
@ -731,7 +727,7 @@
<label>Username<input bind:value={username} autocomplete="username" /></label>
<label>App password<input type="password" bind:value={appPassword} autocomplete="current-password" /></label>
<label class="checkbox-row"><input type="checkbox" bind:checked={rememberPassword} /> Save securely on this device</label>
<button class="primary-button" disabled={busy || !serverUrl || !username || !appPassword}>{busy ? 'Connecting…' : 'Connect'}</button>
<button class="primary-button" disabled={busy || !serverUrl || !username || !appPassword}>{busy ? 'Connecting...' : 'Connect'}</button>
</form>
{/if}
@ -759,7 +755,10 @@
<button class:active={orientation === 'horizontal'} title="Horizontal layout" on:click={() => setOrientation('horizontal')}><Columns3 size={18} /><span>Columns</span></button>
<button class:active={orientation === 'vertical'} title="Vertical layout" on:click={() => setOrientation('vertical')}><LayoutList size={18} /><span>List</span></button>
</div>
<button class="sidebar-tool" title="Refresh" disabled={!client || busy} on:click={() => refreshFromNextcloud()}><RefreshCw size={18} class={busy ? 'spinning' : ''} /><span>Refresh</span></button>
<div class="refresh-row">
<button class="sidebar-tool" title="Refresh" disabled={!client || busy} on:click={() => refreshFromNextcloud()}><RefreshCw size={18} class={busy ? 'spinning' : ''} /><span>Refresh</span></button>
<span class="sync-status" title={status}>{status}</span>
</div>
<button class="sidebar-tool" title="Settings" on:click={() => (settingsOpen = true)}><Settings size={18} /><span>Settings</span></button>
</div>
</aside>
@ -841,10 +840,6 @@
{/if}
</main>
<footer class="statusbar">
{#if busy}<LoaderCircle size={14} class="spinning" />{:else}<span class:connected={Boolean(client)} class="status-dot"></span>{/if}
<span>{status}</span>
</footer>
</div>
{#if dragGhost}
@ -867,7 +862,7 @@
<form class="modal" on:submit|preventDefault={saveCard}>
<header><div><small>{editor.mode === 'create' ? 'NEW CARD' : 'EDIT CARD'}</small><h2>{editor.mode === 'create' ? 'Create a card' : editor.card?.title}</h2></div><button type="button" class="icon-button" on:click={() => (editor = null, colorPickerOpen = false)}><X size={20} /></button></header>
<label>Title<input bind:value={editor.draft.title} maxlength="255" /></label>
<label>Description<textarea bind:value={editor.draft.description} rows="5" placeholder="Add details…"></textarea></label>
<label>Description<textarea bind:value={editor.draft.description} rows="5" placeholder="Add details..."></textarea></label>
<div class="form-row">
<label>Due date<input type="datetime-local" bind:value={editor.draft.dueDate} /></label>
<label>Card color
@ -943,7 +938,7 @@
<div class="modal-backdrop" role="presentation" on:click={(event) => event.currentTarget === event.target && (pendingDeletion = null)}>
<div class="modal confirm-modal" role="alertdialog" aria-modal="true" aria-labelledby="delete-title">
<header>
<div><small>CONFIRM DELETE</small><h2 id="delete-title">Delete “{pendingDeletion.title}”?</h2></div>
<div><small>CONFIRM DELETE</small><h2 id="delete-title">Delete "{pendingDeletion.title}"?</h2></div>
<button type="button" class="icon-button" aria-label="Close" on:click={() => (pendingDeletion = null)}><X size={20} /></button>
</header>
<p class="modal-copy">
@ -953,7 +948,7 @@
This permanently deletes the stack and its {pendingDeletion.cardCount} card{pendingDeletion.cardCount === 1 ? '' : 's'} from Nextcloud.
{/if}
</p>
<footer><button type="button" class="secondary-button" on:click={() => (pendingDeletion = null)}>Cancel</button><button type="button" class="danger-button" disabled={busy} on:click={confirmStructureDeletion}>{busy ? 'Deleting…' : `Delete ${pendingDeletion.kind}`}</button></footer>
<footer><button type="button" class="secondary-button" on:click={() => (pendingDeletion = null)}>Cancel</button><button type="button" class="danger-button" disabled={busy} on:click={confirmStructureDeletion}>{busy ? 'Deleting...' : `Delete ${pendingDeletion.kind}`}</button></footer>
</div>
</div>
{/if}
@ -972,7 +967,7 @@
<label class="checkbox-row"><input type="checkbox" bind:checked={rememberPassword} /> Save securely on this device</label>
<div class="settings-actions">
{#if client}<button type="button" class="secondary-button" on:click={disconnect}>Disconnect</button>{/if}
<button class="primary-button" disabled={busy || !serverUrl || !username || !appPassword}>{busy ? 'Connecting…' : client ? 'Reconnect' : 'Connect'}</button>
<button class="primary-button" disabled={busy || !serverUrl || !username || !appPassword}>{busy ? 'Connecting...' : client ? 'Reconnect' : 'Connect'}</button>
</div>
</form>
</section>
@ -996,7 +991,7 @@
<button type="button" class="primary-button" on:click={installUpdate}>Download and install {pendingUpdate?.version}</button>
{:else}
<button type="button" class="secondary-button" disabled={updateState === 'checking' || updateState === 'downloading' || updateState === 'installing'} on:click={checkForUpdates}>
{updateState === 'checking' ? 'Checking…' : 'Check for updates'}
{updateState === 'checking' ? 'Checking...' : 'Check for updates'}
</button>
{/if}
</div>

View file

@ -29,7 +29,7 @@ button { color: inherit; }
.app-shell {
height: 100%;
display: grid;
grid-template: 34px minmax(0, 1fr) 28px / 250px minmax(0, 1fr);
grid-template: 34px minmax(0, 1fr) / 250px minmax(0, 1fr);
background: var(--bg);
}
@ -115,6 +115,8 @@ textarea { resize: vertical; min-height: 90px; }
.sidebar-layout button { justify-content: center; gap: 6px; padding: 0 6px; background: #292929; }
.sidebar-layout button.active { color: #fff; background: #3a3a3a; box-shadow: inset 0 -2px var(--accent); }
.sidebar-tool span, .sidebar-layout span { font-size: 12px; }
.refresh-row { min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 4px; }
.sync-status { overflow: hidden; padding-right: 7px; color: var(--muted); font-size: 10px; text-align: right; text-overflow: ellipsis; white-space: nowrap; }
.workspace { grid-column: 2; grid-row: 2; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: #232323; }
.workspace-tools { min-height: 46px; flex: 0 0 auto; display: grid; grid-template-columns: minmax(120px, 1fr) minmax(180px, 280px) minmax(80px, 1fr); align-items: center; gap: 16px; padding: 6px 18px; color: var(--muted); font-size: 12px; border-bottom: 1px solid var(--line); }
@ -173,8 +175,6 @@ textarea { resize: vertical; min-height: 90px; }
.empty-illustration span { position: absolute; width: 66px; height: 44px; z-index: -1; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); transform: translate(-28px, 13px) rotate(-12deg); }
.empty-illustration span:last-child { transform: translate(28px, 13px) rotate(12deg); }
.statusbar { grid-column: 1 / -1; grid-row: 3; display: flex; align-items: center; gap: 8px; min-width: 0; padding: 0 12px; border-top: 1px solid var(--line); color: var(--muted); background: #161616; font-size: 11px; }
.statusbar span:last-child { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.spinning { animation: spin .9s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

View file

@ -1,11 +1,11 @@
{
"version": "0.3.0",
"notes": "Board and stack creation, editing, reordering, and deletion; polished management controls; Forgejo update workflow.",
"pub_date": "2026-07-20T02:00:14.7906154+00:00",
"version": "0.3.2",
"notes": "Removed the bottom status bar, moved the updated time beside Refresh, and cleaned malformed text encoding.",
"pub_date": "2026-07-20T16:13:33.7170490+00:00",
"platforms": {
"windows-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTSHZBY0NPTW9uRERoeS84YzV1UnFyMVFkTWFqSEpqUm5rclQ2NDlZb09Ra0ZwZ0treFdVZ3cvOTFGWXY3aHhmUFlwNFNFc3QxQTQ2T0l6UXRPdkFCbW01K2VXaXZHNVFJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg0NTEyODEzCWZpbGU6RGVja3lfMC4zLjBfeDY0LXNldHVwLmV4ZQpBTGlRUjhmdnkyNm1pZDFFTlJhdWNEQzdTZGRSS1crOSt3Mm5XaHpsR3ZtMGNZZmpLYmtYSHJrZFpUSnVxTHRHTEtDaHFlUUxxTkJ3SXRvRnZReWRCUT09Cg==",
"url": "https://git.elijahkuntz.com/Elijah/Decky/releases/download/v0.3.0/Decky_0.3.0_x64-setup.exe"
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTSHZBY0NPTW9uREMyYUlZNUJnYzhqRHRlTE1qeDZCYlZtVXRGRDZtZER5V2p1V3ZBdXprVndkZ3FqNURzQitoUG1ZWFdFQytxRFFzQ1BMNnJnVE10ZlhCYm5UQk9NTFFFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg0NTY0MDEyCWZpbGU6RGVja3lfMC4zLjJfeDY0LXNldHVwLmV4ZQpLOGZ4TXp6V0xYalp6czNDbitleVhBSVlrZHNLT0ZQSmhiTjFGeXlNTUVEYy8vSS9JdlZwQUdiNWdOL0NvcXVFcTVJWDhXWXZwQnhlUnh2Z0xER3dEdz09Cg==",
"url": "https://git.elijahkuntz.com/Elijah/Decky/releases/download/v0.3.2/Decky_0.3.2_x64-setup.exe"
}
}
}

View file

@ -12,6 +12,35 @@ param(
)
$ErrorActionPreference = 'Stop'
$repositoryRoot = Split-Path -Parent $PSScriptRoot
$releaseDirectory = Join-Path $repositoryRoot "artifacts\release\v$Version"
$latestJsonPath = Join-Path $releaseDirectory 'latest.json'
if (-not (Test-Path -LiteralPath $latestJsonPath)) { throw "Run Prepare-Release.ps1 first: $latestJsonPath is missing." }
function Invoke-GitCommand {
param([Parameter(Mandatory)][string[]]$ArgumentList)
$output = @(& git -C $repositoryRoot @ArgumentList 2>&1)
if ($LASTEXITCODE) {
throw "git $($ArgumentList -join ' ') failed: $($output -join [Environment]::NewLine)"
}
return $output
}
$branch = ((Invoke-GitCommand @('rev-parse', '--abbrev-ref', 'HEAD')) | Select-Object -Last 1).Trim()
if ($branch -ne 'main') { throw "Releases must be published from main; the current branch is '$branch'." }
$workingChanges = @(Invoke-GitCommand @('status', '--porcelain'))
if ($workingChanges.Count) {
throw "Commit or discard all source changes before publishing:`n$($workingChanges -join [Environment]::NewLine)"
}
Invoke-GitCommand @('fetch', 'origin', 'main') | Out-Null
$localHead = ((Invoke-GitCommand @('rev-parse', 'HEAD')) | Select-Object -Last 1).Trim()
$remoteHead = ((Invoke-GitCommand @('rev-parse', 'origin/main')) | Select-Object -Last 1).Trim()
if ($localHead -ne $remoteHead) {
throw 'Local main does not exactly match origin/main. Pull/rebase or push the source before publishing.'
}
if (-not $Token) {
Write-Host 'A Forgejo personal access token is required to create the release.' -ForegroundColor Cyan
Write-Host 'The token is used only by this process and is not saved by Decky.' -ForegroundColor DarkGray
@ -20,11 +49,6 @@ if (-not $Token) {
}
if (-not $Token) { throw 'No Forgejo personal access token was entered.' }
$repositoryRoot = Split-Path -Parent $PSScriptRoot
$releaseDirectory = Join-Path $repositoryRoot "artifacts\release\v$Version"
$latestJsonPath = Join-Path $releaseDirectory 'latest.json'
if (-not (Test-Path -LiteralPath $latestJsonPath)) { throw "Run Prepare-Release.ps1 first: $latestJsonPath is missing." }
$headers = @{ Authorization = "token $Token"; Accept = 'application/json' }
$apiRoot = "$($ForgejoBaseUrl.TrimEnd('/'))/api/v1/repos/$Repository"
$tag = "v$Version"
@ -82,3 +106,14 @@ $contentBody = @{
if ($sha) { $contentBody.sha = $sha }
Invoke-RestMethod -Method Put -Uri "$apiRoot/contents/releases/latest.json" -Headers $headers -ContentType 'application/json' -Body ($contentBody | ConvertTo-Json) | Out-Null
Write-Host "Published Decky $Version and updated releases/latest.json."
Invoke-GitCommand @('fetch', 'origin', 'main') | Out-Null
$publishedHead = ((Invoke-GitCommand @('rev-parse', 'origin/main')) | Select-Object -Last 1).Trim()
if ($publishedHead -ne $localHead) {
$mergeBase = ((Invoke-GitCommand @('merge-base', $localHead, $publishedHead)) | Select-Object -Last 1).Trim()
if ($mergeBase -ne $localHead) {
throw 'The release was published, but origin/main also changed independently. Fetch and reconcile it manually.'
}
Invoke-GitCommand @('merge', '--ff-only', 'origin/main') | Out-Null
}
Write-Host 'Local main is synchronized with the Forgejo release metadata commit.'