mirror of
https://github.com/hanzoai/ci.git
synced 2026-10-20 15:28:05 +00:00
the dashboard can be mounted, not only run
It was package main, so nothing could import it — and that is why this surface is the one a valid hanzo.id token cannot read. api.hanzo.ai already carries /v1/deploy, the CD half of this same delivery plane, behind the gateway's IAM identity; the CI half sits behind admin-guard instead, which trusts an X-Org-Id header and answers a bearer with a bare 401. A capability can be mounted; a package main can only be deployed beside things. Serve builds the handler and starts the pollers, taking a context so the caller owns their lifetime. Both caches answer "no snapshot yet" rather than blocking, so a mount never waits on an upstream fetch. Main keeps the standalone server — listener and shutdown — and cmd/ci is four lines over it, so the image is unchanged apart from the path it builds. Main, not Run: a Run in this package is a CI run, the domain object, and the binary's body is not one. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+1
-1
@@ -25,7 +25,7 @@ RUN --mount=type=cache,id=ci-gomod,target=/go/pkg/mod go mod download
|
||||
COPY . .
|
||||
RUN --mount=type=cache,id=ci-gomod,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /build/ci .
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /build/ci ./cmd/ci
|
||||
|
||||
FROM alpine:3.21
|
||||
RUN apk add --no-cache ca-certificates tzdata \
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package ci
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Command ci runs the fleet dashboard as its own server.
|
||||
//
|
||||
// The surface itself lives in the package, not here, so it can be MOUNTED as
|
||||
// well as run — hanzoai/cloud carries /v1/deploy behind the gateway's IAM
|
||||
// identity, and this is the CI half of that same delivery plane. A package main
|
||||
// cannot be imported by anything, which is why this surface was the one a valid
|
||||
// hanzo.id token could not read.
|
||||
package main
|
||||
|
||||
import ci "hanzo.ai/ci"
|
||||
|
||||
func main() { ci.Main() }
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package ci
|
||||
|
||||
import "strings"
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package ci
|
||||
|
||||
import "testing"
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package ci
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
// by org, IAM issues that slug in the `owner` claim, and Hanzo CD fences
|
||||
// projects by it. Filtering here by `org` is therefore the same boundary those
|
||||
// enforce, not a parallel notion of who-sees-what.
|
||||
package main
|
||||
package ci
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -43,15 +43,17 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
logger.Error("config", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Serve builds the dashboard's handler and starts the pollers that feed it.
|
||||
//
|
||||
// Exported so this surface can be MOUNTED as well as run: hanzoai/cloud carries
|
||||
// /v1/deploy behind the gateway's IAM identity, and this is the CI half of that
|
||||
// same delivery plane. A package main cannot be mounted by anything, which is
|
||||
// why it was the one surface a valid hanzo.id token could not read.
|
||||
//
|
||||
// The caller owns the lifetime: cancelling ctx stops the pollers. The returned
|
||||
// handler is safe to serve immediately — both caches answer "no snapshot yet"
|
||||
// rather than blocking, so a mount never waits on an upstream fetch.
|
||||
func Serve(ctx context.Context, logger *slog.Logger, cfg config) http.Handler {
|
||||
src := &gitSource{base: cfg.gitBase, token: cfg.gitToken, http: &http.Client{Timeout: 20 * time.Second}}
|
||||
cache := &runCache{}
|
||||
board := &fleetCache{}
|
||||
@@ -65,9 +67,6 @@ func main() {
|
||||
logger.Warn("cluster identity", "err", err)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
// One poller, one cache. Every viewer reads the same snapshot, so N open
|
||||
// dashboards cost Hanzo Git exactly as much as one — a dashboard that
|
||||
// fanned each page load into upstream calls is how a status page takes the
|
||||
@@ -75,9 +74,28 @@ func main() {
|
||||
go poll(ctx, logger, src, cache, cfg)
|
||||
go watch(ctx, logger, src, cl, board, cfg)
|
||||
|
||||
return routes(cfg, cache, board)
|
||||
}
|
||||
|
||||
// Main is the standalone server: the same handler, with a listener and shutdown
|
||||
// around it. cmd/ci is a thin wrapper over this. Named Main rather than Run
|
||||
// because a Run in this package is a CI run — the domain object — and the
|
||||
// binary's body is not one.
|
||||
func Main() {
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
logger.Error("config", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: cfg.listen,
|
||||
Handler: routes(cfg, cache, board),
|
||||
Handler: Serve(ctx, logger, cfg),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
go func() {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package ci
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package ci
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
Reference in New Issue
Block a user