@checkstack/gitops-backend 0.2.2 → 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,14 @@
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
+
3
12
  ## 0.2.2
4
13
 
5
14
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/gitops-backend",
3
- "version": "0.2.2",
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
  }