mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
feat: prerelease workflow with major version support and version propagation
- Add docker-dev.yml: prerelease CI for dev branch with minor/major bump inputs; auto-continues in-flight major line via existing pre tags; publishes floating major-pre Docker tag (e.g. 2-pre) - Rewrite docker.yml version-bump: tag-based versioning, manual bump inputs (auto/patch/minor/major), major guarded by confirm_major=MAJOR, auto-finalizes in-flight prereleases; publishes floating major tag (e.g. 2) - Inject APP_VERSION build-arg through Dockerfile so the running container knows its real version instead of reading package.json - Server reads APP_VERSION env in authService/adminService; exposes is_prerelease in app config and update-check response; prerelease builds compare against GitHub prerelease releases rather than latest stable - Client stores isPrerelease from config; navbar shows amber version badge on prerelease builds (left of dark-mode toggle); GitHubPanel filters out prerelease releases unless the running build is itself a prerelease
This commit is contained in:
@@ -0,0 +1,160 @@
|
|||||||
|
name: Build & Push Docker Image (Prerelease)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [dev]
|
||||||
|
paths-ignore:
|
||||||
|
- 'docs/**'
|
||||||
|
- '**/*.md'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
bump:
|
||||||
|
description: 'Bump line for next prerelease (auto detects in-flight major)'
|
||||||
|
type: choice
|
||||||
|
options: [auto, minor, major]
|
||||||
|
default: auto
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
version-bump:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.bump.outputs.VERSION }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Determine prerelease version and tag
|
||||||
|
id: bump
|
||||||
|
run: |
|
||||||
|
git fetch --tags
|
||||||
|
|
||||||
|
# Get latest stable tag (exclude prerelease tags)
|
||||||
|
STABLE_TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | grep -v '\-pre\.' | sort -V | tail -1)
|
||||||
|
STABLE="${STABLE_TAG#v}"
|
||||||
|
echo "Latest stable: $STABLE"
|
||||||
|
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$STABLE"
|
||||||
|
|
||||||
|
# Detect any in-flight major prerelease (v(MAJOR+1).0.0-pre.*). Stay on that line if found.
|
||||||
|
NEXT_MAJOR="$((MAJOR + 1)).0.0"
|
||||||
|
MAJOR_PRE_EXISTS=$(git tag -l "v${NEXT_MAJOR}-pre.*" | head -1)
|
||||||
|
|
||||||
|
BUMP_INPUT="${{ github.event.inputs.bump || 'auto' }}"
|
||||||
|
|
||||||
|
if [ "$BUMP_INPUT" = "major" ] || { [ "$BUMP_INPUT" = "auto" ] && [ -n "$MAJOR_PRE_EXISTS" ]; }; then
|
||||||
|
TARGET="$NEXT_MAJOR"
|
||||||
|
else
|
||||||
|
TARGET="${MAJOR}.$((MINOR + 1)).0"
|
||||||
|
fi
|
||||||
|
echo "Target: $TARGET"
|
||||||
|
|
||||||
|
# Count existing prerelease tags for this target and increment
|
||||||
|
N=$(git tag -l "v${TARGET}-pre.*" | wc -l)
|
||||||
|
N=$((N + 1))
|
||||||
|
|
||||||
|
NEW_VERSION="${TARGET}-pre.${N}"
|
||||||
|
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "$STABLE → $NEW_VERSION"
|
||||||
|
|
||||||
|
# Tag only — no file changes, no commit
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git tag "v$NEW_VERSION"
|
||||||
|
git push origin "v$NEW_VERSION"
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
needs: version-bump
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
steps:
|
||||||
|
- name: Prepare platform tag-safe name
|
||||||
|
run: echo "PLATFORM_PAIR=$(echo ${{ matrix.platform }} | sed 's|/|-|g')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: dev
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
outputs: type=image,name=mauriceboe/trek,push-by-digest=true,name-canonical=true,push=true
|
||||||
|
no-cache: true
|
||||||
|
build-args: |
|
||||||
|
APP_VERSION=${{ needs.version-bump.outputs.version }}
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: /tmp/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [version-bump, build]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: dev
|
||||||
|
|
||||||
|
- name: Download build digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/digests
|
||||||
|
pattern: digests-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Create and push multi-arch manifest
|
||||||
|
working-directory: /tmp/digests
|
||||||
|
run: |
|
||||||
|
VERSION=${{ needs.version-bump.outputs.version }}
|
||||||
|
mapfile -t digests < <(printf 'mauriceboe/trek@sha256:%s\n' *)
|
||||||
|
MAJOR_TAG="$(echo "$VERSION" | cut -d. -f1)-pre"
|
||||||
|
docker buildx imagetools create \
|
||||||
|
-t mauriceboe/trek:latest-pre \
|
||||||
|
-t mauriceboe/trek:$MAJOR_TAG \
|
||||||
|
-t mauriceboe/trek:$VERSION \
|
||||||
|
-t mauriceboe/nomad:latest-pre \
|
||||||
|
-t mauriceboe/nomad:$MAJOR_TAG \
|
||||||
|
-t mauriceboe/nomad:$VERSION \
|
||||||
|
"${digests[@]}"
|
||||||
|
|
||||||
|
- name: Inspect manifest
|
||||||
|
run: docker buildx imagetools inspect mauriceboe/trek:latest-pre
|
||||||
@@ -7,6 +7,16 @@ on:
|
|||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- '**/*.md'
|
- '**/*.md'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
bump:
|
||||||
|
description: 'Force bump line (auto = patch/finalize as today)'
|
||||||
|
type: choice
|
||||||
|
options: [auto, patch, minor, major]
|
||||||
|
default: auto
|
||||||
|
confirm_major:
|
||||||
|
description: "Type MAJOR (all caps) to confirm a major release"
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -20,39 +30,61 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Determine bump type and update version
|
- name: Determine bump type and update version
|
||||||
id: bump
|
id: bump
|
||||||
run: |
|
run: |
|
||||||
# Check if this push is a merge commit from dev branch
|
git fetch --tags
|
||||||
COMMIT_MSG=$(git log -1 --pretty=%s)
|
|
||||||
PARENT_COUNT=$(git log -1 --pretty=%p | wc -w)
|
|
||||||
|
|
||||||
if echo "$COMMIT_MSG" | grep -qiE "^Merge (pull request|branch).*dev"; then
|
# Derive version from git tags — no package.json dependency
|
||||||
|
STABLE_TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | grep -v '\-pre\.' | sort -V | tail -1)
|
||||||
|
STABLE="${STABLE_TAG#v}"
|
||||||
|
|
||||||
|
PRE_TAG=$(git tag -l 'v*-pre.*' | sort -V | tail -1)
|
||||||
|
|
||||||
|
BUMP_INPUT="${{ github.event.inputs.bump || 'auto' }}"
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$STABLE"
|
||||||
|
|
||||||
|
if [ "$BUMP_INPUT" = "major" ]; then
|
||||||
|
if [ "${{ github.event.inputs.confirm_major }}" != "MAJOR" ]; then
|
||||||
|
echo "::error::confirm_major must equal 'MAJOR' to cut a major release"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
NEW_VERSION="$((MAJOR + 1)).0.0"
|
||||||
|
BUMP="major"
|
||||||
|
elif [ "$BUMP_INPUT" = "minor" ]; then
|
||||||
|
NEW_VERSION="${MAJOR}.$((MINOR + 1)).0"
|
||||||
BUMP="minor"
|
BUMP="minor"
|
||||||
elif [ "$PARENT_COUNT" -gt 1 ] && git log -1 --pretty=%P | xargs -n1 git branch -r --contains 2>/dev/null | grep -q "origin/dev"; then
|
elif [ "$BUMP_INPUT" = "patch" ]; then
|
||||||
BUMP="minor"
|
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
|
||||||
else
|
|
||||||
BUMP="patch"
|
BUMP="patch"
|
||||||
|
else
|
||||||
|
# auto: finalize in-flight prerelease if one exists, else patch
|
||||||
|
if [ -n "$PRE_TAG" ]; then
|
||||||
|
PRE_BASE="${PRE_TAG#v}"
|
||||||
|
PRE_BASE="${PRE_BASE%-pre.*}"
|
||||||
|
# If prerelease base is strictly greater than stable, finalize it
|
||||||
|
HIGHEST=$(printf '%s\n' "$PRE_BASE" "$STABLE" | sort -V | tail -1)
|
||||||
|
if [ "$HIGHEST" = "$PRE_BASE" ] && [ "$PRE_BASE" != "$STABLE" ]; then
|
||||||
|
NEW_VERSION="$PRE_BASE"
|
||||||
|
BUMP="finalize"
|
||||||
|
else
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||||
|
BUMP="patch"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||||
|
BUMP="patch"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Bump type: $BUMP"
|
echo "Bump type: $BUMP"
|
||||||
|
|
||||||
# Read current version
|
|
||||||
CURRENT=$(node -p "require('./server/package.json').version")
|
|
||||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
|
||||||
|
|
||||||
if [ "$BUMP" = "minor" ]; then
|
|
||||||
MINOR=$((MINOR + 1))
|
|
||||||
PATCH=0
|
|
||||||
else
|
|
||||||
PATCH=$((PATCH + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
|
||||||
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "$CURRENT → $NEW_VERSION ($BUMP)"
|
echo "$STABLE → $NEW_VERSION ($BUMP)"
|
||||||
|
|
||||||
# Update package.json files and Helm chart
|
# Update package.json files and Helm chart
|
||||||
cd server && npm version "$NEW_VERSION" --no-git-tag-version && cd ..
|
cd server && npm version "$NEW_VERSION" --no-git-tag-version && cd ..
|
||||||
@@ -102,6 +134,8 @@ jobs:
|
|||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
outputs: type=image,name=mauriceboe/trek,push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,name=mauriceboe/trek,push-by-digest=true,name-canonical=true,push=true
|
||||||
no-cache: true
|
no-cache: true
|
||||||
|
build-args: |
|
||||||
|
APP_VERSION=${{ needs.version-bump.outputs.version }}
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
run: |
|
run: |
|
||||||
@@ -144,10 +178,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
VERSION=${{ needs.version-bump.outputs.version }}
|
VERSION=${{ needs.version-bump.outputs.version }}
|
||||||
mapfile -t digests < <(printf 'mauriceboe/trek@sha256:%s\n' *)
|
mapfile -t digests < <(printf 'mauriceboe/trek@sha256:%s\n' *)
|
||||||
|
MAJOR_TAG="$(echo "$VERSION" | cut -d. -f1)"
|
||||||
docker buildx imagetools create \
|
docker buildx imagetools create \
|
||||||
-t mauriceboe/trek:latest \
|
-t mauriceboe/trek:latest \
|
||||||
|
-t mauriceboe/trek:$MAJOR_TAG \
|
||||||
-t mauriceboe/trek:$VERSION \
|
-t mauriceboe/trek:$VERSION \
|
||||||
-t mauriceboe/nomad:latest \
|
-t mauriceboe/nomad:latest \
|
||||||
|
-t mauriceboe/nomad:$MAJOR_TAG \
|
||||||
-t mauriceboe/nomad:$VERSION \
|
-t mauriceboe/nomad:$VERSION \
|
||||||
"${digests[@]}"
|
"${digests[@]}"
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ RUN mkdir -p /app/data/logs /app/uploads/files /app/uploads/covers /app/uploads/
|
|||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
|
ARG APP_VERSION=dev
|
||||||
|
ENV APP_VERSION=${APP_VERSION}
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -88,16 +88,17 @@ function RootRedirect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { loadUser, isAuthenticated, demoMode, setDemoMode, setDevMode, setHasMapsKey, setServerTimezone, setAppRequireMfa, setTripRemindersEnabled } = useAuthStore()
|
const { loadUser, isAuthenticated, demoMode, setDemoMode, setDevMode, setIsPrerelease, setHasMapsKey, setServerTimezone, setAppRequireMfa, setTripRemindersEnabled } = useAuthStore()
|
||||||
const { loadSettings } = useSettingsStore()
|
const { loadSettings } = useSettingsStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!location.pathname.startsWith('/shared/') && !location.pathname.startsWith('/public/') && !location.pathname.startsWith('/login')) {
|
if (!location.pathname.startsWith('/shared/') && !location.pathname.startsWith('/public/') && !location.pathname.startsWith('/login')) {
|
||||||
loadUser()
|
loadUser()
|
||||||
}
|
}
|
||||||
authApi.getAppConfig().then(async (config: { demo_mode?: boolean; dev_mode?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean; permissions?: Record<string, PermissionLevel> }) => {
|
authApi.getAppConfig().then(async (config: { demo_mode?: boolean; dev_mode?: boolean; is_prerelease?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean; permissions?: Record<string, PermissionLevel> }) => {
|
||||||
if (config?.demo_mode) setDemoMode(true)
|
if (config?.demo_mode) setDemoMode(true)
|
||||||
if (config?.dev_mode) setDevMode(true)
|
if (config?.dev_mode) setDevMode(true)
|
||||||
|
if (config?.is_prerelease !== undefined) setIsPrerelease(config.is_prerelease)
|
||||||
if (config?.has_maps_key !== undefined) setHasMapsKey(config.has_maps_key)
|
if (config?.has_maps_key !== undefined) setHasMapsKey(config.has_maps_key)
|
||||||
if (config?.timezone) setServerTimezone(config.timezone)
|
if (config?.timezone) setServerTimezone(config.timezone)
|
||||||
if (config?.require_mfa !== undefined) setAppRequireMfa(!!config.require_mfa)
|
if (config?.require_mfa !== undefined) setAppRequireMfa(!!config.require_mfa)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import apiClient from '../../api/client'
|
|||||||
const REPO = 'mauriceboe/TREK'
|
const REPO = 'mauriceboe/TREK'
|
||||||
const PER_PAGE = 10
|
const PER_PAGE = 10
|
||||||
|
|
||||||
export default function GitHubPanel() {
|
export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: boolean }) {
|
||||||
const { t, language } = useTranslation()
|
const { t, language } = useTranslation()
|
||||||
const [releases, setReleases] = useState([])
|
const [releases, setReleases] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -273,7 +273,7 @@ export default function GitHubPanel() {
|
|||||||
<div className="absolute left-[11px] top-3 bottom-3 w-px" style={{ background: 'var(--border-primary)' }} />
|
<div className="absolute left-[11px] top-3 bottom-3 w-px" style={{ background: 'var(--border-primary)' }} />
|
||||||
|
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
{releases.map((release, idx) => {
|
{(isPrerelease ? releases : releases.filter((r: any) => !r.prerelease)).map((release, idx) => {
|
||||||
const isLatest = idx === 0
|
const isLatest = idx === 0
|
||||||
const isExpanded = expanded[release.id]
|
const isExpanded = expanded[release.id]
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ interface Addon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }: NavbarProps): React.ReactElement {
|
export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }: NavbarProps): React.ReactElement {
|
||||||
const { user, logout } = useAuthStore()
|
const { user, logout, isPrerelease } = useAuthStore()
|
||||||
const { settings, updateSetting } = useSettingsStore()
|
const { settings, updateSetting } = useSettingsStore()
|
||||||
const { addons: allAddons, loadAddons } = useAddonStore()
|
const { addons: allAddons, loadAddons } = useAddonStore()
|
||||||
const { t, locale } = useTranslation()
|
const { t, locale } = useTranslation()
|
||||||
@@ -155,6 +155,17 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }:
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Prerelease badge */}
|
||||||
|
{isPrerelease && appVersion && (
|
||||||
|
<span
|
||||||
|
className="hidden sm:flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-semibold flex-shrink-0"
|
||||||
|
style={{ background: 'rgba(245,158,11,0.15)', color: '#d97706', border: '1px solid rgba(245,158,11,0.3)' }}
|
||||||
|
>
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: '#f59e0b' }} />
|
||||||
|
{appVersion}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Dark mode toggle (light ↔ dark, overrides auto) — hidden on mobile */}
|
{/* Dark mode toggle (light ↔ dark, overrides auto) — hidden on mobile */}
|
||||||
<button onClick={toggleDarkMode} title={dark ? t('nav.lightMode') : t('nav.darkMode')}
|
<button onClick={toggleDarkMode} title={dark ? t('nav.lightMode') : t('nav.darkMode')}
|
||||||
className="p-2 rounded-lg transition-colors flex-shrink-0 hidden sm:flex"
|
className="p-2 rounded-lg transition-colors flex-shrink-0 hidden sm:flex"
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ interface UpdateInfo {
|
|||||||
current: string
|
current: string
|
||||||
release_url?: string
|
release_url?: string
|
||||||
is_docker?: boolean
|
is_docker?: boolean
|
||||||
|
is_prerelease?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADMIN_EVENT_LABEL_KEYS: Record<string, string> = {
|
const ADMIN_EVENT_LABEL_KEYS: Record<string, string> = {
|
||||||
@@ -1370,7 +1371,7 @@ export default function AdminPage(): React.ReactElement {
|
|||||||
|
|
||||||
{activeTab === 'mcp-tokens' && <AdminMcpTokensPanel />}
|
{activeTab === 'mcp-tokens' && <AdminMcpTokensPanel />}
|
||||||
|
|
||||||
{activeTab === 'github' && <GitHubPanel />}
|
{activeTab === 'github' && <GitHubPanel isPrerelease={updateInfo?.is_prerelease ?? false} />}
|
||||||
|
|
||||||
{activeTab === 'dev-notifications' && <DevNotificationsPanel />}
|
{activeTab === 'dev-notifications' && <DevNotificationsPanel />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ interface AuthState {
|
|||||||
error: string | null
|
error: string | null
|
||||||
demoMode: boolean
|
demoMode: boolean
|
||||||
devMode: boolean
|
devMode: boolean
|
||||||
|
isPrerelease: boolean
|
||||||
hasMapsKey: boolean
|
hasMapsKey: boolean
|
||||||
serverTimezone: string
|
serverTimezone: string
|
||||||
/** Server policy: all users must enable MFA */
|
/** Server policy: all users must enable MFA */
|
||||||
@@ -41,6 +42,7 @@ interface AuthState {
|
|||||||
deleteAvatar: () => Promise<void>
|
deleteAvatar: () => Promise<void>
|
||||||
setDemoMode: (val: boolean) => void
|
setDemoMode: (val: boolean) => void
|
||||||
setDevMode: (val: boolean) => void
|
setDevMode: (val: boolean) => void
|
||||||
|
setIsPrerelease: (val: boolean) => void
|
||||||
setHasMapsKey: (val: boolean) => void
|
setHasMapsKey: (val: boolean) => void
|
||||||
setServerTimezone: (tz: string) => void
|
setServerTimezone: (tz: string) => void
|
||||||
setAppRequireMfa: (val: boolean) => void
|
setAppRequireMfa: (val: boolean) => void
|
||||||
@@ -58,6 +60,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
error: null,
|
error: null,
|
||||||
demoMode: localStorage.getItem('demo_mode') === 'true',
|
demoMode: localStorage.getItem('demo_mode') === 'true',
|
||||||
devMode: false,
|
devMode: false,
|
||||||
|
isPrerelease: false,
|
||||||
hasMapsKey: false,
|
hasMapsKey: false,
|
||||||
serverTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
serverTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
appRequireMfa: false,
|
appRequireMfa: false,
|
||||||
@@ -222,6 +225,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
setDevMode: (val: boolean) => set({ devMode: val }),
|
setDevMode: (val: boolean) => set({ devMode: val }),
|
||||||
|
setIsPrerelease: (val: boolean) => set({ isPrerelease: val }),
|
||||||
setHasMapsKey: (val: boolean) => set({ hasMapsKey: val }),
|
setHasMapsKey: (val: boolean) => set({ hasMapsKey: val }),
|
||||||
setServerTimezone: (tz: string) => set({ serverTimezone: tz }),
|
setServerTimezone: (tz: string) => set({ serverTimezone: tz }),
|
||||||
setAppRequireMfa: (val: boolean) => set({ appRequireMfa: val }),
|
setAppRequireMfa: (val: boolean) => set({ appRequireMfa: val }),
|
||||||
|
|||||||
@@ -304,19 +304,41 @@ export async function getGithubReleases(perPage: string = '10', page: string = '
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function checkVersion() {
|
export async function checkVersion() {
|
||||||
const { version: currentVersion } = require('../../package.json');
|
const currentVersion: string = process.env.APP_VERSION ?? require('../../package.json').version;
|
||||||
|
const isPrerelease = currentVersion.includes('-pre.');
|
||||||
|
const fallback = { current: currentVersion, latest: currentVersion, update_available: false, is_docker: isDocker, is_prerelease: isPrerelease };
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(
|
if (isPrerelease) {
|
||||||
'https://api.github.com/repos/mauriceboe/TREK/releases/latest',
|
// Fetch release list and find the newest prerelease
|
||||||
{ headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'TREK-Server' } }
|
const resp = await fetch(
|
||||||
);
|
'https://api.github.com/repos/mauriceboe/TREK/releases?per_page=20',
|
||||||
if (!resp.ok) return { current: currentVersion, latest: currentVersion, update_available: false };
|
{ headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'TREK-Server' } }
|
||||||
const data = await resp.json() as { tag_name?: string; html_url?: string };
|
);
|
||||||
const latest = (data.tag_name || '').replace(/^v/, '');
|
if (!resp.ok) return fallback;
|
||||||
const update_available = latest && latest !== currentVersion && compareVersions(latest, currentVersion) > 0;
|
const data = await resp.json() as Array<{ tag_name?: string; html_url?: string; prerelease?: boolean }>;
|
||||||
return { current: currentVersion, latest, update_available, release_url: data.html_url || '', is_docker: isDocker };
|
const prereleases = Array.isArray(data) ? data.filter(r => r.prerelease) : [];
|
||||||
|
if (!prereleases.length) return fallback;
|
||||||
|
// Sort by version descending and pick highest
|
||||||
|
const sorted = prereleases.sort((a, b) => compareVersions(
|
||||||
|
(b.tag_name || '').replace(/^v/, ''),
|
||||||
|
(a.tag_name || '').replace(/^v/, '')
|
||||||
|
));
|
||||||
|
const latest = (sorted[0].tag_name || '').replace(/^v/, '');
|
||||||
|
const update_available = !!latest && latest !== currentVersion && compareVersions(latest, currentVersion) > 0;
|
||||||
|
return { current: currentVersion, latest, update_available, release_url: sorted[0].html_url || '', is_docker: isDocker, is_prerelease: true };
|
||||||
|
} else {
|
||||||
|
const resp = await fetch(
|
||||||
|
'https://api.github.com/repos/mauriceboe/TREK/releases/latest',
|
||||||
|
{ headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'TREK-Server' } }
|
||||||
|
);
|
||||||
|
if (!resp.ok) return fallback;
|
||||||
|
const data = await resp.json() as { tag_name?: string; html_url?: string };
|
||||||
|
const latest = (data.tag_name || '').replace(/^v/, '');
|
||||||
|
const update_available = !!latest && latest !== currentVersion && compareVersions(latest, currentVersion) > 0;
|
||||||
|
return { current: currentVersion, latest, update_available, release_url: data.html_url || '', is_docker: isDocker, is_prerelease: false };
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return { current: currentVersion, latest: currentVersion, update_available: false, is_docker: isDocker };
|
return fallback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ export function getAppConfig(authenticatedUser: { id: number } | null) {
|
|||||||
const userCount = (db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }).count;
|
const userCount = (db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }).count;
|
||||||
const isDemo = process.env.DEMO_MODE === 'true';
|
const isDemo = process.env.DEMO_MODE === 'true';
|
||||||
const toggles = resolveAuthToggles();
|
const toggles = resolveAuthToggles();
|
||||||
const { version } = require('../../package.json');
|
const version: string = process.env.APP_VERSION ?? require('../../package.json').version;
|
||||||
const hasGoogleKey = !!db.prepare("SELECT maps_api_key FROM users WHERE role = 'admin' AND maps_api_key IS NOT NULL AND maps_api_key != '' LIMIT 1").get();
|
const hasGoogleKey = !!db.prepare("SELECT maps_api_key FROM users WHERE role = 'admin' AND maps_api_key IS NOT NULL AND maps_api_key != '' LIMIT 1").get();
|
||||||
const oidcDisplayName = process.env.OIDC_DISPLAY_NAME ||
|
const oidcDisplayName = process.env.OIDC_DISPLAY_NAME ||
|
||||||
(db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_display_name'").get() as { value: string } | undefined)?.value || null;
|
(db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_display_name'").get() as { value: string } | undefined)?.value || null;
|
||||||
@@ -244,6 +244,7 @@ export function getAppConfig(authenticatedUser: { id: number } | null) {
|
|||||||
has_users: userCount > 0,
|
has_users: userCount > 0,
|
||||||
setup_complete: setupComplete,
|
setup_complete: setupComplete,
|
||||||
version,
|
version,
|
||||||
|
is_prerelease: version.includes('-pre.'),
|
||||||
has_maps_key: hasGoogleKey,
|
has_maps_key: hasGoogleKey,
|
||||||
oidc_configured: oidcConfigured,
|
oidc_configured: oidcConfigured,
|
||||||
oidc_display_name: oidcConfigured ? (oidcDisplayName || 'SSO') : undefined,
|
oidc_display_name: oidcConfigured ? (oidcDisplayName || 'SSO') : undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user