Docker & Deployment: - Add Dockerfile (multi-stage, alpine, non-root) - Add docker-compose.yml (caddy, backend, postgres, watchtower) - Add Caddyfile (TLS, file_server, reverse proxy) - Add .env.example Database: - Add migrations/001_init.sql (all tables + indexes) CI/CD: - Add cmd/ci-release/main.go (launcher binary upload tool) Session management: - Add internal/session/cleanup.go (background expired session cleanup) - Integrate cleanup worker into main.go Bug fixes: - Fix launcherLatest download URL to include version segment - Fix serveLauncherAsset path to match route pattern - Add Content-Type detection from file extension in CAS serveFile - Add empty-field validation in webLogin - Format string fix in ci-release (%d → %s for resp.Status) Tests: - Add internal/auth/auth_test.go (8 tests) - Add internal/cas/cas_test.go (7 tests) - Add internal/session/cleanup_test.go (1 test) - Add internal/api/api_test.go (5 tests) - All tests passing, go vet clean Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
3.0 KiB
Go
130 lines
3.0 KiB
Go
// Command ci-release uploads a new launcher binary to the server.
|
|
//
|
|
// Usage:
|
|
//
|
|
// ci-release \
|
|
// --url https://minecraft.mrixs.me \
|
|
// --token <CI_TOKEN> \
|
|
// --version 1.2.0 \
|
|
// --os windows \
|
|
// --arch amd64 \
|
|
// --file ./build/launcher-windows-amd64.exe
|
|
//
|
|
// SHA-256 is computed automatically from the file.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
func main() {
|
|
var (
|
|
serverURL = flag.String("url", "", "Server base URL (e.g. https://minecraft.mrixs.me)")
|
|
token = flag.String("token", "", "CI token (from CI_SECRET)")
|
|
version = flag.String("version", "", "Launcher version (e.g. 1.2.0)")
|
|
osParam = flag.String("os", "", "Target OS: windows, linux, darwin")
|
|
arch = flag.String("arch", "", "Target arch: amd64, arm64")
|
|
filePath = flag.String("file", "", "Path to launcher binary")
|
|
)
|
|
flag.Parse()
|
|
|
|
// Validate required flags.
|
|
missing := false
|
|
for _, f := range []struct{ name, value string }{
|
|
{"--url", *serverURL},
|
|
{"--token", *token},
|
|
{"--version", *version},
|
|
{"--os", *osParam},
|
|
{"--arch", *arch},
|
|
{"--file", *filePath},
|
|
} {
|
|
if f.value == "" {
|
|
fmt.Fprintf(os.Stderr, "ERROR: missing required flag %s\n", f.name)
|
|
missing = true
|
|
}
|
|
}
|
|
if missing {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Read the binary.
|
|
data, err := os.ReadFile(*filePath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "ERROR reading file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Compute SHA-256.
|
|
hash := sha256.Sum256(data)
|
|
sha256hex := hex.EncodeToString(hash[:])
|
|
|
|
fmt.Printf("Uploading %s (%s/%s) v%s (%d bytes, sha256=%s)\n",
|
|
filepath.Base(*filePath), *osParam, *arch, *version, len(data), sha256hex)
|
|
|
|
// Build multipart form.
|
|
var body bytes.Buffer
|
|
w := multipart.NewWriter(&body)
|
|
|
|
// Text fields.
|
|
for _, f := range []struct{ name, value string }{
|
|
{"version", *version},
|
|
{"os", *osParam},
|
|
{"arch", *arch},
|
|
{"sha256", sha256hex},
|
|
} {
|
|
if err := w.WriteField(f.name, f.value); err != nil {
|
|
fmt.Fprintf(os.Stderr, "ERROR building form: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// File part.
|
|
fw, err := w.CreateFormFile("file", filepath.Base(*filePath))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "ERROR creating form file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if _, err := fw.Write(data); err != nil {
|
|
fmt.Fprintf(os.Stderr, "ERROR writing file data: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
w.Close()
|
|
|
|
// Send request.
|
|
url := *serverURL + "/api/admin/launcher/release"
|
|
req, err := http.NewRequest("POST", url, &body)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "ERROR creating request: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
req.Header.Set("Content-Type", w.FormDataContentType())
|
|
req.Header.Set("X-CI-Token", *token)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "ERROR sending request: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
fmt.Printf("HTTP %s: %s\n", resp.Status, string(respBody))
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println("✓ Release uploaded successfully.")
|
|
}
|