@checkstack/gitops-backend 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/CHANGELOG.md ADDED
@@ -0,0 +1,67 @@
1
+ # @checkstack/gitops-backend
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6c40b5b: feat: add GitOps Entity System foundation — entity envelope schema, Entity Kind Registry extension point, secret field utility, secret resolution engine, provenance tracking, and RPC contract
8
+ - 6c40b5b: Generalized provenance system and GitOps frontend plugin
9
+
10
+ **Breaking**: `EntityKindDefinition.reconcile()` now returns `{ entityId: string }` instead of `void`. Plugins must return the plugin-specific entity ID (e.g., catalog system UUID) so the engine can store it in provenance.
11
+
12
+ - Added `entityId` column to the provenance table (non-nullable)
13
+ - Reconciler engine passes `existingEntityId` to plugins for updates
14
+ - `getProvenance` now supports lookup by `entityId` in addition to `entityName`
15
+ - Added provider CRUD endpoints: `createProvider`, `updateProvider`, `deleteProvider`
16
+ - Created `gitops-frontend` plugin with provider management, secret management, and sync status dashboard
17
+ - Removed `gitops_entity_name` metadata markers from catalog entities
18
+ - Removed `findSystemByGitOpsName`, `deleteSystemByGitOpsName` (and Group equivalents) from EntityService
19
+ - Added provenance-based UI locking in catalog-frontend: edit/delete/drag disabled for GitOps-managed systems and groups
20
+
21
+ - 6c40b5b: ### GitOps Ecosystem: Healthcheck Kind Registration (Phase 5)
22
+
23
+ **gitops-common**: Added required `resolveEntityRef` to `ReconcileContext`, enabling extension reconcilers to resolve cross-kind entity references (e.g., healthcheck refs in System extensions).
24
+
25
+ **gitops-backend**: Updated reconciler to populate `resolveEntityRef` by querying local provenance — no RPC round-trip needed.
26
+
27
+ **healthcheck-backend**: Registered `kind: Healthcheck` and `System → healthchecks` extension with the EntityKindRegistry:
28
+
29
+ - Validates strategy configs against registered strategy schemas at reconcile time
30
+ - Validates collector configs against registered collector schemas at reconcile time
31
+ - Manages system ↔ healthcheck associations with automatic stale removal
32
+
33
+ **healthcheck-frontend**: Added GitOps provenance locking to the HealthCheck IDE editor — GitOps-managed health checks show a lock banner and disable editing.
34
+
35
+ **catalog-backend**: Updated test fixtures for new required `resolveEntityRef` context field.
36
+
37
+ - 6c40b5b: Add GitOps discovery and sync engine (Phase 2)
38
+
39
+ - YAML document parser with multi-document support and SHA-256 content hashing for diff detection
40
+ - GitHub scraper: org/user repo enumeration, single-repo mode, default branch resolution, recursive Git Trees API, minimatch path filtering, Link header pagination
41
+ - GitLab scraper: group project enumeration (including subgroups), single-project mode, recursive tree walking, minimatch filtering, x-next-page pagination
42
+ - Configurable `baseUrl` per provider for GitHub Enterprise and self-managed GitLab instances
43
+ - Reconciliation orchestrator: scrape → parse → validate → resolve secrets → reconcile (base + extensions) → provenance tracking → orphan detection
44
+ - Sync worker: recurring queue jobs per provider, one-off manual trigger via triggerSync RPC
45
+ - Per-entity error isolation ensures individual failures don't halt the sync
46
+
47
+ - 6c40b5b: Add Kind Registry browser and developer documentation
48
+
49
+ - Added `gitopsAccess.kinds.read` access rule for standalone Kind Registry access
50
+ - Added `describeKinds()` method to the internal entity kind registry, serializing Zod schemas to JSON Schema
51
+ - Added `listKinds` RPC endpoint gated by the new access rule
52
+ - Created standalone Kind Registry page with schema visualization, extension listing, and auto-generated YAML examples
53
+ - Added Kind Registry link to the user menu
54
+ - Created developer documentation for entity kind and extension registration in `docs/backend/gitops-entity-kinds.md`
55
+
56
+ ### Patch Changes
57
+
58
+ - 6c40b5b: Register catalog System and Group as GitOps entity kinds
59
+
60
+ - **catalog-backend**: Registers `kind: System` and `kind: Group` with the GitOps Entity Kind Registry. The catalog now supports declarative management via YAML descriptors in Git repositories. Systems and groups are reconciled using the `metadata.gitops_entity_name` marker for cross-sync identity lookup.
61
+ - **gitops-backend**: Wires up the delete reconciler for orphan cleanup — both automatic deletion (via `deletionPolicy: "auto"`) and manual orphan confirmation now invoke the owning plugin's `delete()` handler before removing provenance entries.
62
+
63
+ - Updated dependencies [6c40b5b]
64
+ - Updated dependencies [6c40b5b]
65
+ - Updated dependencies [6c40b5b]
66
+ - Updated dependencies [6c40b5b]
67
+ - @checkstack/gitops-common@0.1.0
@@ -0,0 +1,46 @@
1
+ CREATE TYPE "deletion_policy" AS ENUM('orphan', 'auto');--> statement-breakpoint
2
+ CREATE TYPE "provenance_status" AS ENUM('synced', 'error', 'orphaned');--> statement-breakpoint
3
+ CREATE TYPE "provider_type" AS ENUM('github', 'gitlab');--> statement-breakpoint
4
+ CREATE TABLE "provenance" (
5
+ "id" text PRIMARY KEY NOT NULL,
6
+ "api_version" text NOT NULL,
7
+ "kind" text NOT NULL,
8
+ "entity_name" text NOT NULL,
9
+ "entity_id" text NOT NULL,
10
+ "provider_id" text NOT NULL,
11
+ "repository" text NOT NULL,
12
+ "file_path" text NOT NULL,
13
+ "last_sync_hash" text NOT NULL,
14
+ "status" "provenance_status" DEFAULT 'synced' NOT NULL,
15
+ "error_message" text,
16
+ "last_synced_at" timestamp DEFAULT now() NOT NULL,
17
+ "created_at" timestamp DEFAULT now() NOT NULL
18
+ );
19
+ --> statement-breakpoint
20
+ CREATE TABLE "providers" (
21
+ "id" text PRIMARY KEY NOT NULL,
22
+ "type" "provider_type" NOT NULL,
23
+ "target" text NOT NULL,
24
+ "path_pattern" text NOT NULL,
25
+ "auth_token" text,
26
+ "base_url" text,
27
+ "sync_interval" integer DEFAULT 300 NOT NULL,
28
+ "deletion_policy" "deletion_policy" DEFAULT 'orphan' NOT NULL,
29
+ "last_sync_at" timestamp,
30
+ "last_sync_error" text,
31
+ "created_at" timestamp DEFAULT now() NOT NULL,
32
+ "updated_at" timestamp DEFAULT now() NOT NULL
33
+ );
34
+ --> statement-breakpoint
35
+ CREATE TABLE "secrets" (
36
+ "id" text PRIMARY KEY NOT NULL,
37
+ "name" text NOT NULL,
38
+ "encrypted_value" text NOT NULL,
39
+ "description" text,
40
+ "created_by" text,
41
+ "created_at" timestamp DEFAULT now() NOT NULL,
42
+ "updated_at" timestamp DEFAULT now() NOT NULL,
43
+ CONSTRAINT "secrets_name_unique" UNIQUE("name")
44
+ );
45
+ --> statement-breakpoint
46
+ ALTER TABLE "provenance" ADD CONSTRAINT "provenance_provider_id_providers_id_fk" FOREIGN KEY ("provider_id") REFERENCES "providers"("id") ON DELETE cascade ON UPDATE no action;
@@ -0,0 +1,310 @@
1
+ {
2
+ "id": "01bbd45b-d962-4e98-b25d-53310f487822",
3
+ "prevId": "00000000-0000-0000-0000-000000000000",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.provenance": {
8
+ "name": "provenance",
9
+ "schema": "",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "text",
14
+ "primaryKey": true,
15
+ "notNull": true
16
+ },
17
+ "api_version": {
18
+ "name": "api_version",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true
22
+ },
23
+ "kind": {
24
+ "name": "kind",
25
+ "type": "text",
26
+ "primaryKey": false,
27
+ "notNull": true
28
+ },
29
+ "entity_name": {
30
+ "name": "entity_name",
31
+ "type": "text",
32
+ "primaryKey": false,
33
+ "notNull": true
34
+ },
35
+ "entity_id": {
36
+ "name": "entity_id",
37
+ "type": "text",
38
+ "primaryKey": false,
39
+ "notNull": true
40
+ },
41
+ "provider_id": {
42
+ "name": "provider_id",
43
+ "type": "text",
44
+ "primaryKey": false,
45
+ "notNull": true
46
+ },
47
+ "repository": {
48
+ "name": "repository",
49
+ "type": "text",
50
+ "primaryKey": false,
51
+ "notNull": true
52
+ },
53
+ "file_path": {
54
+ "name": "file_path",
55
+ "type": "text",
56
+ "primaryKey": false,
57
+ "notNull": true
58
+ },
59
+ "last_sync_hash": {
60
+ "name": "last_sync_hash",
61
+ "type": "text",
62
+ "primaryKey": false,
63
+ "notNull": true
64
+ },
65
+ "status": {
66
+ "name": "status",
67
+ "type": "provenance_status",
68
+ "typeSchema": "public",
69
+ "primaryKey": false,
70
+ "notNull": true,
71
+ "default": "'synced'"
72
+ },
73
+ "error_message": {
74
+ "name": "error_message",
75
+ "type": "text",
76
+ "primaryKey": false,
77
+ "notNull": false
78
+ },
79
+ "last_synced_at": {
80
+ "name": "last_synced_at",
81
+ "type": "timestamp",
82
+ "primaryKey": false,
83
+ "notNull": true,
84
+ "default": "now()"
85
+ },
86
+ "created_at": {
87
+ "name": "created_at",
88
+ "type": "timestamp",
89
+ "primaryKey": false,
90
+ "notNull": true,
91
+ "default": "now()"
92
+ }
93
+ },
94
+ "indexes": {},
95
+ "foreignKeys": {
96
+ "provenance_provider_id_providers_id_fk": {
97
+ "name": "provenance_provider_id_providers_id_fk",
98
+ "tableFrom": "provenance",
99
+ "tableTo": "providers",
100
+ "columnsFrom": [
101
+ "provider_id"
102
+ ],
103
+ "columnsTo": [
104
+ "id"
105
+ ],
106
+ "onDelete": "cascade",
107
+ "onUpdate": "no action"
108
+ }
109
+ },
110
+ "compositePrimaryKeys": {},
111
+ "uniqueConstraints": {},
112
+ "policies": {},
113
+ "checkConstraints": {},
114
+ "isRLSEnabled": false
115
+ },
116
+ "public.providers": {
117
+ "name": "providers",
118
+ "schema": "",
119
+ "columns": {
120
+ "id": {
121
+ "name": "id",
122
+ "type": "text",
123
+ "primaryKey": true,
124
+ "notNull": true
125
+ },
126
+ "type": {
127
+ "name": "type",
128
+ "type": "provider_type",
129
+ "typeSchema": "public",
130
+ "primaryKey": false,
131
+ "notNull": true
132
+ },
133
+ "target": {
134
+ "name": "target",
135
+ "type": "text",
136
+ "primaryKey": false,
137
+ "notNull": true
138
+ },
139
+ "path_pattern": {
140
+ "name": "path_pattern",
141
+ "type": "text",
142
+ "primaryKey": false,
143
+ "notNull": true
144
+ },
145
+ "auth_token": {
146
+ "name": "auth_token",
147
+ "type": "text",
148
+ "primaryKey": false,
149
+ "notNull": false
150
+ },
151
+ "base_url": {
152
+ "name": "base_url",
153
+ "type": "text",
154
+ "primaryKey": false,
155
+ "notNull": false
156
+ },
157
+ "sync_interval": {
158
+ "name": "sync_interval",
159
+ "type": "integer",
160
+ "primaryKey": false,
161
+ "notNull": true,
162
+ "default": 300
163
+ },
164
+ "deletion_policy": {
165
+ "name": "deletion_policy",
166
+ "type": "deletion_policy",
167
+ "typeSchema": "public",
168
+ "primaryKey": false,
169
+ "notNull": true,
170
+ "default": "'orphan'"
171
+ },
172
+ "last_sync_at": {
173
+ "name": "last_sync_at",
174
+ "type": "timestamp",
175
+ "primaryKey": false,
176
+ "notNull": false
177
+ },
178
+ "last_sync_error": {
179
+ "name": "last_sync_error",
180
+ "type": "text",
181
+ "primaryKey": false,
182
+ "notNull": false
183
+ },
184
+ "created_at": {
185
+ "name": "created_at",
186
+ "type": "timestamp",
187
+ "primaryKey": false,
188
+ "notNull": true,
189
+ "default": "now()"
190
+ },
191
+ "updated_at": {
192
+ "name": "updated_at",
193
+ "type": "timestamp",
194
+ "primaryKey": false,
195
+ "notNull": true,
196
+ "default": "now()"
197
+ }
198
+ },
199
+ "indexes": {},
200
+ "foreignKeys": {},
201
+ "compositePrimaryKeys": {},
202
+ "uniqueConstraints": {},
203
+ "policies": {},
204
+ "checkConstraints": {},
205
+ "isRLSEnabled": false
206
+ },
207
+ "public.secrets": {
208
+ "name": "secrets",
209
+ "schema": "",
210
+ "columns": {
211
+ "id": {
212
+ "name": "id",
213
+ "type": "text",
214
+ "primaryKey": true,
215
+ "notNull": true
216
+ },
217
+ "name": {
218
+ "name": "name",
219
+ "type": "text",
220
+ "primaryKey": false,
221
+ "notNull": true
222
+ },
223
+ "encrypted_value": {
224
+ "name": "encrypted_value",
225
+ "type": "text",
226
+ "primaryKey": false,
227
+ "notNull": true
228
+ },
229
+ "description": {
230
+ "name": "description",
231
+ "type": "text",
232
+ "primaryKey": false,
233
+ "notNull": false
234
+ },
235
+ "created_by": {
236
+ "name": "created_by",
237
+ "type": "text",
238
+ "primaryKey": false,
239
+ "notNull": false
240
+ },
241
+ "created_at": {
242
+ "name": "created_at",
243
+ "type": "timestamp",
244
+ "primaryKey": false,
245
+ "notNull": true,
246
+ "default": "now()"
247
+ },
248
+ "updated_at": {
249
+ "name": "updated_at",
250
+ "type": "timestamp",
251
+ "primaryKey": false,
252
+ "notNull": true,
253
+ "default": "now()"
254
+ }
255
+ },
256
+ "indexes": {},
257
+ "foreignKeys": {},
258
+ "compositePrimaryKeys": {},
259
+ "uniqueConstraints": {
260
+ "secrets_name_unique": {
261
+ "name": "secrets_name_unique",
262
+ "nullsNotDistinct": false,
263
+ "columns": [
264
+ "name"
265
+ ]
266
+ }
267
+ },
268
+ "policies": {},
269
+ "checkConstraints": {},
270
+ "isRLSEnabled": false
271
+ }
272
+ },
273
+ "enums": {
274
+ "public.deletion_policy": {
275
+ "name": "deletion_policy",
276
+ "schema": "public",
277
+ "values": [
278
+ "orphan",
279
+ "auto"
280
+ ]
281
+ },
282
+ "public.provenance_status": {
283
+ "name": "provenance_status",
284
+ "schema": "public",
285
+ "values": [
286
+ "synced",
287
+ "error",
288
+ "orphaned"
289
+ ]
290
+ },
291
+ "public.provider_type": {
292
+ "name": "provider_type",
293
+ "schema": "public",
294
+ "values": [
295
+ "github",
296
+ "gitlab"
297
+ ]
298
+ }
299
+ },
300
+ "schemas": {},
301
+ "sequences": {},
302
+ "roles": {},
303
+ "policies": {},
304
+ "views": {},
305
+ "_meta": {
306
+ "columns": {},
307
+ "schemas": {},
308
+ "tables": {}
309
+ }
310
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1776797758378,
9
+ "tag": "0000_tense_stryfe",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ schema: "./src/schema.ts",
5
+ out: "./drizzle",
6
+ dialect: "postgresql",
7
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@checkstack/gitops-backend",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "checkstack": {
7
+ "type": "backend"
8
+ },
9
+ "scripts": {
10
+ "typecheck": "tsc --noEmit",
11
+ "generate": "drizzle-kit generate",
12
+ "lint": "bun run lint:code",
13
+ "lint:code": "eslint . --max-warnings 0"
14
+ },
15
+ "dependencies": {
16
+ "@checkstack/backend-api": "0.12.0",
17
+ "@checkstack/gitops-common": "0.0.1",
18
+ "@checkstack/common": "0.6.5",
19
+ "@checkstack/command-backend": "0.1.19",
20
+ "@checkstack/queue-api": "0.2.13",
21
+ "@orpc/server": "^1.13.2",
22
+ "drizzle-orm": "^0.45.0",
23
+ "minimatch": "^10.0.0",
24
+ "uuid": "^13.0.0",
25
+ "zod": "^4.2.1",
26
+ "yaml": "^2.7.0"
27
+ },
28
+ "devDependencies": {
29
+ "@checkstack/drizzle-helper": "0.0.4",
30
+ "@checkstack/scripts": "0.1.2",
31
+ "@checkstack/tsconfig": "0.0.5",
32
+ "@types/bun": "^1.3.5",
33
+ "@types/node": "^20.0.0",
34
+ "@types/uuid": "^11.0.0",
35
+ "typescript": "^5.0.0"
36
+ }
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,136 @@
1
+ import {
2
+ createBackendPlugin,
3
+ createExtensionPoint,
4
+ coreServices,
5
+ } from "@checkstack/backend-api";
6
+ import type { SafeDatabase } from "@checkstack/backend-api";
7
+ import {
8
+ pluginMetadata,
9
+ gitopsAccessRules,
10
+ gitopsContract,
11
+ } from "@checkstack/gitops-common";
12
+ import type {
13
+ EntityKindDefinition,
14
+ EntityKindExtensionDefinition,
15
+ EntityKindRegistry,
16
+ } from "@checkstack/gitops-common";
17
+ import { createEntityKindRegistry } from "./kind-registry";
18
+ import { createGitOpsRouter } from "./router";
19
+ import { setupSyncWorker } from "./sync/sync-worker";
20
+ import { decrypt } from "@checkstack/backend-api";
21
+ import * as schema from "./schema";
22
+ import { eq } from "drizzle-orm";
23
+
24
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25
+ // Extension Points
26
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27
+
28
+ /**
29
+ * Extension point for the Entity Kind Registry.
30
+ * Plugins use this during their `register()` phase to register entity kinds
31
+ * and spec extensions for the GitOps system.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * import { entityKindExtensionPoint } from "@checkstack/gitops-backend";
36
+ *
37
+ * // In your plugin's register() function:
38
+ * const registry = env.getExtensionPoint(entityKindExtensionPoint);
39
+ * registry.registerKind({
40
+ * apiVersion: "checkstack.io/v1alpha1",
41
+ * kind: "System",
42
+ * specSchema: z.object({ description: z.string().optional() }),
43
+ * reconcile: async ({ entity }) => { ... },
44
+ * });
45
+ * ```
46
+ */
47
+ export const entityKindExtensionPoint =
48
+ createExtensionPoint<EntityKindRegistry>("gitops.entity-kind-registry");
49
+
50
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
51
+ // Plugin Definition
52
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
53
+
54
+ export default createBackendPlugin({
55
+ metadata: pluginMetadata,
56
+
57
+ register(env) {
58
+ // Create the kind registry
59
+ const kindRegistry = createEntityKindRegistry();
60
+
61
+ // Register access rules
62
+ env.registerAccessRules(gitopsAccessRules);
63
+
64
+ // Register the Entity Kind Extension Point
65
+ // Other plugins call this to register their entity kinds and extensions
66
+ env.registerExtensionPoint(entityKindExtensionPoint, {
67
+ registerKind<TSpec>(definition: EntityKindDefinition<TSpec>) {
68
+ kindRegistry.registerKind(definition);
69
+ },
70
+ registerKindExtension<TExtensionSpec>(
71
+ definition: EntityKindExtensionDefinition<TExtensionSpec>,
72
+ ) {
73
+ kindRegistry.registerKindExtension(definition);
74
+ },
75
+ });
76
+
77
+ env.registerInit({
78
+ schema,
79
+ deps: {
80
+ logger: coreServices.logger,
81
+ rpc: coreServices.rpc,
82
+ queueManager: coreServices.queueManager,
83
+ },
84
+ init: async ({ logger, database, rpc, queueManager }) => {
85
+ logger.debug("🔄 Initializing GitOps Backend...");
86
+
87
+ const db = database as SafeDatabase<typeof schema>;
88
+
89
+ const router = createGitOpsRouter({ database: db, queueManager, kindRegistry });
90
+ rpc.registerRouter(router, gitopsContract);
91
+
92
+ logger.debug("✅ GitOps Backend initialized.");
93
+ },
94
+ afterPluginsReady: async ({ logger, database, queueManager }) => {
95
+ const registeredKinds = kindRegistry.getKinds();
96
+ logger.debug(
97
+ `🔄 GitOps: ${registeredKinds.length} entity kinds registered: ${registeredKinds.map((k) => k.kind).join(", ")}`,
98
+ );
99
+
100
+ const db = database as SafeDatabase<typeof schema>;
101
+
102
+ // Create a SecretStore backed by the secrets table
103
+ const secretStore = {
104
+ resolve: async (name: string): Promise<string> => {
105
+ const rows = await db
106
+ .select()
107
+ .from(schema.secrets)
108
+ .where(eq(schema.secrets.name, name));
109
+ const secret = rows[0];
110
+ if (!secret) {
111
+ throw new Error(`Secret not found: ${name}`);
112
+ }
113
+ return decrypt(secret.encryptedValue);
114
+ },
115
+ };
116
+
117
+ // Bootstrap sync worker
118
+ await setupSyncWorker({
119
+ db,
120
+ logger,
121
+ queueManager,
122
+ kindRegistry,
123
+ secretStore,
124
+ });
125
+ },
126
+ });
127
+ },
128
+ });
129
+
130
+ // Re-export types for consumer plugins
131
+ export type {
132
+ EntityKindDefinition,
133
+ EntityKindExtensionDefinition,
134
+ EntityKindRegistry,
135
+ ReconcileContext,
136
+ } from "@checkstack/gitops-common";