diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5003593 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,81 @@ +name: release + +# Release estable por tag v*: empaqueta la webapp completa (frontend build + +# server + node_modules de producción PRE-BUILT por arch) + checksums.txt. +# El 'Comprobar actualizaciones' del front y el ribbon semanal consumen estas +# releases (releases/latest, semver). + +on: + push: + tags: ['v*'] + workflow_dispatch: # sin tag = empaqueta y sube como artifact de prueba + +permissions: + contents: write + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runs-on: ubuntu-latest + - arch: arm64 + runs-on: ubuntu-24.04-arm + runs-on: ${{ matrix.runs-on }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: | + app/package-lock.json + server/package-lock.json + + - name: Tests del server (vitest) + working-directory: server + run: npm ci --no-audit --no-fund && npm test + + - name: Build frontend + working-directory: app + run: npm ci --no-audit --no-fund && npm run build + + - name: node_modules de producción (sin dev, compila prebuilds del arch) + working-directory: server + run: rm -rf node_modules && npm ci --omit=dev --no-audit --no-fund + + - name: Empaqueta release + env: + REF: ${{ github.ref_name }} + run: | + VERSION="${REF#v}" + [ "${{ github.ref_type }}" = "tag" ] || VERSION="0.0.0-SNAPSHOT-${GITHUB_SHA::7}" + mkdir -p stage/server + cp -r app/dist stage/dist + cp -r server/src server/package.json stage/server/ + cp -r shared stage/shared + cp -r server/node_modules stage/server/node_modules + tar -czf "helios_${VERSION}_linux_${{ matrix.arch }}.tar.gz" -C stage . + sha256sum helios_*.tar.gz > checksums.txt + cat checksums.txt + + - name: Publica en la release (solo tags) + if: github.ref_type == 'tag' + uses: softprops/action-gh-release@v2 + with: + files: | + helios_*.tar.gz + checksums.txt + + - name: Sube artifact de prueba (solo workflow_dispatch) + if: github.ref_type != 'tag' + uses: actions/upload-artifact@v4 + with: + name: helios-snapshot-linux-${{ matrix.arch }} + path: | + helios_*.tar.gz + checksums.txt + retention-days: 7 diff --git a/app/package-lock.json b/app/package-lock.json index 417cdf0..4582b61 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "helios", - "version": "0.3.2", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "helios", - "version": "0.3.2", + "version": "0.6.0", "dependencies": { "@gsap/react": "^2.1.2", "@radix-ui/react-accordion": "^1.2.12", diff --git a/app/package.json b/app/package.json index aaef141..9c3ccc2 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "helios", "private": true, - "version": "0.5.0", + "version": "0.6.0", "type": "module", "scripts": { "dev": "vite", diff --git a/app/src/components/UpdateRibbon.tsx b/app/src/components/UpdateRibbon.tsx new file mode 100644 index 0000000..d1ee2db --- /dev/null +++ b/app/src/components/UpdateRibbon.tsx @@ -0,0 +1,98 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { RefreshCw } from 'lucide-react'; +import { apiFetch } from '@/data/api-client'; +import pkg from '../../package.json'; + +const CHECK_KEY = 'helios-last-update-check'; +const CHECK_INTERVAL = 7 * 24 * 60 * 60 * 1000; // 1 vez por semana (regla app-auto-update) +const REPO_URL = 'https://github.com/gnacho/helios'; + +/** a vs b semver ('1.10.0' > '1.9.0'); prefijos 'v' ignorados. */ +function compareSemver(a: string, b: string): number { + const pa = a.replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0); + const pb = b.replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0); + for (let i = 0; i < 3; i++) { + if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0); + } + return 0; +} + +/** + * Ribbon de actualización (patrón app-auto-update: check semanal + aviso si hay + * versión nueva en el repo público). Solo admin. GET anónima a GitHub releases + * (el repo es público), 1 vez por semana vía localStorage; aplicar = redesplegar + * en el servidor, así que el ribbon enlaza a la release. + */ +export default function UpdateRibbon() { + const { t } = useTranslation(); + const [state, setState] = useState<'idle' | 'checking' | 'uptodate' | 'available' | 'error'>('idle'); + const [latestVersion, setLatestVersion] = useState(''); + + useEffect(() => { + let stale = false; + const run = async () => { + try { + // Solo admin ve el ribbon (como el resto de la zona de actualizaciones). + const me = await apiFetch<{ user?: { role?: string } }>('/api/auth/me').catch(() => null); + if (me?.user?.role !== 'admin') return; + + const last = Number(window.localStorage.getItem(CHECK_KEY) || 0); + if (Date.now() - last < CHECK_INTERVAL) return; // ya se comprobó esta semana + window.localStorage.setItem(CHECK_KEY, String(Date.now())); + + const repo = REPO_URL.match(/github\.com\/([^/]+\/[^/.]+)/)?.[1]; + if (!repo) return; + setState('checking'); + const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, { + headers: { Accept: 'application/vnd.github+json' }, + }); + let version = ''; + if (res.ok) { + const data = (await res.json()) as { tag_name?: string }; + version = data.tag_name || ''; + } else if (res.status === 404) { + const tagRes = await fetch(`https://api.github.com/repos/${repo}/tags?per_page=1`, { + headers: { Accept: 'application/vnd.github+json' }, + }); + if (tagRes.ok) { + const tags = (await tagRes.json()) as { name?: string }[]; + version = tags[0]?.name || ''; + } + } + if (stale) return; + if (!version || compareSemver(version, pkg.version) <= 0) setState('uptodate'); + else { + setLatestVersion(version.replace(/^v/, '')); + setState('available'); + } + } catch { + if (!stale) setState('error'); + } + }; + void run(); + return () => { + stale = true; + }; + }, []); + + if (state !== 'available') return null; + + return ( +