Update 1.5.0
release-tag / release-image (push) Successful in 10m52s

This commit is contained in:
2026-08-27 07:51:39 +02:00
parent 1decb831d6
commit 8c67c7a7fa
58 changed files with 10768 additions and 626 deletions
+4 -1
View File
@@ -3,7 +3,9 @@ WORKDIR /src
COPY go.mod ./
COPY cmd ./cmd
COPY internal ./internal
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/glpi-ai-agent ./cmd/agent
RUN go test ./... && \
go vet ./... && \
CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/glpi-ai-agent ./cmd/agent
# One-shot helper used by docker compose to prepare the persistent volume for
# the distroless non-root runtime user (UID/GID 65532).
@@ -16,5 +18,6 @@ COPY --from=build /out/glpi-ai-agent /app/glpi-ai-agent
COPY knowledge /app/knowledge
VOLUME ["/app/data"]
EXPOSE 8080
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=4 CMD ["/app/glpi-ai-agent", "healthcheck"]
USER 65532:65532
ENTRYPOINT ["/app/glpi-ai-agent"]
+20
View File
@@ -27,6 +27,10 @@ import (
)
func main() {
if len(os.Args) == 2 && os.Args[1] == "healthcheck" {
runHealthcheck()
return
}
cfg, err := config.Load()
if err != nil {
slog.Error("configuration invalid", "error", err)
@@ -204,6 +208,22 @@ func main() {
slog.Info("shutdown complete")
}
func runHealthcheck() {
client := &http.Client{Timeout: 2 * time.Second}
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/healthz", nil)
if err != nil {
os.Exit(1)
}
resp, err := client.Do(req)
if err != nil {
os.Exit(1)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
os.Exit(1)
}
}
func waitForOllamaPool(ctx context.Context, client *ollama.Client, retryInterval time.Duration) error {
if retryInterval < 2*time.Second {
retryInterval = 5 * time.Second
+7
View File
@@ -325,6 +325,7 @@ func Load() (Config, error) {
CommunicationSalutation: env("COMMUNICATION_SALUTATION", "Guten Tag,"),
CommunicationClosing: env("COMMUNICATION_CLOSING", "Mit freundlichen Grüßen"),
CommunicationSignature: env("COMMUNICATION_SIGNATURE", "IT-Service"),
AIContentLabelEnabled: envBool("AI_CONTENT_LABEL_ENABLED", true),
AutoCategory: envBool("AUTO_CATEGORY", true),
AutoReply: envBool("AUTO_REPLY", false),
PriorityEnabled: envBool("PRIORITY_ENABLED", true),
@@ -651,6 +652,12 @@ func (c Config) Validate() error {
return fmt.Errorf("KNOWLEDGE_VECTOR_BACKEND must be one of: local, dual, neuroforge (got %q)", c.KnowledgeVectorBackend)
}
if c.KnowledgeVectorBackend == "dual" || c.KnowledgeVectorBackend == "neuroforge" {
if len(c.NeuroForgeAPIKey) < 24 {
return errors.New("NEUROFORGE_API_KEY must contain at least 24 characters for dual/neuroforge mode")
}
if isPlaceholder(c.NeuroForgeAPIKey) {
return errors.New("NEUROFORGE_API_KEY still contains a CHANGE_ME placeholder")
}
u, err := url.Parse(c.NeuroForgeURL)
if err != nil || u.Scheme == "" || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
return errors.New("NEUROFORGE_URL must be an absolute http(s) URL")
@@ -515,3 +515,40 @@ func TestValidateGLPIKBAutoReplyUncategorizedRequiresArticleAllowlist(t *testing
t.Fatalf("expected uncategorized auto reply with explicit article allowlist to validate: %v", err)
}
}
func TestLoadReadsAIContentLabelEnabled(t *testing.T) {
for key, value := range map[string]string{
"GLPI_URL": "https://glpi.internal.example",
"GLPI_CLIENT_ID": "client-id",
"GLPI_CLIENT_SECRET": "real-secret-value",
"GLPI_USERNAME": "svc-agent",
"GLPI_PASSWORD": "real-password-value",
"WEB_ALLOW_ANONYMOUS": "true",
"AI_CONTENT_LABEL_ENABLED": "false",
} {
t.Setenv(key, value)
}
c, err := Load()
if err != nil {
t.Fatal(err)
}
if c.AIContentLabelEnabled {
t.Fatal("AI_CONTENT_LABEL_ENABLED=false was not loaded")
}
}
func TestValidateNeuroForgeAPIKeyForRemoteVectorBackend(t *testing.T) {
c := validConfig()
c.KnowledgeVectorBackend = "dual"
c.NeuroForgeURL = "http://neuroforge:8090"
c.NeuroForgeSearchK = 128
c.NeuroForgeTimeout = 5 * time.Second
c.NeuroForgeAPIKey = "CHANGE_ME_NEUROFORGE_API_KEY"
if err := c.Validate(); err == nil {
t.Fatal("expected placeholder NEUROFORGE_API_KEY to be rejected")
}
c.NeuroForgeAPIKey = "integration-token-0123456789abcdef"
if err := c.Validate(); err != nil {
t.Fatalf("expected strong NEUROFORGE_API_KEY to validate: %v", err)
}
}