@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 +19 -0
- package/package.json +1 -1
- package/src/sync/reconciler-fast-path.test.ts +137 -0
- package/src/sync/reconciler.ts +3 -3
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
|
@@ -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
|
+
});
|
package/src/sync/reconciler.ts
CHANGED
|
@@ -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
|
|
311
|
+
existingEntityId: existing && !existing.entityId.startsWith("pending-") ? existing.entityId : undefined,
|
|
312
312
|
context,
|
|
313
313
|
});
|
|
314
314
|
|