Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d726595fc | ||
|
|
315c22de65 | ||
|
|
19a80d1c74 | ||
|
|
e85bdabf10 | ||
|
|
e2ae729a60 | ||
|
|
8b18dc55c8 | ||
|
|
3e0385d5d9 |
@@ -0,0 +1,116 @@
|
||||
name: Release
|
||||
|
||||
# Overrides the default run name (the tagged commit's message) so the Actions
|
||||
# runs list shows "Release v0.1.0" instead.
|
||||
run-name: "Release ${{ gitea.event.inputs.tag || gitea.ref_name }}"
|
||||
|
||||
# Publishes the games-frontend container image to the Gitea container registry
|
||||
# AND creates a Gitea Release whose body is the annotated tag's message
|
||||
# (generated locally by `make release VERSION=...` via git-cliff). Triggered
|
||||
# by a version tag push, or manually via workflow_dispatch with a tag. Pushing
|
||||
# to main does nothing, so work-in-progress commits never ship. Each release
|
||||
# publishes two image tags: the version (e.g. v0.1.0) and "latest".
|
||||
#
|
||||
# The image carries no backend URL (Vite build runs with no .env); the
|
||||
# backend is injected at container start from VITE_API_URL, so no
|
||||
# build-args or secrets are needed here beyond the registry login.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to release (e.g. v0.1.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
env:
|
||||
# Resolve the target tag for both triggers: explicit input on manual
|
||||
# dispatch, otherwise the pushed tag ref.
|
||||
TAG: ${{ gitea.event.inputs.tag || gitea.ref_name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Full history ensures the tag annotation (the release notes) is present.
|
||||
fetch-depth: 0
|
||||
ref: ${{ gitea.event.inputs.tag || gitea.ref }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.linuxhg.com
|
||||
username: ${{ gitea.actor }}
|
||||
# PAT stored as a repo Actions secret (auto GITHUB_TOKEN lacks package scope in Gitea)
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
# Publishes both the exact version (e.g. v0.1.0) and the movable "latest" tag.
|
||||
# Deployments default to "latest" via ${IMAGE_TAG:-latest} in docker-compose.yml;
|
||||
# pin or roll back by setting IMAGE_TAG in .env.
|
||||
tags: |
|
||||
git.linuxhg.com/games-database/games-frontend:${{ env.TAG }}
|
||||
git.linuxhg.com/games-database/games-frontend:latest
|
||||
|
||||
- name: Create Gitea Release
|
||||
env:
|
||||
# REGISTRY_TOKEN is reused for release creation because Gitea's auto
|
||||
# GITHUB_TOKEN cannot create releases on this instance. The PAT must
|
||||
# carry write:repository scope. Idempotent: re-runs update an existing
|
||||
# release for this tag instead of failing with 409. On any HTTP error
|
||||
# the API response body is printed so a 403 names the missing scope.
|
||||
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
: "${TAG:?TAG is required}"
|
||||
API="https://git.linuxhg.com/api/v1/repos/${REPO}/releases"
|
||||
AUTH="Authorization: token ${TOKEN}"
|
||||
# Release body = the annotated tag's message (the git-cliff notes).
|
||||
BODY="$(git tag -l --format='%(contents)' "${TAG}")"
|
||||
|
||||
# Tags containing a '-' (e.g. v0.1.0-rc1) are published as pre-releases.
|
||||
PRE="false"; case "${TAG}" in *-*) PRE="true";; esac
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg t "${TAG}" --arg n "${TAG}" --arg b "${BODY}" --argjson p "${PRE}" \
|
||||
'{tag_name:$t, name:$n, body:$b, draft:false, prerelease:$p}')
|
||||
|
||||
# POST/PATCH the release, surfacing Gitea's error message on failure
|
||||
# (e.g. "token does not have write scope") instead of failing silently.
|
||||
api_call() {
|
||||
local method="$1" url="$2" resp code rbody
|
||||
resp="$(curl -sS -w '\n%{http_code}' -X "${method}" \
|
||||
-H "${AUTH}" -H "Content-Type: application/json" \
|
||||
-d "${PAYLOAD}" "${url}")"
|
||||
code="$(printf '%s' "${resp}" | tail -n1)"
|
||||
rbody="$(printf '%s' "${resp}" | sed '$d')"
|
||||
if [ "${code}" -ge 400 ]; then
|
||||
echo "::error::Release API ${code} (${method} ${url}): ${rbody}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
EXISTING_ID="$(curl -sS -H "${AUTH}" "${API}/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null || true)"
|
||||
if [ -n "${EXISTING_ID}" ]; then
|
||||
api_call PATCH "${API}/${EXISTING_ID}"
|
||||
echo "Updated existing release id=${EXISTING_ID} for ${TAG}"
|
||||
else
|
||||
api_call POST "${API}"
|
||||
echo "Created new release for ${TAG}"
|
||||
fi
|
||||
+18
-1
@@ -1,2 +1,19 @@
|
||||
# Multi-stage: build the Vite app here so CI produces an env-independent
|
||||
# image (no .env needed at build time — the backend URL is injected at
|
||||
# container start, see docker-entrypoint.d/30-frontend-config.sh).
|
||||
FROM oven/bun:1 AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lockb ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
|
||||
FROM nginx:stable-alpine-perl
|
||||
COPY ./dist /usr/share/nginx/html
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
# nginx's entrypoint auto-runs *.sh in /docker-entrypoint.d/ on start.
|
||||
# Unaffected by mounting a custom /etc/nginx/nginx.conf (separate path).
|
||||
COPY docker-entrypoint.d/ /docker-entrypoint.d/
|
||||
RUN chmod +x /docker-entrypoint.d/*.sh
|
||||
EXPOSE 80
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
.PHONY: help release
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo ""
|
||||
@echo "Release:"
|
||||
@echo " ./release v0.1.0 - Tag, push, and release (notes auto-generated from commits)"
|
||||
@echo " make release VERSION=v0.1.0 - Same, explicit make form"
|
||||
|
||||
# Create an annotated version tag carrying auto-generated release notes (git-cliff)
|
||||
# and push it. The tag push triggers .gitea/workflows/release.yml, which builds the
|
||||
# image and publishes a Gitea Release whose body is this tag's message. Notes come
|
||||
# entirely from Conventional Commits — no hand-written message required.
|
||||
#
|
||||
# git-cliff's --latest needs the tag to exist to scope the notes, so we create a
|
||||
# throwaway lightweight tag, generate the notes, replace it with an annotated tag,
|
||||
# then push. --cleanup=verbatim keeps the markdown "###" group headers (git's
|
||||
# default cleanup would strip lines starting with "#").
|
||||
#
|
||||
# Requires git-cliff: https://git-cliff.org/install
|
||||
# Usage: make release VERSION=v0.1.0
|
||||
release:
|
||||
@test -n "$(VERSION)" || { echo "Usage: make release VERSION=v0.1.0"; exit 1; }
|
||||
@command -v git-cliff >/dev/null 2>&1 || { echo "git-cliff not found — install: https://git-cliff.org/install"; exit 1; }
|
||||
@if git rev-parse "$(VERSION)" >/dev/null 2>&1; then echo "Tag $(VERSION) already exists locally — delete it first: git tag -d $(VERSION)"; exit 1; fi
|
||||
@echo "Generating release notes for $(VERSION)..."
|
||||
@git tag "$(VERSION)" HEAD && \
|
||||
(git cliff --latest --config cliff.toml > .release-notes.tmp && git tag -d "$(VERSION)" >/dev/null) || \
|
||||
{ git tag -d "$(VERSION)" >/dev/null 2>&1; rm -f .release-notes.tmp; echo "git-cliff failed"; exit 1; }
|
||||
@git tag -a --cleanup=verbatim -F .release-notes.tmp "$(VERSION)" HEAD && rm -f .release-notes.tmp
|
||||
@git push origin "$(VERSION)"
|
||||
@echo "Pushed $(VERSION) — Gitea Actions will build the image and publish the Release."
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# git-cliff configuration — generates the body of each Gitea Release from
|
||||
# Conventional Commits accumulated since the previous tag. Used locally by
|
||||
# `make release` with --latest so only the current tag's section is emitted
|
||||
# (no full history, no header — the Gitea Release title is the tag).
|
||||
# Docs: https://git-cliff.org/docs/configuration
|
||||
|
||||
[changelog]
|
||||
header = ""
|
||||
body = """
|
||||
{% for group, commits in commits | group_by(attribute="group") %}\
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits %}\
|
||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}{{ commit.message | upper_first }} ({{ commit.id | truncate(length=7, end="") }})
|
||||
{% endfor %}\
|
||||
{% endfor %}\
|
||||
"""
|
||||
trim = true
|
||||
footer = ""
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = false
|
||||
require_conventional = false
|
||||
split_commits = false
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^refactor", group = "Refactor" },
|
||||
{ message = "^docs", group = "Documentation" },
|
||||
{ message = "^test", group = "Tests" },
|
||||
{ message = "^chore|^ci", group = "Miscellaneous Tasks" },
|
||||
{ message = ".*", group = "Other" },
|
||||
]
|
||||
filter_commits = false
|
||||
tag_pattern = "v[0-9].*"
|
||||
sort_commits = "oldest"
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
# Prod run template. The backend URL is injected at container start
|
||||
# (docker-entrypoint.d/30-frontend-config.sh), so the image carries no env.
|
||||
# Set VITE_API_URL including the /api suffix, e.g.:
|
||||
# VITE_API_URL=https://gamesapi.linuxhg.com/api
|
||||
# Either inline below or via env_file .env (environment wins if both set).
|
||||
# Traefik setups: drop ports, add your labels/networks like the existing
|
||||
# games-frontend service; custom nginx.conf mounts keep working.
|
||||
services:
|
||||
games-frontend:
|
||||
image: git.linuxhg.com/games-database/games-frontend:${IMAGE_TAG:-latest}
|
||||
container_name: games-frontend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- VITE_API_URL=
|
||||
# env_file:
|
||||
# - .env
|
||||
ports:
|
||||
- 80:80
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O /dev/null http://localhost/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
# Generates runtime frontend config from container environment.
|
||||
# Runs automatically at container start via nginx's /docker-entrypoint.d/.
|
||||
# Lets one image talk to any backend: set VITE_API_URL on the service
|
||||
# (compose environment or .env) and restart — no rebuild needed.
|
||||
# The app (src/api/agent.ts) prefers this file, falling back to the
|
||||
# build-time VITE_API_URL when it is absent (local dev, vite preview).
|
||||
set -eu
|
||||
|
||||
CONFIG_FILE="/usr/share/nginx/html/config.js"
|
||||
mkdir -p "$(dirname "${CONFIG_FILE}")"
|
||||
|
||||
if [ -z "${VITE_API_URL:-}" ]; then
|
||||
echo "frontend-config: VITE_API_URL not set, writing empty config (build-time env applies)"
|
||||
printf 'window.__APP_CONFIG__ = {};\n' > "${CONFIG_FILE}"
|
||||
else
|
||||
echo "frontend-config: writing config.js with VITE_API_URL=${VITE_API_URL}"
|
||||
printf 'window.__APP_CONFIG__ = { VITE_API_URL: "%s" };\n' "${VITE_API_URL}" > "${CONFIG_FILE}"
|
||||
fi
|
||||
@@ -32,6 +32,9 @@
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!-- Runtime config (backend URL) injected at container start.
|
||||
See docker-entrypoint.d/30-frontend-config.sh and public/config.js. -->
|
||||
<script src="/config.js"></script>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Placeholder replaced at container start by
|
||||
// docker-entrypoint.d/30-frontend-config.sh, which writes the real
|
||||
// VITE_API_URL from the container environment. Served as /config.js
|
||||
// (see index.html). Empty here so local builds fall back to the
|
||||
// build-time import.meta.env value.
|
||||
window.__APP_CONFIG__ = {};
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env sh
|
||||
# Project-attached wrapper around `make release` so you can run:
|
||||
# ./release v0.1.0 (or) ./release 0.1.0
|
||||
# instead of:
|
||||
# make release VERSION=v0.1.0
|
||||
# Lives in the repo (no machine-specific alias needed).
|
||||
set -eu
|
||||
|
||||
[ "$#" -ge 1 ] || { echo "Usage: ./release v0.1.0" >&2; exit 1; }
|
||||
|
||||
# Accept "0.1.0" or "v0.1.0"; ensure the tag starts with 'v' (the workflow
|
||||
# only triggers on v* tags).
|
||||
VERSION="v${1#v}"
|
||||
|
||||
exec make release "VERSION=${VERSION}"
|
||||
+1
-1
@@ -58,7 +58,7 @@ function App() {
|
||||
<>
|
||||
<ToastContainer position='bottom-right' hideProgressBar />
|
||||
{loggedIn ? <Header /> : ''}
|
||||
<Box mt={loadedPreferences.get().stickyNav ? "20" : "4"} mb={'4'}>
|
||||
<Box mt={loggedIn ? (loadedPreferences.get().stickyNav ? "20" : "4") : "0"} mb={loggedIn ? '4' : '0'}>
|
||||
{/*<Box mt={loadedPreferences.get().stickyNav ? isSmallerThan768 ? "15%" : "9%" : "4"} mb={'4'}>*/}
|
||||
<Routes>
|
||||
<Route path='/*' element={loggedIn ? <Navigate to='games' /> : <Home />} />
|
||||
|
||||
+8
-1
@@ -11,7 +11,14 @@ const sleep = (delay: number) => {
|
||||
setTimeout(resolve, delay)
|
||||
})
|
||||
}
|
||||
axios.defaults.baseURL = import.meta.env.VITE_API_URL
|
||||
// Backend URL: container runtime config first (/config.js, written from
|
||||
// VITE_API_URL at container start), build-time env as fallback for dev.
|
||||
declare global {
|
||||
interface Window {
|
||||
__APP_CONFIG__?: { VITE_API_URL?: string }
|
||||
}
|
||||
}
|
||||
axios.defaults.baseURL = window.__APP_CONFIG__?.VITE_API_URL ?? import.meta.env.VITE_API_URL
|
||||
|
||||
axios.interceptors.request.use((config) => {
|
||||
const token = userToken.get()
|
||||
|
||||
@@ -80,6 +80,7 @@ const ActiveFilters = () => {
|
||||
)
|
||||
}}
|
||||
><Link
|
||||
color="inherit"
|
||||
onMouseDown={() => setSearchModalOpen.set(true)}>{searchParams}</Link>
|
||||
</Tag>
|
||||
</>
|
||||
|
||||
@@ -191,9 +191,10 @@ export default function NavBar() {
|
||||
colorPalette={'blue'}
|
||||
size={'md'}
|
||||
asChild
|
||||
w={'70%'}
|
||||
px={4}
|
||||
borderWidth={0}
|
||||
>
|
||||
<RouteLink to="/games/create"><LuPlus/> <Text mt="1">Create Game</Text></RouteLink>
|
||||
<RouteLink to="/games/create"><LuPlus/><Text mt="1">Create Game</Text></RouteLink>
|
||||
</Button>
|
||||
<Separator/>
|
||||
<Heading as={'h3'} size={'md'}>Keyboard Shortcuts</Heading>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { VStack, Image, Button, Heading, Flex, Center } from '@chakra-ui/react'
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<Flex align={'center'} bgImage={'linear-gradient(to bottom right, #314755, #26a0da)'} height={'100vh'}>
|
||||
<Flex align={'center'} bgImage={'linear-gradient(to bottom right, #314755, #26a0da)'} minH={'100dvh'} w={'100%'}>
|
||||
<Center w={'100%'}>
|
||||
<VStack gap={4} align={'center'} margin={'auto'}>
|
||||
<Image src="/assets/Logo.svg" alt="Games Database Logo"/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { login } from '../../stateManagement/userState'
|
||||
import {
|
||||
Container,
|
||||
Button,
|
||||
Input, HStack, Center,
|
||||
Input, Center, Flex,
|
||||
} from '@chakra-ui/react'
|
||||
import {Field} from '../ui/field'
|
||||
import {useColorModeValue} from '../ui/color-mode'
|
||||
@@ -35,7 +35,8 @@ export default function LoginForm() {
|
||||
)
|
||||
|
||||
return (
|
||||
<Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}>
|
||||
<Flex minH={'100dvh'} w={'100%'} align={'center'} justify={'center'}>
|
||||
<Container maxW={'md'} w={'full'} py={'4'}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Field label="Email" invalid={Boolean(errors.email)} errorText={errors.email?.type === 'required' ? errors.email.message?.toString() : errors.email?.type === 'pattern' ? 'Invalid Email Address' : undefined} mb={'3'}>
|
||||
<Input
|
||||
@@ -65,8 +66,7 @@ export default function LoginForm() {
|
||||
{...register('password', { required: 'You must specify a password' })}
|
||||
/>
|
||||
</Field>
|
||||
<br/>
|
||||
<HStack>
|
||||
<Center mt={'6'}>
|
||||
<Button
|
||||
type="submit"
|
||||
colorPalette="blue"
|
||||
@@ -75,11 +75,12 @@ export default function LoginForm() {
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</HStack>
|
||||
</Center>
|
||||
<Center mt={'5rem'}>
|
||||
<Button asChild variant="outline"><Link to={'/forgotpassword'}>Forgot Password</Link></Button>
|
||||
</Center>
|
||||
</form>
|
||||
</Container>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -349,6 +349,11 @@ const config = defineConfig({
|
||||
bg: { base: 'gray.200', _dark: 'gray.800' },
|
||||
fontSize: '1.0625rem',
|
||||
},
|
||||
// Native text-selection was near-invisible on dark surfaces.
|
||||
// Match the hover-selection treatment (gray.200 / whiteAlpha.300).
|
||||
'::selection': {
|
||||
bg: { base: 'gray.200', _dark: 'whiteAlpha.300' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user