mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-16 20:47:55 +00:00
Add gh workflow for release, new fields in podium and versioning
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
name: Release Go Binaries
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Binary
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, darwin, windows]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: server/go.mod
|
||||
|
||||
- name: Build Binary
|
||||
working-directory: server
|
||||
run: |
|
||||
mkdir -p bin
|
||||
OUT_NAME="poc-server-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||
OUT_NAME="${OUT_NAME}.exe"
|
||||
fi
|
||||
echo "Building for ${{ matrix.goos }}/${{ matrix.goarch }}..."
|
||||
CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -ldflags "-s -w -X main.serverVersion=${{ github.ref_name }}" -o bin/${OUT_NAME} ./cmd/poc-server
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: poc-server-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: server/bin/
|
||||
|
||||
release:
|
||||
name: Create GitHub Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: release-binaries
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "Creating GitHub Release for tag ${{ github.ref_name }}..."
|
||||
gh release create "${{ github.ref_name }}" \
|
||||
--title "Release ${{ github.ref_name }}" \
|
||||
--generate-notes \
|
||||
release-binaries/*
|
||||
@@ -62,7 +62,7 @@ import (
|
||||
"github.com/x0gp/server/internal/transport"
|
||||
)
|
||||
|
||||
const serverVersion = "0.1.0-poc"
|
||||
var serverVersion = "0.1.0-poc"
|
||||
|
||||
var (
|
||||
upgrader = websocket.Upgrader{
|
||||
@@ -95,9 +95,15 @@ func main() {
|
||||
devMode = flag.Bool("dev", true, "enable development mode")
|
||||
seedRaces = flag.Bool("seed-races", false, "seed mock race data (finished/live/plans) on startup")
|
||||
seedReset = flag.Bool("reset", false, "with --seed-races, wipe seed-managed data before inserting")
|
||||
version = flag.Bool("version", false, "print version and exit")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *version {
|
||||
fmt.Println(serverVersion)
|
||||
return
|
||||
}
|
||||
|
||||
if *addr != "" {
|
||||
_ = os.Setenv("X0GP_HTTP_ADDR", *addr)
|
||||
}
|
||||
@@ -118,7 +124,7 @@ func main() {
|
||||
}
|
||||
|
||||
logger := newLogger(cfg.LogLevel)
|
||||
logger.Info("starting x0gp poc-server",
|
||||
logger.Info(fmt.Sprintf("starting x0gp poc-server version %s", serverVersion),
|
||||
"version", serverVersion,
|
||||
"addr", cfg.HTTPAddr,
|
||||
"tick_hz", cfg.TickRate,
|
||||
@@ -276,6 +282,7 @@ func main() {
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/health", healthHandler(hub, engine))
|
||||
mux.HandleFunc("/api/version", versionHandler())
|
||||
mux.HandleFunc("/stats", statsHandler(hub, engine))
|
||||
mux.HandleFunc("/api/catalog", catalogHandler(cat))
|
||||
mux.HandleFunc("/api/tracks", tracksRouter(cat))
|
||||
@@ -366,6 +373,23 @@ func (h *Hub) BroadcastSnapshot(snap transport.RaceSnapshot) {
|
||||
|
||||
// HTTP handlers ------------------------------------------------------------
|
||||
|
||||
// versionHandler godoc
|
||||
// @Summary Get server version
|
||||
// @Description Returns the current server version.
|
||||
// @Tags system
|
||||
// @Produce json
|
||||
// @Success 200 {object} transport.VersionResponse "Success"
|
||||
// @Router /api/version [get]
|
||||
func versionHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(transport.VersionResponse{
|
||||
Version: serverVersion,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// healthHandler godoc
|
||||
// @Summary Liveness probe
|
||||
// @Description Returns 200 OK with current engine phase, WS connection count and total snapshot drops. Intended for load-balancer health checks.
|
||||
|
||||
@@ -485,6 +485,9 @@ func lobbyToWireFinished(m lobby.RaceMeta, podium []races.PodiumEntry) transport
|
||||
DriverID: p.DriverID,
|
||||
Name: p.Name,
|
||||
TotalTimeMs: p.TotalTimeMs,
|
||||
Nickname: p.Nickname,
|
||||
AvatarUrl: p.AvatarUrl,
|
||||
ClanID: p.ClanID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1750,6 +1750,26 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/version": {
|
||||
"get": {
|
||||
"description": "Returns the current server version.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"system"
|
||||
],
|
||||
"summary": "Get server version",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/transport.VersionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/video/control": {
|
||||
"post": {
|
||||
"description": "Sends a text command (e.g. start, stop, left, right) to the ESP32 command port (UDP 8888).",
|
||||
@@ -2552,6 +2572,13 @@ const docTemplate = `{
|
||||
"transport.RacePodiumEntry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"clan_id": {
|
||||
"type": "string",
|
||||
"example": "clan-bob"
|
||||
},
|
||||
"driver_id": {
|
||||
"type": "string",
|
||||
"example": "driver-alice"
|
||||
@@ -2560,6 +2587,10 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"example": "driver-alice"
|
||||
},
|
||||
"nickname": {
|
||||
"type": "string",
|
||||
"example": "driver-alice"
|
||||
},
|
||||
"position": {
|
||||
"type": "integer",
|
||||
"example": 1
|
||||
@@ -2851,6 +2882,15 @@ const docTemplate = `{
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transport.VersionResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "0.1.0-poc"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
@@ -1748,6 +1748,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/version": {
|
||||
"get": {
|
||||
"description": "Returns the current server version.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"system"
|
||||
],
|
||||
"summary": "Get server version",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/transport.VersionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/video/control": {
|
||||
"post": {
|
||||
"description": "Sends a text command (e.g. start, stop, left, right) to the ESP32 command port (UDP 8888).",
|
||||
@@ -2550,6 +2570,13 @@
|
||||
"transport.RacePodiumEntry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"clan_id": {
|
||||
"type": "string",
|
||||
"example": "clan-bob"
|
||||
},
|
||||
"driver_id": {
|
||||
"type": "string",
|
||||
"example": "driver-alice"
|
||||
@@ -2558,6 +2585,10 @@
|
||||
"type": "string",
|
||||
"example": "driver-alice"
|
||||
},
|
||||
"nickname": {
|
||||
"type": "string",
|
||||
"example": "driver-alice"
|
||||
},
|
||||
"position": {
|
||||
"type": "integer",
|
||||
"example": 1
|
||||
@@ -2849,6 +2880,15 @@
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transport.VersionResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "0.1.0-poc"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,12 +478,20 @@ definitions:
|
||||
type: object
|
||||
transport.RacePodiumEntry:
|
||||
properties:
|
||||
avatar_url:
|
||||
type: string
|
||||
clan_id:
|
||||
example: clan-bob
|
||||
type: string
|
||||
driver_id:
|
||||
example: driver-alice
|
||||
type: string
|
||||
name:
|
||||
example: driver-alice
|
||||
type: string
|
||||
nickname:
|
||||
example: driver-alice
|
||||
type: string
|
||||
position:
|
||||
example: 1
|
||||
type: integer
|
||||
@@ -677,6 +685,12 @@ definitions:
|
||||
width_m:
|
||||
type: number
|
||||
type: object
|
||||
transport.VersionResponse:
|
||||
properties:
|
||||
version:
|
||||
example: 0.1.0-poc
|
||||
type: string
|
||||
type: object
|
||||
host: localhost:8080
|
||||
info:
|
||||
contact:
|
||||
@@ -1894,6 +1908,19 @@ paths:
|
||||
summary: Get / update / delete a track by id
|
||||
tags:
|
||||
- tracks
|
||||
/api/version:
|
||||
get:
|
||||
description: Returns the current server version.
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Success
|
||||
schema:
|
||||
$ref: '#/definitions/transport.VersionResponse'
|
||||
summary: Get server version
|
||||
tags:
|
||||
- system
|
||||
/api/video/control:
|
||||
post:
|
||||
description: Sends a text command (e.g. start, stop, left, right) to the ESP32
|
||||
|
||||
@@ -22,6 +22,9 @@ type PodiumEntry struct {
|
||||
DriverID string `json:"driver_id" example:"driver-alice"`
|
||||
Name string `json:"name" example:"driver-alice"`
|
||||
TotalTimeMs int64 `json:"total_time_ms" example:"123456"`
|
||||
Nickname string `json:"nickname" example:"driver-alice"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
ClanID string `json:"clan_id" example:"clan-bob"`
|
||||
}
|
||||
|
||||
// DriverResult is the per-driver outcome of a race. Stored in
|
||||
@@ -31,9 +34,13 @@ type PodiumEntry struct {
|
||||
// fields are derived from this row set on read.
|
||||
type DriverResult struct {
|
||||
DriverID string
|
||||
Name string
|
||||
TotalTimeMs int64
|
||||
BestLapMs int64
|
||||
Position *int // nil for DNF
|
||||
Nickname string `json:"nickname" example:"driver-alice"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
ClanID string `json:"clan_id" example:"clan-bob"`
|
||||
}
|
||||
|
||||
// FinishedRace is the on-disk shape of a completed race. Mirrors the
|
||||
@@ -587,8 +594,8 @@ func (s *PgStore) scanFinished(ctx context.Context, rows pgx.Rows) ([]FinishedRa
|
||||
// and derives the Winner* and Podium fields.
|
||||
func (s *PgStore) hydrateFinished(ctx context.Context, r *FinishedRace) error {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT driver_id, total_time_ms, best_lap_ms, position
|
||||
FROM race_drivers
|
||||
SELECT rd.driver_id, rd.total_time_ms, rd.best_lap_ms, rd.position, d.name, d.nickname, d.avatar_url, d.clan_id
|
||||
FROM race_drivers rd left join drivers d on rd.driver_id = d.id
|
||||
WHERE race_id = $1
|
||||
ORDER BY slot ASC, joined_ms ASC`, r.ID)
|
||||
if err != nil {
|
||||
@@ -602,10 +609,28 @@ func (s *PgStore) hydrateFinished(ctx context.Context, r *FinishedRace) error {
|
||||
for rows.Next() {
|
||||
var dr DriverResult
|
||||
var pos *int
|
||||
if err := rows.Scan(&dr.DriverID, &dr.TotalTimeMs, &dr.BestLapMs, &pos); err != nil {
|
||||
var name, nick, avatar, clanID *string
|
||||
if err := rows.Scan(
|
||||
&dr.DriverID, &dr.TotalTimeMs, &dr.BestLapMs, &pos,
|
||||
&name, &nick, &avatar, &clanID,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
dr.Position = pos
|
||||
if name != nil {
|
||||
dr.Name = *name
|
||||
} else {
|
||||
dr.Name = dr.DriverID
|
||||
}
|
||||
if nick != nil {
|
||||
dr.Nickname = *nick
|
||||
}
|
||||
if avatar != nil {
|
||||
dr.AvatarUrl = *avatar
|
||||
}
|
||||
if clanID != nil {
|
||||
dr.ClanID = *clanID
|
||||
}
|
||||
r.Results = append(r.Results, dr)
|
||||
r.DriverIDs = append(r.DriverIDs, dr.DriverID)
|
||||
}
|
||||
@@ -621,7 +646,7 @@ func (s *PgStore) hydrateFinished(ctx context.Context, r *FinishedRace) error {
|
||||
if bestTotal == -1 || dr.TotalTimeMs < bestTotal {
|
||||
bestTotal = dr.TotalTimeMs
|
||||
r.WinnerDriverID = dr.DriverID
|
||||
r.WinnerName = dr.DriverID
|
||||
r.WinnerName = dr.Name
|
||||
}
|
||||
if dr.BestLapMs > 0 && (r.BestLapMs == 0 || dr.BestLapMs < r.BestLapMs) {
|
||||
r.BestLapMs = dr.BestLapMs
|
||||
@@ -632,13 +657,25 @@ func (s *PgStore) hydrateFinished(ctx context.Context, r *FinishedRace) error {
|
||||
id string
|
||||
t int64
|
||||
best int64
|
||||
name string
|
||||
nick string
|
||||
avatar string
|
||||
clanID string
|
||||
}
|
||||
var sorted []row
|
||||
for _, dr := range r.Results {
|
||||
if dr.TotalTimeMs <= 0 {
|
||||
continue
|
||||
}
|
||||
sorted = append(sorted, row{dr.DriverID, dr.TotalTimeMs, dr.BestLapMs})
|
||||
sorted = append(sorted, row{
|
||||
id: dr.DriverID,
|
||||
t: dr.TotalTimeMs,
|
||||
best: dr.BestLapMs,
|
||||
name: dr.Name,
|
||||
nick: dr.Nickname,
|
||||
avatar: dr.AvatarUrl,
|
||||
clanID: dr.ClanID,
|
||||
})
|
||||
}
|
||||
for i := 1; i < len(sorted); i++ {
|
||||
for j := i; j > 0 && sorted[j-1].t > sorted[j].t; j-- {
|
||||
@@ -649,8 +686,11 @@ func (s *PgStore) hydrateFinished(ctx context.Context, r *FinishedRace) error {
|
||||
r.Podium = append(r.Podium, PodiumEntry{
|
||||
Position: i + 1,
|
||||
DriverID: sorted[i].id,
|
||||
Name: sorted[i].id,
|
||||
Name: sorted[i].name,
|
||||
TotalTimeMs: sorted[i].t,
|
||||
Nickname: sorted[i].nick,
|
||||
AvatarUrl: sorted[i].avatar,
|
||||
ClanID: sorted[i].clanID,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -229,6 +229,14 @@ type RacePodiumEntry struct {
|
||||
DriverID string `json:"driver_id" example:"driver-alice"`
|
||||
Name string `json:"name" example:"driver-alice"`
|
||||
TotalTimeMs int64 `json:"total_time_ms" example:"123456"`
|
||||
Nickname string `json:"nickname" example:"driver-alice"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
ClanID string `json:"clan_id" example:"clan-bob"`
|
||||
}
|
||||
|
||||
// VersionResponse is the body of the version API.
|
||||
type VersionResponse struct {
|
||||
Version string `json:"version" example:"0.1.0-poc"`
|
||||
}
|
||||
|
||||
// LobbyDriver is the wire form of lobby.DriverMeta.
|
||||
|
||||
Reference in New Issue
Block a user