Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
single-tenant build is unchanged by default.
- **Helm** — `agent.tools.findRunbook.*` values + configmap wiring.

### Changed

- **`services.CreateIncident` now returns `(incidentID string, err error)`**
(`pkg/services/incident.go`) — the created incident's ID was discarded;
it is now returned so callers can act on the new incident (e.g. a future
auto-analysis step). The ID is returned even on a downstream send/on-call
error, because the incident is persisted before fan-out. All call sites
updated (`cmd/main.go`, `pkg/controllers/incident.go`,
`pkg/controllers/sns.go`, `pkg/services/agent.go`); behavior is otherwise
unchanged.

---

## [1.4.3] — 2026-05
Expand Down
3 changes: 2 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,8 @@ func handleQueueMessage(content *map[string]interface{}) error {
// instead of the default "http". Agent-originated incidents carry
// their own Source and ignore this hint.
overwrite := map[string]string{"incident_source": "sqs"}
return services.CreateIncident("", content, &overwrite) // teamID as empty string
_, err := services.CreateIncident("", content, &overwrite) // teamID as empty string
return err
}

func handlerRedisOptions(rc c.RedisConfig) *redis.Options {
Expand Down
8 changes: 4 additions & 4 deletions pkg/controllers/incident.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ func CreateIncident(c *fiber.Ctx) error {
for _, record := range records {
// capture pointer per iteration
rec := record
if err = services.CreateIncident("", &rec, &overwriteVaule); err != nil {
if _, err = services.CreateIncident("", &rec, &overwriteVaule); err != nil {
break
}
}
} else {
for _, record := range records {
rec := record
if err = services.CreateIncident("", &rec); err != nil {
if _, err = services.CreateIncident("", &rec); err != nil {
break
}
}
Expand All @@ -75,9 +75,9 @@ func CreateIncident(c *fiber.Ctx) error {
if len(c.Queries()) > 0 {
overwriteVaule := c.Queries()
delete(overwriteVaule, "incident_source") // reserved: ingress-only, not client-settable
err = services.CreateIncident("", body, &overwriteVaule)
_, err = services.CreateIncident("", body, &overwriteVaule)
} else {
err = services.CreateIncident("", body)
_, err = services.CreateIncident("", body)
}

if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/controllers/sns.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ func SNS(c *fiber.Ctx) error {
if len(c.Queries()) > 0 {
overwriteVaule := c.Queries()
overwriteVaule["incident_source"] = "sns"
err = services.CreateIncident("", content, &overwriteVaule)
_, err = services.CreateIncident("", content, &overwriteVaule)
} else {
overwriteVaule := map[string]string{"incident_source": "sns"}
err = services.CreateIncident("", content, &overwriteVaule)
_, err = services.CreateIncident("", content, &overwriteVaule)
}

if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion pkg/services/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ func CreateIncidentFromFinding(f *core.AIFinding, r core.AgentResult, source, se
params["oncall_enable"] = "false"
}

return CreateIncident("", &content, &params)
_, err := CreateIncident("", &content, &params)
return err
}
19 changes: 13 additions & 6 deletions pkg/services/incident.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ import (
m "github.com/VersusControl/versus-incident/pkg/models"
)

func CreateIncident(teamID string, content *map[string]interface{}, params ...*map[string]string) error {
// CreateIncident persists and fans out an incident, returning the created
// incident's ID alongside any send/on-call error. The ID is returned even
// when a downstream channel or on-call step fails, because the incident is
// persisted first (line below) and therefore exists; callers that need to
// act on the new incident (e.g. auto-analysis) can rely on the ID whenever
// it is non-empty. An empty ID is returned only when no incident was
// created (provider construction failed before persistence).
func CreateIncident(teamID string, content *map[string]interface{}, params ...*map[string]string) (string, error) {
var cfg *config.Config

if len(params) > 0 {
Expand All @@ -28,7 +35,7 @@ func CreateIncident(teamID string, content *map[string]interface{}, params ...*m
factory := common.NewAlertProviderFactory(cfg)
providers, err := factory.CreateProviders()
if err != nil {
return fmt.Errorf("failed to create providers: %v", err)
return "", fmt.Errorf("failed to create providers: %v", err)
}

alert := core.NewAlert(providers...)
Expand Down Expand Up @@ -116,13 +123,13 @@ func CreateIncident(teamID string, content *map[string]interface{}, params ...*m

switch {
case sendErr != nil && oncallErr != nil:
return fmt.Errorf("send: %w; oncall: %v", sendErr, oncallErr)
return incident.ID, fmt.Errorf("send: %w; oncall: %v", sendErr, oncallErr)
case sendErr != nil:
return sendErr
return incident.ID, sendErr
case oncallErr != nil:
return oncallErr
return incident.ID, oncallErr
}
return nil
return incident.ID, nil
}

// buildIncidentRecord copies the alert into a durable IncidentRecord.
Expand Down
64 changes: 64 additions & 0 deletions pkg/services/incident_returnid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package services

import (
"os"
"path/filepath"
"testing"

"github.com/VersusControl/versus-incident/pkg/config"
"github.com/VersusControl/versus-incident/pkg/storage"
)

// TestCreateIncident_ReturnsPersistedID asserts CreateIncident returns the
// ID of the incident it persisted — non-empty and resolvable in the store.
// That ID is the seam an auto-analysis step needs to act on a freshly
// created incident; before this change the function returned only an error.
func TestCreateIncident_ReturnsPersistedID(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(path, []byte(minimalReturnIDConfig), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
if err := config.LoadConfig(path); err != nil {
t.Fatalf("LoadConfig: %v", err)
}

mem := storage.NewMemory()
SetStorage(mem)
t.Cleanup(func() { SetStorage(nil) })

content := map[string]interface{}{"title": "disk space low on worker-04"}
id, err := CreateIncident("", &content)
if err != nil {
t.Fatalf("CreateIncident: %v", err)
}
if id == "" {
t.Fatal("expected a non-empty incident ID")
}

got, err := mem.GetIncident(id)
if err != nil {
t.Fatalf("stored incident not found by returned ID %q: %v", id, err)
}
if got.ID != id {
t.Errorf("stored ID = %q, returned ID = %q", got.ID, id)
}
}

// Minimal config: every channel disabled so the fan-out has zero providers
// (succeeds trivially) and on-call is off — isolating the return-value
// behavior from any network path.
const minimalReturnIDConfig = `
name: test
host: 127.0.0.1
port: 3000
public_host: ''
alert:
debug_body: false
queue:
enable: false
oncall:
enable: false
storage:
type: file
`