@checkstack/gitops-backend 0.2.1 → 0.2.3

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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # @checkstack/gitops-backend
2
2
 
3
+ ## 0.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - adc89a8: Fix GitOps engine skipping retry of failed entities
8
+
9
+ - Updated the fast-path condition in the Reconciler engine to only skip reconciliation if the entity is in a `synced` state.
10
+ - Prevents entities from remaining permanently stuck in an error state without being retried if the underlying YAML file is not modified.
11
+
12
+ ## 0.2.2
13
+
14
+ ### Patch Changes
15
+
16
+ - b53a40e: Fix GitOps entity update failures due to pending error records
17
+
18
+ - Ensured the `existingEntityId` parameter in the Reconciler engine is set to `undefined` instead of a `"pending-UUID"` when handling entities that failed to sync initially.
19
+ - Hardened the `Healthcheck` GitOps kind logic to explicitly ignore `"pending-"` IDs, preventing SQL update errors on synthetic provenance IDs.
20
+ - Fixed a bug where resolving YAML syntax errors would cause the subsequent sync to fail with `failed query: update [...]` because it attempted to update the nonexistent `"pending-"` entity instead of creating a new one.
21
+
3
22
  ## 0.2.1
4
23
 
5
24
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/gitops-backend",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "checkstack": {
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect, mock } from "bun:test";
2
+ import { reconcileProvider } from "./reconciler";
3
+ import { createEntityKindRegistry } from "../kind-registry";
4
+ import { CHECKSTACK_API_VERSION } from "@checkstack/gitops-common";
5
+ import { z } from "zod";
6
+ import { computeHash } from "./document-parser";
7
+
8
+ const DUMMY_YAML = `apiVersion: ${CHECKSTACK_API_VERSION}
9
+ kind: TestKind
10
+ metadata:
11
+ name: test-entity
12
+ spec: {}`;
13
+
14
+ describe("GitOps reconciler fast-path retry logic", () => {
15
+ it("skips reconciliation if file hash matches and status is synced", async () => {
16
+ const hash = computeHash({ input: DUMMY_YAML });
17
+ let whereCalls = 0;
18
+
19
+ const mockDb = {
20
+ select: () => mockDb,
21
+ from: () => mockDb,
22
+ where: async () => {
23
+ whereCalls++;
24
+ if (whereCalls === 1) { // existingProvenance lookup
25
+ return [{
26
+ id: "prov-1",
27
+ status: "synced",
28
+ lastSyncHash: hash,
29
+ entityId: "real-id"
30
+ }];
31
+ }
32
+ return [];
33
+ },
34
+ update: () => mockDb,
35
+ set: () => mockDb,
36
+ insert: () => mockDb,
37
+ values: async () => [],
38
+ delete: () => mockDb,
39
+ } as any;
40
+
41
+ const kindRegistry = createEntityKindRegistry();
42
+ const reconcileMock = mock(async () => ({ entityId: "new-id" }));
43
+ kindRegistry.registerKind({
44
+ apiVersion: CHECKSTACK_API_VERSION,
45
+ kind: "TestKind",
46
+ specSchema: z.object({}),
47
+ reconcile: reconcileMock,
48
+ });
49
+
50
+ const result = await reconcileProvider({
51
+ providerId: "test-provider",
52
+ providerType: "github",
53
+ target: "test-target",
54
+ pathPattern: "*.yaml",
55
+ deletionPolicy: "orphan",
56
+ db: mockDb,
57
+ logger: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} } as any,
58
+ kindRegistry,
59
+ secretStore: { resolve: async () => {} } as any,
60
+ scraper: {
61
+ discoverFiles: async () => [{
62
+ repository: "repo",
63
+ filePath: "test.yaml",
64
+ branch: "main",
65
+ content: DUMMY_YAML,
66
+ }],
67
+ },
68
+ });
69
+
70
+ // Should skip because hash matches and status is synced
71
+ expect(result.unchanged).toBe(1);
72
+ expect(result.updated).toBe(0);
73
+ expect(result.created).toBe(0);
74
+ expect(reconcileMock).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it("retries reconciliation if file hash matches but status is error", async () => {
78
+ const hash = computeHash({ input: DUMMY_YAML });
79
+ let whereCalls = 0;
80
+
81
+ const mockDb = {
82
+ select: () => mockDb,
83
+ from: () => mockDb,
84
+ where: async () => {
85
+ whereCalls++;
86
+ if (whereCalls === 1) { // existingProvenance lookup
87
+ return [{
88
+ id: "prov-1",
89
+ status: "error",
90
+ lastSyncHash: hash,
91
+ entityId: "pending-123"
92
+ }];
93
+ }
94
+ return [];
95
+ },
96
+ update: () => mockDb,
97
+ set: () => mockDb,
98
+ insert: () => mockDb,
99
+ values: async () => [],
100
+ delete: () => mockDb,
101
+ } as any;
102
+
103
+ const kindRegistry = createEntityKindRegistry();
104
+ const reconcileMock = mock(async () => ({ entityId: "new-id" }));
105
+ kindRegistry.registerKind({
106
+ apiVersion: CHECKSTACK_API_VERSION,
107
+ kind: "TestKind",
108
+ specSchema: z.object({}),
109
+ reconcile: reconcileMock,
110
+ });
111
+
112
+ const result = await reconcileProvider({
113
+ providerId: "test-provider",
114
+ providerType: "github",
115
+ target: "test-target",
116
+ pathPattern: "*.yaml",
117
+ deletionPolicy: "orphan",
118
+ db: mockDb,
119
+ logger: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} } as any,
120
+ kindRegistry,
121
+ secretStore: { resolve: async () => {} } as any,
122
+ scraper: {
123
+ discoverFiles: async () => [{
124
+ repository: "repo",
125
+ filePath: "test.yaml",
126
+ branch: "main",
127
+ content: DUMMY_YAML,
128
+ }],
129
+ },
130
+ });
131
+
132
+ // Should retry because status is error, even if hash matches
133
+ expect(result.unchanged).toBe(0);
134
+ expect(result.updated).toBe(1);
135
+ expect(reconcileMock).toHaveBeenCalledTimes(1);
136
+ });
137
+ });
@@ -298,8 +298,8 @@ async function reconcileEntity(params: {
298
298
 
299
299
  const existing = existingProvenance[0];
300
300
 
301
- if (existing && existing.lastSyncHash === contentHash) {
302
- // Unchanged — skip reconciliation
301
+ if (existing && existing.status === "synced" && existing.lastSyncHash === contentHash) {
302
+ // Unchanged and synced — skip reconciliation
303
303
  result.unchanged++;
304
304
  return;
305
305
  }
@@ -308,7 +308,7 @@ async function reconcileEntity(params: {
308
308
  // Plugins use context.resolveSecretsBySchema() to resolve specific fields.
309
309
  const reconcileResult = await kindDef.reconcile({
310
310
  entity: entity as typeof entity & { spec: Record<string, unknown> },
311
- existingEntityId: existing?.entityId ?? undefined,
311
+ existingEntityId: existing && !existing.entityId.startsWith("pending-") ? existing.entityId : undefined,
312
312
  context,
313
313
  });
314
314