ndomo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bun-version +1 -0
- package/.dockerignore +79 -0
- package/.editorconfig +18 -0
- package/.env.example +19 -0
- package/.github/CODEOWNERS +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- package/.github/ISSUE_TEMPLATE/config.yml +2 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +34 -0
- package/.github/dependabot.yml +36 -0
- package/.github/pull_request_template.md +24 -0
- package/.github/release.yml +30 -0
- package/.github/workflows/gitleaks.yml +28 -0
- package/.github/workflows/release-please.yml +27 -0
- package/.github/workflows/smoke.yml +29 -0
- package/.husky/commit-msg +1 -0
- package/CHANGELOG.md +114 -0
- package/Dockerfile +32 -0
- package/README.es.md +174 -0
- package/README.md +187 -0
- package/agents/chronicler.md +98 -0
- package/agents/ci-smith.md +136 -0
- package/agents/craftsman.md +341 -0
- package/agents/deploy-smith.md +138 -0
- package/agents/foreman.md +377 -0
- package/agents/go-smith.md +164 -0
- package/agents/guild.md +188 -0
- package/agents/inspector.md +83 -0
- package/agents/js-smith.md +127 -0
- package/agents/ops-scout.md +173 -0
- package/agents/painter.md +200 -0
- package/agents/python-smith.md +120 -0
- package/agents/ranger.md +307 -0
- package/agents/release-smith.md +165 -0
- package/agents/rust-smith.md +159 -0
- package/agents/sage.md +178 -0
- package/agents/scout.md +144 -0
- package/agents/scribe.md +156 -0
- package/agents/smith.md +201 -0
- package/agents/vue-smith.md +155 -0
- package/agents/warden.md +216 -0
- package/agents/zig-smith.md +156 -0
- package/bin/ndomo-analyses.ts +4 -0
- package/bin/ndomo-status.ts +4 -0
- package/biome.json +57 -0
- package/bun.lock +514 -0
- package/commitlint.config.js +3 -0
- package/config/ndomo.config.json +258 -0
- package/config/ndomo.schema.json +166 -0
- package/docs/agents.md +375 -0
- package/docs/bugs/plan-create-orphan-fk.md +131 -0
- package/docs/bugs/task_create_batch-order-index-collision.md +158 -0
- package/docs/configuration.md +276 -0
- package/docs/database.md +364 -0
- package/docs/features/feature-flexible-builder-v1.md +724 -0
- package/docs/features/feature-flexible-builder-v2.md +882 -0
- package/docs/features/feature-flexible-builder.md +974 -0
- package/docs/http-server.md +244 -0
- package/docs/installation.md +259 -0
- package/docs/integrations.md +129 -0
- package/docs/operations/anti-pattern-sub-agent-verify-2026-06-21.md +32 -0
- package/docs/operations/audit-v1.md +417 -0
- package/docs/operations/audit-v2.md +197 -0
- package/docs/operations/audit-v3.md +306 -0
- package/docs/operations/db-optimize-foundations.md +123 -0
- package/docs/operations/verify-gate-architecture.md +82 -0
- package/docs/workflows.md +448 -0
- package/opencode.json +5 -0
- package/package.json +65 -0
- package/release-please-config.json +11 -0
- package/scripts/dev-bust-cache.sh +164 -0
- package/scripts/install.sh +688 -0
- package/scripts/smoke-e2e.ts +704 -0
- package/scripts/smoke-hot.ts +417 -0
- package/scripts/smoke-http.sh +228 -0
- package/scripts/smoke-v4.ts +256 -0
- package/scripts/smoke-v5.ts +397 -0
- package/scripts/smoke.sh +9 -0
- package/scripts/uninstall.sh +224 -0
- package/skills/api-security-best-practices/SKILL.md +915 -0
- package/skills/bash-scripting/SKILL.md +201 -0
- package/skills/bun/SKILL.md +313 -0
- package/skills/cavecrew/SKILL.md +82 -0
- package/skills/caveman/SKILL.md +74 -0
- package/skills/caveman-review/README.md +33 -0
- package/skills/caveman-review/SKILL.md +55 -0
- package/skills/find-skills/SKILL.md +142 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +55 -0
- package/skills/golang-patterns/SKILL.md +674 -0
- package/skills/golang-security/SKILL.md +185 -0
- package/skills/golang-security/evals/evals.json +595 -0
- package/skills/golang-security/references/architecture.md +268 -0
- package/skills/golang-security/references/checklist.md +80 -0
- package/skills/golang-security/references/cookies.md +200 -0
- package/skills/golang-security/references/cryptography.md +424 -0
- package/skills/golang-security/references/filesystem.md +285 -0
- package/skills/golang-security/references/injection.md +315 -0
- package/skills/golang-security/references/logging.md +163 -0
- package/skills/golang-security/references/memory-safety.md +241 -0
- package/skills/golang-security/references/network.md +253 -0
- package/skills/golang-security/references/secrets.md +189 -0
- package/skills/golang-security/references/third-party.md +159 -0
- package/skills/golang-security/references/threat-modeling.md +189 -0
- package/skills/golang-testing/SKILL.md +720 -0
- package/skills/grill-me/SKILL.md +7 -0
- package/skills/javascript-testing-patterns/SKILL.md +537 -0
- package/skills/javascript-testing-patterns/references/advanced-testing-patterns.md +513 -0
- package/skills/modern-javascript-patterns/SKILL.md +43 -0
- package/skills/modern-javascript-patterns/references/advanced-patterns.md +487 -0
- package/skills/modern-javascript-patterns/references/details.md +457 -0
- package/skills/python-anti-patterns/SKILL.md +349 -0
- package/skills/python-design-patterns/SKILL.md +85 -0
- package/skills/python-design-patterns/references/details.md +353 -0
- package/skills/python-error-handling/SKILL.md +193 -0
- package/skills/python-error-handling/references/details.md +171 -0
- package/skills/python-testing-patterns/SKILL.md +278 -0
- package/skills/python-testing-patterns/references/advanced-patterns.md +411 -0
- package/skills/python-testing-patterns/references/details.md +349 -0
- package/skills/rust-patterns/SKILL.md +500 -0
- package/skills/rust-testing/SKILL.md +501 -0
- package/skills/security-review/SKILL.md +504 -0
- package/skills/security-review/cloud-infrastructure-security.md +361 -0
- package/skills/vue-best-practices/SKILL.md +154 -0
- package/skills/vue-best-practices/references/animation-class-based-technique.md +254 -0
- package/skills/vue-best-practices/references/animation-state-driven-technique.md +291 -0
- package/skills/vue-best-practices/references/component-async.md +97 -0
- package/skills/vue-best-practices/references/component-data-flow.md +307 -0
- package/skills/vue-best-practices/references/component-fallthrough-attrs.md +174 -0
- package/skills/vue-best-practices/references/component-keep-alive.md +137 -0
- package/skills/vue-best-practices/references/component-slots.md +216 -0
- package/skills/vue-best-practices/references/component-suspense.md +228 -0
- package/skills/vue-best-practices/references/component-teleport.md +108 -0
- package/skills/vue-best-practices/references/component-transition-group.md +128 -0
- package/skills/vue-best-practices/references/component-transition.md +125 -0
- package/skills/vue-best-practices/references/composables.md +290 -0
- package/skills/vue-best-practices/references/directives.md +162 -0
- package/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -0
- package/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -0
- package/skills/vue-best-practices/references/perf-virtualize-large-lists.md +187 -0
- package/skills/vue-best-practices/references/plugins.md +166 -0
- package/skills/vue-best-practices/references/reactivity.md +344 -0
- package/skills/vue-best-practices/references/render-functions.md +201 -0
- package/skills/vue-best-practices/references/sfc.md +310 -0
- package/skills/vue-best-practices/references/state-management.md +135 -0
- package/skills/vue-best-practices/references/updated-hook-performance.md +187 -0
- package/skills/vue-pinia-best-practices/SKILL.md +21 -0
- package/skills/vue-pinia-best-practices/reference/pinia-no-active-pinia-error.md +248 -0
- package/skills/vue-pinia-best-practices/reference/pinia-setup-store-return-all-state.md +227 -0
- package/skills/vue-pinia-best-practices/reference/pinia-store-destructuring-breaks-reactivity.md +193 -0
- package/skills/vue-pinia-best-practices/reference/state-url-for-ephemeral-filters.md +238 -0
- package/skills/vue-pinia-best-practices/reference/state-use-pinia-for-large-apps.md +262 -0
- package/skills/vue-pinia-best-practices/reference/store-method-binding-parentheses.md +191 -0
- package/skills/zig-0.16/SKILL.md +840 -0
- package/skills/zig-0.16/scripts/check-zig-version.sh +21 -0
- package/src/cli/analyses.ts +280 -0
- package/src/cli/index.ts +108 -0
- package/src/cli/serve.ts +192 -0
- package/src/cli/smoke.ts +131 -0
- package/src/cli/status.test.ts +204 -0
- package/src/cli/status.ts +263 -0
- package/src/cli/vacuum.test.ts +82 -0
- package/src/cli/vacuum.ts +96 -0
- package/src/config/schema.test.ts +88 -0
- package/src/config/schema.ts +64 -0
- package/src/db/analyses-migration.test.ts +210 -0
- package/src/db/analyses.test.ts +466 -0
- package/src/db/analyses.ts +375 -0
- package/src/db/auto-checkpoint.ts +131 -0
- package/src/db/client.test.ts +129 -0
- package/src/db/client.ts +55 -0
- package/src/db/fts-escape.ts +20 -0
- package/src/db/incidents.test.ts +201 -0
- package/src/db/incidents.ts +93 -0
- package/src/db/index.ts +86 -0
- package/src/db/migrations-v13.test.ts +141 -0
- package/src/db/migrations-v8.test.ts +301 -0
- package/src/db/migrations.ts +147 -0
- package/src/db/plan-archive.test.ts +180 -0
- package/src/db/plan-archive.ts +274 -0
- package/src/db/plan-create.test.ts +276 -0
- package/src/db/plan-create.ts +78 -0
- package/src/db/plan-files.test.ts +289 -0
- package/src/db/plan-update-status.ts +287 -0
- package/src/db/plans.test.ts +490 -0
- package/src/db/plans.ts +534 -0
- package/src/db/resolve-project-dir.test.ts +143 -0
- package/src/db/resolve-project-dir.ts +75 -0
- package/src/db/rollbacks.test.ts +150 -0
- package/src/db/rollbacks.ts +67 -0
- package/src/db/schema.ts +907 -0
- package/src/db/sessions.test.ts +80 -0
- package/src/db/sessions.ts +135 -0
- package/src/db/shutdown.test.ts +147 -0
- package/src/db/shutdown.ts +45 -0
- package/src/db/tasks.test.ts +921 -0
- package/src/db/tasks.ts +747 -0
- package/src/db/types.ts +619 -0
- package/src/http/__tests__/auth.test.ts +196 -0
- package/src/http/__tests__/routes.test.ts +465 -0
- package/src/http/__tests__/sse.test.ts +317 -0
- package/src/http/auth.ts +72 -0
- package/src/http/middleware/cors.ts +53 -0
- package/src/http/middleware/security-headers.ts +21 -0
- package/src/http/routes/events.ts +112 -0
- package/src/http/routes/health.ts +51 -0
- package/src/http/routes/plans.ts +66 -0
- package/src/http/routes/sessions.ts +50 -0
- package/src/http/routes/tasks.ts +60 -0
- package/src/http/server.ts +95 -0
- package/src/http/sse.ts +116 -0
- package/src/index.ts +37 -0
- package/src/lib.ts +65 -0
- package/src/mem/scoped.ts +65 -0
- package/src/orchestrator/background.test.ts +268 -0
- package/src/orchestrator/background.ts +293 -0
- package/src/orchestrator/memory-hook.ts +182 -0
- package/src/orchestrator/reconciler.ts +123 -0
- package/src/orchestrator/scheduler.test.ts +300 -0
- package/src/orchestrator/scheduler.ts +243 -0
- package/src/plugin.test.ts +2574 -0
- package/src/plugin.ts +1690 -0
- package/src/sdk/client.ts +66 -0
- package/src/worktrees/manager.ts +236 -0
- package/src/worktrees/state.ts +87 -0
- package/tests/integration/ranger-flow.test.ts +257 -0
- package/tools/analysis_archive.ts +28 -0
- package/tools/analysis_create.ts +55 -0
- package/tools/analysis_get.ts +33 -0
- package/tools/analysis_link_plan.ts +44 -0
- package/tools/analysis_list.ts +48 -0
- package/tools/analysis_search.ts +36 -0
- package/tools/analysis_update.ts +44 -0
- package/tools/plan_approve.ts +31 -0
- package/tools/plan_create.ts +58 -0
- package/tools/plan_get.ts +40 -0
- package/tools/plan_list.ts +37 -0
- package/tools/plan_search.ts +34 -0
- package/tools/plan_update_status.ts +71 -0
- package/tools/session_checkpoint.ts +31 -0
- package/tools/session_end.ts +26 -0
- package/tools/session_start.ts +43 -0
- package/tools/task_create_batch.ts +70 -0
- package/tools/task_list.ts +35 -0
- package/tools/task_next_for_agent.ts +30 -0
- package/tools/task_search.ts +34 -0
- package/tools/task_update_status.ts +37 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the project directory for ndomo DB operations.
|
|
3
|
+
*
|
|
4
|
+
* Chains `worktree → directory → process.cwd()`, picking the first VALID
|
|
5
|
+
* path (absolute, non-root, non-empty). When the opencode SDK passes an
|
|
6
|
+
* invalid path (e.g. `ctx.directory="/"` for an empty project without
|
|
7
|
+
* `.opencode/`, as observed with pgadmin), falls back to `process.cwd()`
|
|
8
|
+
* so `.ndomo/` gets created in the user's actual CWD rather than at the
|
|
9
|
+
* filesystem root (which would EACCES on `/.ndomo`).
|
|
10
|
+
*
|
|
11
|
+
* Validation in `openDb()` remains as a final defense-in-depth guard —
|
|
12
|
+
* this helper is the "smart" layer, `openDb` is the "strict" layer.
|
|
13
|
+
*
|
|
14
|
+
* See plan bb805ff9 (auto-bootstrap-ndomo-dir-with-cwd-fallback).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { isAbsolute } from "node:path";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Minimal slice of an opencode tool/plugin context that carries project
|
|
21
|
+
* path hints. Both `worktree` and `directory` are optional in the SDK
|
|
22
|
+
* (an empty project may yield `directory="/"`).
|
|
23
|
+
*/
|
|
24
|
+
export interface ProjectDirContext {
|
|
25
|
+
worktree?: string | undefined;
|
|
26
|
+
directory?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A path is "valid" for `.ndomo/` placement when it is:
|
|
31
|
+
* - a string (not undefined/null)
|
|
32
|
+
* - non-empty
|
|
33
|
+
* - not the filesystem root "/"
|
|
34
|
+
* - absolute (so `.ndomo` lands at a deterministic location)
|
|
35
|
+
*/
|
|
36
|
+
function isValidPath(p: string | undefined | null): p is string {
|
|
37
|
+
return typeof p === "string" && p !== "" && p !== "/" && isAbsolute(p);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the project directory to use for `.ndomo/state.db`.
|
|
42
|
+
*
|
|
43
|
+
* Resolution order:
|
|
44
|
+
* 1. `ctx.worktree` (git worktree root, when set)
|
|
45
|
+
* 2. `ctx.directory` (opencode project directory)
|
|
46
|
+
* 3. `process.cwd()` (default flow for projects without `.opencode/`,
|
|
47
|
+
* e.g. pgadmin — the SDK passes `directory="/"` and CWD is the
|
|
48
|
+
* expected, normal resolution, NOT an exception)
|
|
49
|
+
* 4. throw — only if even `process.cwd()` is invalid (extremely unlikely;
|
|
50
|
+
* would indicate a misconfigured shell/env).
|
|
51
|
+
*
|
|
52
|
+
* Emits a `console.warn` only when `ctx.worktree` is present but invalid
|
|
53
|
+
* (anomalous SDK config). The CWD fallback is silent because it is the
|
|
54
|
+
* expected default for marker-only / no-`.opencode/` projects.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveProjectDir(ctx: ProjectDirContext): string {
|
|
57
|
+
if (isValidPath(ctx.worktree)) {
|
|
58
|
+
return ctx.worktree;
|
|
59
|
+
}
|
|
60
|
+
if (isValidPath(ctx.directory)) {
|
|
61
|
+
if (ctx.worktree) {
|
|
62
|
+
console.warn(
|
|
63
|
+
`[ndomo] ctx.worktree=${JSON.stringify(ctx.worktree)} invalid, using ctx.directory=${JSON.stringify(ctx.directory)}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return ctx.directory;
|
|
67
|
+
}
|
|
68
|
+
const cwd = process.cwd();
|
|
69
|
+
if (isValidPath(cwd)) {
|
|
70
|
+
return cwd;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(
|
|
73
|
+
`resolveProjectDir: no valid project directory — ctx.worktree=${JSON.stringify(ctx.worktree)}, ctx.directory=${JSON.stringify(ctx.directory)}, process.cwd()=${JSON.stringify(cwd)}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
import { beforeEach, describe, expect, test } from "bun:test"
|
|
3
|
+
import { createIncident } from "./incidents.ts"
|
|
4
|
+
import { runMigrations } from "./migrations.ts"
|
|
5
|
+
import { getRollback, listRollbacksForDeployment, listRollbacksForIncident, recordRollback } from "./rollbacks.ts"
|
|
6
|
+
|
|
7
|
+
function createTestDeployment(db: Database, id = "d1"): string {
|
|
8
|
+
const now = Date.now()
|
|
9
|
+
db.query("INSERT OR IGNORE INTO environments (id, name, slug, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run("e1", "prod", "prod", now, now)
|
|
10
|
+
db.query("INSERT OR IGNORE INTO releases (id, version, title, created_at) VALUES (?, ?, ?, ?)").run("r1", "1.0.0", "rel", now)
|
|
11
|
+
db.query("INSERT INTO deployments (id, release_id, environment_id, status, created_at) VALUES (?, ?, ?, ?, ?)").run(id, "r1", "e1", "planned", now)
|
|
12
|
+
return id
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("rollbacks.ts", () => {
|
|
16
|
+
let db: Database
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
db = new Database(":memory:")
|
|
19
|
+
db.exec("PRAGMA foreign_keys = ON")
|
|
20
|
+
runMigrations(db)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe("recordRollback", () => {
|
|
24
|
+
test("happy path — defaults to status=planned", () => {
|
|
25
|
+
createTestDeployment(db)
|
|
26
|
+
const rb = recordRollback(db, { deploymentId: "d1", plan: "Revert to previous version" })
|
|
27
|
+
expect(rb.id).toBeTruthy()
|
|
28
|
+
expect(rb.deploymentId).toBe("d1")
|
|
29
|
+
expect(rb.status).toBe("planned")
|
|
30
|
+
expect(rb.plan).toBe("Revert to previous version")
|
|
31
|
+
expect(rb.incidentId).toBeNull()
|
|
32
|
+
expect(rb.newDeploymentId).toBeNull()
|
|
33
|
+
expect(rb.executedAt).toBeNull()
|
|
34
|
+
expect(rb.createdAt).toBeGreaterThan(0)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test("with explicit status", () => {
|
|
38
|
+
createTestDeployment(db)
|
|
39
|
+
const rb = recordRollback(db, { deploymentId: "d1", plan: "Approved rollback", status: "approved" })
|
|
40
|
+
expect(rb.status).toBe("approved")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test("with metadata", () => {
|
|
44
|
+
createTestDeployment(db)
|
|
45
|
+
const rb = recordRollback(db, { deploymentId: "d1", plan: "Meta test", metadata: { reason: "regression" } })
|
|
46
|
+
expect(rb.metadata).toEqual({ reason: "regression" })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("throws on invalid status", () => {
|
|
50
|
+
createTestDeployment(db)
|
|
51
|
+
expect(() => recordRollback(db, { deploymentId: "d1", plan: "Bad", status: "invalid" as any })).toThrow("invalid rollback status")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("throws on empty plan (whitespace only)", () => {
|
|
55
|
+
createTestDeployment(db)
|
|
56
|
+
expect(() => recordRollback(db, { deploymentId: "d1", plan: " " })).toThrow("rollback plan cannot be empty")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("trims plan", () => {
|
|
60
|
+
createTestDeployment(db)
|
|
61
|
+
const rb = recordRollback(db, { deploymentId: "d1", plan: " trimmed " })
|
|
62
|
+
expect(rb.plan).toBe("trimmed")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test("truncates plan >16KB", () => {
|
|
66
|
+
createTestDeployment(db)
|
|
67
|
+
const bigPlan = "x".repeat(20_000)
|
|
68
|
+
const rb = recordRollback(db, { deploymentId: "d1", plan: bigPlan })
|
|
69
|
+
expect(rb.plan.length).toBeLessThan(20_000)
|
|
70
|
+
expect(rb.plan).toContain("…[truncated]")
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test("FK validation — non-existent deployment_id throws", () => {
|
|
74
|
+
expect(() => recordRollback(db, { deploymentId: "nonexistent", plan: "test" })).toThrow("deployment 'nonexistent' not found")
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("FK validation — non-existent incident_id throws", () => {
|
|
78
|
+
createTestDeployment(db)
|
|
79
|
+
expect(() => recordRollback(db, { deploymentId: "d1", plan: "test", incidentId: "nonexistent" })).toThrow("incident 'nonexistent' not found")
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("FK validation — non-existent new_deployment_id throws", () => {
|
|
83
|
+
createTestDeployment(db)
|
|
84
|
+
expect(() => recordRollback(db, { deploymentId: "d1", plan: "test", newDeploymentId: "nonexistent" })).toThrow("new_deployment 'nonexistent' not found")
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("with valid incident_id", () => {
|
|
88
|
+
createTestDeployment(db)
|
|
89
|
+
const inc = createIncident(db, { title: "Test incident", severity: "sev1" })
|
|
90
|
+
const rb = recordRollback(db, { deploymentId: "d1", plan: "Rollback for incident", incidentId: inc.id })
|
|
91
|
+
expect(rb.incidentId).toBe(inc.id)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("with valid new_deployment_id", () => {
|
|
95
|
+
createTestDeployment(db, "d1")
|
|
96
|
+
createTestDeployment(db, "d2")
|
|
97
|
+
const rb = recordRollback(db, { deploymentId: "d1", plan: "Roll forward", newDeploymentId: "d2" })
|
|
98
|
+
expect(rb.newDeploymentId).toBe("d2")
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe("getRollback", () => {
|
|
103
|
+
test("returns existing rollback", () => {
|
|
104
|
+
createTestDeployment(db)
|
|
105
|
+
const created = recordRollback(db, { deploymentId: "d1", plan: "Find me" })
|
|
106
|
+
const found = getRollback(db, created.id)
|
|
107
|
+
expect(found).not.toBeNull()
|
|
108
|
+
expect(found!.plan).toBe("Find me")
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("returns null for non-existent id", () => {
|
|
112
|
+
expect(getRollback(db, "nonexistent")).toBeNull()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe("listRollbacksForIncident", () => {
|
|
117
|
+
test("returns rollbacks linked to incident", () => {
|
|
118
|
+
createTestDeployment(db)
|
|
119
|
+
const inc = createIncident(db, { title: "Inc", severity: "sev1" })
|
|
120
|
+
recordRollback(db, { deploymentId: "d1", plan: "RB1", incidentId: inc.id })
|
|
121
|
+
recordRollback(db, { deploymentId: "d1", plan: "RB2", incidentId: inc.id })
|
|
122
|
+
recordRollback(db, { deploymentId: "d1", plan: "RB3", incidentId: inc.id })
|
|
123
|
+
const rbs = listRollbacksForIncident(db, inc.id)
|
|
124
|
+
expect(rbs.length).toBe(3)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test("returns empty for incident with no rollbacks", () => {
|
|
128
|
+
createTestDeployment(db)
|
|
129
|
+
const inc = createIncident(db, { title: "Empty", severity: "sev1" })
|
|
130
|
+
const rbs = listRollbacksForIncident(db, inc.id)
|
|
131
|
+
expect(rbs.length).toBe(0)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe("listRollbacksForDeployment", () => {
|
|
136
|
+
test("returns rollbacks for deployment", () => {
|
|
137
|
+
createTestDeployment(db)
|
|
138
|
+
recordRollback(db, { deploymentId: "d1", plan: "RB1" })
|
|
139
|
+
recordRollback(db, { deploymentId: "d1", plan: "RB2" })
|
|
140
|
+
const rbs = listRollbacksForDeployment(db, "d1")
|
|
141
|
+
expect(rbs.length).toBe(2)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test("returns empty for deployment with no rollbacks", () => {
|
|
145
|
+
createTestDeployment(db)
|
|
146
|
+
const rbs = listRollbacksForDeployment(db, "d1")
|
|
147
|
+
expect(rbs.length).toBe(0)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ndomo DB — Rollback execution CRUD.
|
|
3
|
+
*
|
|
4
|
+
* All functions take a Database instance and return camelCase TS types.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Database } from "bun:sqlite"
|
|
8
|
+
import { rollbackFromRow, type InsertRollback, type RollbackExecution, type RollbackStatus } from "./types.ts"
|
|
9
|
+
|
|
10
|
+
const MAX_TEXT_BYTES = 16 * 1024
|
|
11
|
+
|
|
12
|
+
function truncateText(s: string): string {
|
|
13
|
+
if (s.length <= MAX_TEXT_BYTES) return s
|
|
14
|
+
return `${s.slice(0, MAX_TEXT_BYTES)}…[truncated]`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const VALID_STATUSES: RollbackStatus[] = ["planned", "approved", "dry_run", "executing", "success", "failed", "cancelled"]
|
|
18
|
+
|
|
19
|
+
export function recordRollback(
|
|
20
|
+
db: Database,
|
|
21
|
+
input: InsertRollback,
|
|
22
|
+
): RollbackExecution {
|
|
23
|
+
// FK: deployment_id required
|
|
24
|
+
const dep = db.query("SELECT id FROM deployments WHERE id = ?").get(input.deploymentId)
|
|
25
|
+
if (!dep) throw new Error(`ndomo: deployment '${input.deploymentId}' not found (FK violation)`)
|
|
26
|
+
// FK: incident_id optional
|
|
27
|
+
if (input.incidentId) {
|
|
28
|
+
const inc = db.query("SELECT id FROM incidents WHERE id = ?").get(input.incidentId)
|
|
29
|
+
if (!inc) throw new Error(`ndomo: incident '${input.incidentId}' not found (FK violation)`)
|
|
30
|
+
}
|
|
31
|
+
// FK: new_deployment_id optional
|
|
32
|
+
if (input.newDeploymentId) {
|
|
33
|
+
const newDep = db.query("SELECT id FROM deployments WHERE id = ?").get(input.newDeploymentId)
|
|
34
|
+
if (!newDep) throw new Error(`ndomo: new_deployment '${input.newDeploymentId}' not found (FK violation)`)
|
|
35
|
+
}
|
|
36
|
+
// Status (default planned)
|
|
37
|
+
const status: RollbackStatus = input.status ?? "planned"
|
|
38
|
+
if (!VALID_STATUSES.includes(status)) {
|
|
39
|
+
throw new Error(`ndomo: invalid rollback status '${status}' (must be one of: ${VALID_STATUSES.join(", ")})`)
|
|
40
|
+
}
|
|
41
|
+
// Plan: trim + truncate + validate non-empty
|
|
42
|
+
const plan = truncateText(input.plan.trim())
|
|
43
|
+
if (plan.length === 0) throw new Error("ndomo: rollback plan cannot be empty")
|
|
44
|
+
const id = crypto.randomUUID()
|
|
45
|
+
const now = Date.now()
|
|
46
|
+
const metadata = input.metadata ? JSON.stringify(input.metadata) : null
|
|
47
|
+
db.query(
|
|
48
|
+
`INSERT INTO rollback_executions (id, deployment_id, incident_id, new_deployment_id, status, plan, executed_at, created_at, metadata)
|
|
49
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)`,
|
|
50
|
+
).run(id, input.deploymentId, input.incidentId ?? null, input.newDeploymentId ?? null, status, plan, now, metadata)
|
|
51
|
+
return getRollback(db, id)!
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getRollback(db: Database, id: string): RollbackExecution | null {
|
|
55
|
+
const row = db.query("SELECT * FROM rollback_executions WHERE id = ?").get(id)
|
|
56
|
+
return row ? rollbackFromRow(row) : null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function listRollbacksForIncident(db: Database, incidentId: string): RollbackExecution[] {
|
|
60
|
+
const rows = db.query("SELECT * FROM rollback_executions WHERE incident_id = ? ORDER BY created_at DESC").all(incidentId)
|
|
61
|
+
return rows.map(rollbackFromRow)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function listRollbacksForDeployment(db: Database, deploymentId: string): RollbackExecution[] {
|
|
65
|
+
const rows = db.query("SELECT * FROM rollback_executions WHERE deployment_id = ? ORDER BY created_at DESC").all(deploymentId)
|
|
66
|
+
return rows.map(rollbackFromRow)
|
|
67
|
+
}
|