feat: add Docker infrastructure, migrations, CI/CD client, session cleanup, tests
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>
This commit is contained in:
129
cmd/ci-release/main.go
Normal file
129
cmd/ci-release/main.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// 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.")
|
||||
}
|
||||
Reference in New Issue
Block a user