deploy: split desktop releases and fix downloads navigation
Some checks failed
CI / verify (push) Has been cancelled
CI / print-rust (push) Has been cancelled

This commit is contained in:
Admin 2026-09-23 22:53:10 +00:00
parent 8611415452
commit 927d9b298f
10 changed files with 109 additions and 288 deletions

View File

@ -13,3 +13,6 @@ WEB_ORIGIN=https://fieldlens.example.invalid
FIELDLENS_PRINT_RELEASE_TAG=fieldlens-print-v0.1.0
FIELDLENS_PRINT_RELEASE_BASE_URL=https://git.example.invalid/owner/fieldlens/releases/download
FIELDLENS_PRINT_DOWNLOAD_DIR=downloads
FIELDLENS_DESKTOP_RELEASE_BASE_URL=https://git.arrowstack.dev/Admin/fieldlens-desktop/releases/download
FIELDLENS_DESKTOP_RELEASE_TAG=v0.1.0
FIELDLENS_DESKTOP_DOWNLOAD_BASE_URL=https://git.arrowstack.dev/Admin/fieldlens-desktop/raw/branch/main/downloads

View File

@ -1,202 +1,10 @@
name: Desktop Release
name: Desktop Release (moved)
on:
push:
branches: [main]
tags:
- "fieldlens-print-v*"
paths:
- "apps/print/**"
- ".gitea/workflows/desktop-release.yml"
pull_request:
paths:
- "apps/print/**"
- ".gitea/workflows/desktop-release.yml"
workflow_dispatch:
permissions:
code: read
releases: read
jobs:
windows-nsis:
name: Windows x64 NSIS
moved:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Install Windows cross-build tools
run: |
sudo apt-get update
sudo apt-get install -y nsis lld llvm clang
cargo install --locked cargo-xwin
- name: Install Node dependencies
run: npm install
- name: Build Windows NSIS installer
run: npm run bundle:windows --workspace @fieldlens/print
env:
XWIN_CACHE_DIR: ${{ runner.temp }}/xwin-cache
- name: Stage Windows installer
shell: bash
run: |
set -euo pipefail
source_file="$(find apps/print/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis -maxdepth 1 -type f -name '*-setup.exe' -print -quit)"
test -n "$source_file"
mkdir -p dist/desktop
cp "$source_file" dist/desktop/fieldlens-print-windows-x64-setup.exe
- name: Upload Windows installer
uses: actions/upload-artifact@v3
with:
name: fieldlens-print-windows-x64
path: dist/desktop/fieldlens-print-windows-x64-setup.exe
if-no-files-found: error
retention-days: 14
linux-deb:
name: Linux x64 DEB
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- uses: dtolnay/rust-toolchain@stable
- name: Install Tauri Linux prerequisites
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
curl \
wget \
file \
libwebkit2gtk-4.1-dev \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf
- name: Install Node dependencies
run: npm install
- name: Build Debian package
run: npm run bundle:deb --workspace @fieldlens/print
- name: Stage Debian package
shell: bash
run: |
set -euo pipefail
source_file="$(find apps/print/src-tauri/target/release/bundle/deb -maxdepth 1 -type f -name '*.deb' -print -quit)"
test -n "$source_file"
mkdir -p dist/desktop
cp "$source_file" dist/desktop/fieldlens-print-linux-x64.deb
- name: Upload Debian package
uses: actions/upload-artifact@v3
with:
name: fieldlens-print-linux-x64-deb
path: dist/desktop/fieldlens-print-linux-x64.deb
if-no-files-found: error
retention-days: 14
publish-release:
name: Publish tagged desktop release
if: gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/fieldlens-print-v')
needs: [windows-nsis, linux-deb]
runs-on: ubuntu-latest
permissions:
code: read
releases: write
steps:
- name: Download Windows installer
uses: actions/download-artifact@v3
with:
name: fieldlens-print-windows-x64
path: release-assets
- name: Download Debian package
uses: actions/download-artifact@v3
with:
name: fieldlens-print-linux-x64-deb
path: release-assets
- name: Publish Gitea release assets
shell: bash
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_SERVER_URL: ${{ gitea.server_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_REF: ${{ gitea.ref }}
GITEA_REF_NAME: ${{ gitea.ref_name }}
GITEA_SHA: ${{ gitea.sha }}
run: |
set -euo pipefail
api="${GITEA_SERVER_URL%/}/api/v1/repos/${GITEA_REPOSITORY}"
auth_header="Authorization: token ${GITEA_TOKEN}"
release_tag="$GITEA_REF_NAME"
if [ "$GITEA_REF" = "refs/heads/main" ]; then
release_tag="fieldlens-print-v0.1.0"
fi
status="$(curl -sS -o /tmp/release.json -w '%{http_code}' \
-H "$auth_header" \
"$api/releases/tags/$release_tag")"
if [ "$status" = "404" ]; then
jq -n \
--arg tag "$release_tag" \
--arg sha "$GITEA_SHA" \
'{
tag_name: $tag,
target_commitish: $sha,
name: ("FieldLens Print " + ($tag | sub("^fieldlens-print-"; ""))),
body: "FieldLens Print desktop installers.",
draft: false,
prerelease: false
}' > /tmp/create-release.json
curl -fsS \
-X POST \
-H "$auth_header" \
-H "Content-Type: application/json" \
--data-binary @/tmp/create-release.json \
"$api/releases" > /tmp/release.json
elif [ "$status" != "200" ]; then
cat /tmp/release.json
exit 1
fi
release_id="$(jq -r '.id' /tmp/release.json)"
test "$release_id" != "null"
curl -fsS -H "$auth_header" "$api/releases/$release_id/assets" > /tmp/assets.json
for file in release-assets/fieldlens-print-windows-x64-setup.exe release-assets/fieldlens-print-linux-x64.deb; do
name="$(basename "$file")"
existing_id="$(jq -r --arg name "$name" '.[] | select(.name == $name) | .id' /tmp/assets.json | head -n 1)"
if [ -n "$existing_id" ] && [ "$existing_id" != "null" ]; then
curl -fsS -X DELETE -H "$auth_header" "$api/releases/$release_id/assets/$existing_id"
fi
curl -fsS \
-X POST \
-H "$auth_header" \
-F "attachment=@$file" \
"$api/releases/$release_id/assets?name=$name"
done
- run: echo "Desktop builds now run from Admin/fieldlens-desktop"

View File

@ -1,53 +1,3 @@
FROM node:24-bookworm AS desktop-build
WORKDIR /workspace
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
clang \
curl \
file \
libayatana-appindicator3-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev \
lld \
llvm \
nsis \
patchelf \
pkg-config \
librsvg2-dev \
wget \
&& rm -rf /var/lib/apt/lists/*
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal
ENV PATH="/root/.cargo/bin:$PATH"
RUN rustup target add x86_64-pc-windows-msvc \
&& cargo install --locked cargo-xwin
COPY package.json ./
COPY apps/api/package.json apps/api/package.json
COPY apps/web/package.json apps/web/package.json
COPY apps/print/package.json apps/print/package.json
COPY packages/contracts/package.json packages/contracts/package.json
COPY packages/fieldlens-core/package.json packages/fieldlens-core/package.json
RUN npm install
COPY apps/print apps/print
RUN npm run bundle:windows --workspace @fieldlens/print \
&& npm run bundle:deb --workspace @fieldlens/print \
&& mkdir -p /desktop-artifacts \
&& windows_file="$(find apps/print/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis -maxdepth 1 -type f -name '*-setup.exe' -print -quit)" \
&& linux_file="$(find apps/print/src-tauri/target/release/bundle/deb -maxdepth 1 -type f -name '*.deb' -print -quit)" \
&& test -n "$windows_file" \
&& test -n "$linux_file" \
&& cp "$windows_file" /desktop-artifacts/fieldlens-print-windows-x64-setup.exe \
&& cp "$linux_file" /desktop-artifacts/fieldlens-print-linux-x64.deb
FROM node:24-alpine AS build
WORKDIR /workspace
@ -81,7 +31,6 @@ WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV STATIC_DIR=/app/web
ENV FIELDLENS_PRINT_DOWNLOAD_DIR=/app/downloads
COPY --from=build /workspace/package.json /app/package.json
COPY --from=build /workspace/node_modules /app/node_modules
@ -91,7 +40,6 @@ COPY --from=build /workspace/migrations /app/migrations
COPY --from=build /workspace/packages/contracts/package.json /app/packages/contracts/package.json
COPY --from=build /workspace/packages/contracts/dist /app/packages/contracts/dist
COPY --from=build /workspace/apps/web/dist /app/web
COPY --from=desktop-build /desktop-artifacts /app/downloads
USER node
EXPOSE 3000

View File

@ -62,20 +62,18 @@ The Windows NSIS bundle is configured for `currentUser` installation, so install
### Desktop release artifacts
The `.gitea/workflows/desktop-release.yml` workflow packages FieldLens Print on changes to the desktop app, on `main`, and on `fieldlens-print-v*` tags.
Desktop packaging is intentionally separate from the PWA/API deployment.
Artifacts:
- Private source of truth: `Admin/fieldlens/apps/print`
- Public desktop build/release mirror: `Admin/fieldlens-desktop`
- PWA/API deploy mirror: `Admin/fieldlens-deploy`
- **Windows x64:** NSIS current-user installer from `apps/print/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe`
- **Linux x64:** Debian package from `apps/print/src-tauri/target/release/bundle/deb/*.deb`
The desktop mirror builds Windows and Linux installers with Gitea Actions and publishes public release assets. Normal FieldLens web/API deployments do not install Rust, Tauri, cargo-xwin, NSIS, or Linux desktop packaging dependencies.
After a successful Gitea Actions run, download the corresponding `fieldlens-print-windows-x64` or `fieldlens-print-linux-x64-deb` artifact from the workflow run.
Current public release assets:
Local packaging commands:
- `fieldlens-print-windows-x64-setup.exe`
- `fieldlens-print-linux-x64.deb`
```bash
npm run bundle:windows --workspace @fieldlens/print
npm run bundle:deb --workspace @fieldlens/print
```
The FieldLens `/downloads` page keeps stable same-site links; the API redirects those links to the configured desktop release tag.
The Windows package is cross-compiled with `cargo-xwin` and bundled with NSIS. MSI packaging is intentionally not part of the Linux runner workflow because WiX/MSI bundling requires Windows.

View File

@ -14,3 +14,6 @@ STATIC_DIR=
FIELDLENS_PRINT_RELEASE_TAG=fieldlens-print-v0.1.0
FIELDLENS_PRINT_RELEASE_BASE_URL=https://git.example.invalid/owner/fieldlens/releases/download
FIELDLENS_PRINT_DOWNLOAD_DIR=downloads
FIELDLENS_DESKTOP_RELEASE_BASE_URL=https://git.arrowstack.dev/Admin/fieldlens-desktop/releases/download
FIELDLENS_DESKTOP_RELEASE_TAG=v0.1.0
FIELDLENS_DESKTOP_DOWNLOAD_BASE_URL=https://git.arrowstack.dev/Admin/fieldlens-desktop/raw/branch/main/downloads

View File

@ -18,5 +18,5 @@ export const config = {
logLevel: process.env.LOG_LEVEL?.trim() || 'info',
webOrigin: process.env.WEB_ORIGIN?.trim() || 'http://localhost:5173',
staticDir: process.env.STATIC_DIR?.trim() || '',
printDownloadDir: process.env.FIELDLENS_PRINT_DOWNLOAD_DIR?.trim() || 'downloads',
desktopDownloadBaseUrl: process.env.FIELDLENS_DESKTOP_DOWNLOAD_BASE_URL?.trim() || 'https://git.arrowstack.dev/Admin/fieldlens-desktop/raw/branch/main/downloads',
}

View File

@ -1,6 +1,6 @@
import path from 'node:path'
import cors from 'cors'
import express, { type Response } from 'express'
import express from 'express'
import helmet from 'helmet'
import { FIELDLENS_SCHEMA_VERSION, isSyncRequest, type DeviceRegistration } from '@fieldlens/contracts'
import { getAccessRequest, listAccessRequests, requestAccess, reviewAccessRequest } from './access.js'
@ -61,33 +61,27 @@ app.get('/api/health/live', (_req, res) => {
res.json({ status: 'live', version: process.env.FIELDLENS_VERSION ?? 'dev' })
})
const desktopDownloadDir = path.resolve(config.printDownloadDir)
function sendDesktopDownload(res: Response, filename: string) {
const file = path.join(desktopDownloadDir, filename)
res.setHeader('Cache-Control', 'public, max-age=300')
res.download(file, filename, (error) => {
if (!error || res.headersSent) return
log('error', 'desktop_download_missing', { filename, file })
res.status(404).json({ error: 'desktop_download_unavailable' })
})
function redirectDesktopDownload(res: express.Response, filename: string) {
const base = config.desktopDownloadBaseUrl.replace(/\/$/, '')
res.setHeader('Cache-Control', 'no-store')
res.redirect(302, `${base}/${filename}`)
}
app.get('/downloads/fieldlens-print-windows-x64-setup.exe', (_req, res) => {
sendDesktopDownload(res, 'fieldlens-print-windows-x64-setup.exe')
redirectDesktopDownload(res, 'fieldlens-print-windows-x64-setup.exe')
})
app.get('/downloads/fieldlens-print-linux-x64.deb', (_req, res) => {
sendDesktopDownload(res, 'fieldlens-print-linux-x64.deb')
redirectDesktopDownload(res, 'fieldlens-print-linux-x64.deb')
})
// Backward-compatible aliases used by the first downloads page.
app.get('/downloads/fieldlens-print/windows-x64', (_req, res) => {
sendDesktopDownload(res, 'fieldlens-print-windows-x64-setup.exe')
redirectDesktopDownload(res, 'fieldlens-print-windows-x64-setup.exe')
})
app.get('/downloads/fieldlens-print/linux-x64.deb', (_req, res) => {
sendDesktopDownload(res, 'fieldlens-print-linux-x64.deb')
redirectDesktopDownload(res, 'fieldlens-print-linux-x64.deb')
})
app.get('/api/health/ready', async (_req, res) => {

View File

@ -15,7 +15,7 @@ const downloads = [
},
]
export function DesktopDownloadsLink() {
export function DesktopDownloadsLink({ onOpen }: { onOpen: () => void }) {
return (
<section className="settings-section desktop-downloads-link">
<header>
@ -23,22 +23,22 @@ export function DesktopDownloadsLink() {
<div><h2>Desktop app</h2><p>Capture printed barcodes without a physical scan</p></div>
</header>
<a className="desktop-download-settings-link" href="/downloads">
<button className="desktop-download-settings-link" type="button" onClick={onOpen}>
<span>
<strong>Get the desktop app</strong>
<small>Windows and Linux installers</small>
</span>
<span className="desktop-download-action"><Download size={13} /> Downloads</span>
</a>
</button>
</section>
)
}
export function DesktopDownloadsPage() {
export function DesktopDownloadsPage({ onBack }: { onBack: () => void }) {
return (
<main className="downloads-page">
<div className="downloads-shell">
<a className="downloads-back" href="/"><ArrowLeft size={16} /> Back to FieldLens</a>
<button className="downloads-back" type="button" onClick={onBack}><ArrowLeft size={16} /> Back to FieldLens</button>
<header className="downloads-hero">
<span className="downloads-mark">FL</span>

View File

@ -235,3 +235,23 @@
.downloads-panel-heading { padding: 18px 17px 15px; }
.downloads-notes { padding: 13px 16px 16px; }
}
.host-app-behind-page {
display: none;
}
.desktop-download-settings-link {
width: calc(100% - 36px);
cursor: pointer;
text-align: left;
font: inherit;
}
.downloads-back {
border: 0;
padding: 0;
background: transparent;
cursor: pointer;
font: inherit;
}

View File

@ -1,4 +1,4 @@
import { StrictMode } from 'react'
import { StrictMode, useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
import '../../../packages/fieldlens-core/src/index.css'
@ -19,15 +19,62 @@ startDeviceHoldTracking()
registerArrowstackReferenceProvider()
installSyncTriggers()
const isDownloadsPage = window.location.pathname === '/downloads'
const root = createRoot(document.getElementById('root')!)
function HostApp() {
const initialDownloadsPage = window.location.pathname === '/downloads'
const [downloadsOpen, setDownloadsOpen] = useState(initialDownloadsPage)
const [appMounted, setAppMounted] = useState(!initialDownloadsPage)
root.render(
useEffect(() => {
const onPopState = () => {
const open = window.location.pathname === '/downloads'
setDownloadsOpen(open)
if (!open) setAppMounted(true)
}
window.addEventListener('popstate', onPopState)
return () => window.removeEventListener('popstate', onPopState)
}, [])
const openDownloads = () => {
setAppMounted(true)
if (window.location.pathname !== '/downloads') {
window.history.pushState({ fieldlensPage: 'downloads' }, '', '/downloads')
}
setDownloadsOpen(true)
}
const closeDownloads = () => {
if (window.location.pathname === '/downloads' && window.history.length > 1) {
window.history.back()
return
}
window.history.replaceState({}, '', '/')
setDownloadsOpen(false)
setAppMounted(true)
}
return (
<>
{appMounted && (
<div className={downloadsOpen ? 'host-app-behind-page' : undefined} aria-hidden={downloadsOpen || undefined}>
<CoreApp
settingsAddon={(
<>
<AccountSettings />
<DesktopDownloadsLink onOpen={openDownloads} />
</>
)}
/>
</div>
)}
{downloadsOpen && <DesktopDownloadsPage onBack={closeDownloads} />}
</>
)
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
{isDownloadsPage ? (
<DesktopDownloadsPage />
) : (
<CoreApp settingsAddon={<><AccountSettings /><DesktopDownloadsLink /></>} />
)}
<HostApp />
</StrictMode>,
)