@a5c-ai/kradle 5.0.1-staging.3abdf9534c25
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/Dockerfile +31 -0
- package/README.md +187 -0
- package/bin/kradle-demo.mjs +23 -0
- package/bin/kradle-server.mjs +14 -0
- package/dist/kradle-controller-ui.json +3482 -0
- package/dist/kradle-lifecycle.json +201 -0
- package/dist/kradle-runtime-snapshot.json +3125 -0
- package/dist/kradle-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/architecture-v2.md +2759 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/crd-behaviors-and-relationships.md +3926 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/integration-and-design-decisions.md +1530 -0
- package/docs/kradle-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1291 -0
- package/docs/product-requirements.md +62 -0
- package/docs/requirements-v2.md +235 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/sdk-api-reference.md +1108 -0
- package/docs/system-requirements.md +90 -0
- package/docs/system-spec-v2.md +1230 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/docs/web-console-spec.md +533 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +66 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +95 -0
- package/scripts/validate-ui.mjs +305 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +549 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-identity-migration.js +115 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +589 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-persona-controller.js +135 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-prompt-composition.js +55 -0
- package/src/agent-provider-config-controller.js +151 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +421 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +387 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +621 -0
- package/src/argocd-gitops.js +43 -0
- package/src/artifact-registry-controller.js +542 -0
- package/src/assistant-runtime.js +284 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +310 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +112 -0
- package/src/controller-ui.js +620 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +397 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +221 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/health-probes.js +134 -0
- package/src/hooks-events.js +63 -0
- package/src/hooks-lifecycle.js +117 -0
- package/src/http-server.js +409 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +71 -0
- package/src/jitsi-agent-bridge.js +141 -0
- package/src/jitsi-meeting-controller.js +291 -0
- package/src/jitsi-sync-controller.js +198 -0
- package/src/kradle-inference-service-controller.js +246 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +904 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/model-route-controller.js +364 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +282 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/virtual-model-controller.js +538 -0
- package/src/virtual-model-hook-bridge.js +200 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +679 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-identity-migration.test.js +87 -0
- package/tests/agent-memory-controller.test.js +461 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +389 -0
- package/tests/agent-mux-integration.test.js +971 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-persona-controller.test.js +127 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-prompt-composition.test.js +76 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +303 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +283 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +271 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/artifact-registry.test.js +511 -0
- package/tests/assistant-runtime.test.js +506 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/controller-client.test.js +133 -0
- package/tests/deployment.test.js +527 -0
- package/tests/e2e/lifecycle.test.js +120 -0
- package/tests/event-bus-integration.test.js +355 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +415 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +223 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/health-probes.test.js +90 -0
- package/tests/hooks-lifecycle.test.js +364 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/jitsi-agent-bridge.test.js +119 -0
- package/tests/jitsi-helm-integration.test.js +77 -0
- package/tests/jitsi-meeting-controller.test.js +170 -0
- package/tests/jitsi-resource-model.test.js +73 -0
- package/tests/jitsi-sync-controller.test.js +112 -0
- package/tests/kradle-inference-service.test.js +689 -0
- package/tests/kradle.test.js +779 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/model-route-controller.test.js +733 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +315 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/virtual-model-controller.test.js +877 -0
- package/tests/virtual-model-hook-bridge.test.js +384 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// External Conflict Controller — Slice 3.5
|
|
2
|
+
// Detects field divergence between local Kradle state and external provider state,
|
|
3
|
+
// manages resolution workflows, and handles superseded conflict cleanup.
|
|
4
|
+
|
|
5
|
+
import { createResource, clone } from '../resource-model.js';
|
|
6
|
+
|
|
7
|
+
export const CONFLICT_CONTROLLER_BOUNDARY = {
|
|
8
|
+
role: 'external-conflict-controller',
|
|
9
|
+
scope: 'Field-level conflict detection and resolution for ExternalSyncConflict resources',
|
|
10
|
+
owns: ['conflict detection', 'resolution workflow', 'superseded cleanup', 'open conflict listing'],
|
|
11
|
+
delegatesTo: ['resource-model'],
|
|
12
|
+
mustNotOwn: ['write intent lifecycle', 'sync scheduling', 'external API client']
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const VALID_STRATEGIES = ['prefer-external', 'prefer-kradle', 'manual', 'ignore'];
|
|
16
|
+
const OPEN_PHASES = new Set(['Open']);
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Validation
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate a conflict detection input object.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} input
|
|
26
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
27
|
+
*/
|
|
28
|
+
export function validateConflict(input) {
|
|
29
|
+
const errors = [];
|
|
30
|
+
if (!input) {
|
|
31
|
+
return { valid: false, errors: ['input must not be null or undefined'] };
|
|
32
|
+
}
|
|
33
|
+
if (!input.resourceRef || typeof input.resourceRef !== 'string') {
|
|
34
|
+
errors.push('resourceRef is required and must be a non-empty string');
|
|
35
|
+
}
|
|
36
|
+
if (!input.fieldPath || typeof input.fieldPath !== 'string') {
|
|
37
|
+
errors.push('fieldPath is required and must be a non-empty string');
|
|
38
|
+
}
|
|
39
|
+
return { valid: errors.length === 0, errors };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Controller factory
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a ConflictController that manages ExternalSyncConflict resources.
|
|
48
|
+
*
|
|
49
|
+
* @param {{ persistFn?: (resource: object) => Promise<any> }} [opts]
|
|
50
|
+
* Optional persistFn is called (fire-and-forget) after conflict state changes.
|
|
51
|
+
* @returns {object}
|
|
52
|
+
*/
|
|
53
|
+
export function createConflictController({ persistFn } = {}) {
|
|
54
|
+
/**
|
|
55
|
+
* Fire-and-forget persistence helper.
|
|
56
|
+
* @param {object} resource
|
|
57
|
+
*/
|
|
58
|
+
function persist(resource) {
|
|
59
|
+
if (typeof persistFn === 'function') {
|
|
60
|
+
Promise.resolve(persistFn(resource)).catch(() => {});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
role: 'conflict-controller',
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Detect a conflict between local and external field values.
|
|
69
|
+
* Returns { conflict: null } when values match (no conflict).
|
|
70
|
+
* Returns { conflict: ExternalSyncConflict } when values differ.
|
|
71
|
+
*
|
|
72
|
+
* @param {{ resourceRef, fieldPath, localValue, externalValue, namespace?, organizationRef? }} input
|
|
73
|
+
* @returns {{ conflict: object|null }}
|
|
74
|
+
*/
|
|
75
|
+
detectConflict({
|
|
76
|
+
resourceRef,
|
|
77
|
+
fieldPath,
|
|
78
|
+
localValue,
|
|
79
|
+
externalValue,
|
|
80
|
+
namespace = 'default',
|
|
81
|
+
organizationRef = 'default'
|
|
82
|
+
}) {
|
|
83
|
+
const validation = validateConflict({ resourceRef, fieldPath });
|
|
84
|
+
if (!validation.valid) {
|
|
85
|
+
return { conflict: null, error: true, message: validation.errors.join('; ') };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Values match — no conflict
|
|
89
|
+
if (localValue === externalValue) {
|
|
90
|
+
return { conflict: null };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const now = new Date().toISOString();
|
|
94
|
+
const conflictName = `conflict-${resourceRef.replace(/[^a-zA-Z0-9]/g, '-')}-${fieldPath.replace(/[^a-zA-Z0-9]/g, '-')}-${Date.now()}`;
|
|
95
|
+
|
|
96
|
+
const conflict = createResource('ExternalSyncConflict', { name: conflictName, namespace }, {
|
|
97
|
+
organizationRef,
|
|
98
|
+
resourceRef,
|
|
99
|
+
fieldPath,
|
|
100
|
+
localValue,
|
|
101
|
+
externalValue,
|
|
102
|
+
detectedAt: now
|
|
103
|
+
});
|
|
104
|
+
conflict.status = {
|
|
105
|
+
phase: 'Open',
|
|
106
|
+
detectedAt: now
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
persist(conflict);
|
|
110
|
+
return { conflict };
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve an Open conflict using the specified strategy.
|
|
115
|
+
*
|
|
116
|
+
* Strategies:
|
|
117
|
+
* - prefer-external: choose externalValue, phase → Resolved
|
|
118
|
+
* - prefer-kradle: choose localValue, phase → Resolved
|
|
119
|
+
* - manual: choose resolvedValue, phase → Resolved (requires resolvedValue)
|
|
120
|
+
* - ignore: phase → Ignored (no value chosen)
|
|
121
|
+
*
|
|
122
|
+
* @param {{ conflictName, strategy, resolvedValue?, resources }} opts
|
|
123
|
+
* @returns {{ conflict: object, resolution: object } | { error: true, message: string }}
|
|
124
|
+
*/
|
|
125
|
+
resolveConflict({ conflictName, strategy, resolvedValue, resources = {} }) {
|
|
126
|
+
if (!conflictName) {
|
|
127
|
+
return { error: true, reason: 'missing-name', message: 'conflictName is required' };
|
|
128
|
+
}
|
|
129
|
+
if (!strategy || !VALID_STRATEGIES.includes(strategy)) {
|
|
130
|
+
return {
|
|
131
|
+
error: true,
|
|
132
|
+
reason: 'invalid-strategy',
|
|
133
|
+
message: `strategy must be one of: ${VALID_STRATEGIES.join(', ')}`
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const conflicts = resources.ExternalSyncConflict || [];
|
|
138
|
+
const found = conflicts.find((c) => c.metadata?.name === conflictName);
|
|
139
|
+
if (!found) {
|
|
140
|
+
return { error: true, reason: 'not-found', message: `ExternalSyncConflict not found: ${conflictName}` };
|
|
141
|
+
}
|
|
142
|
+
if (found.status?.phase !== 'Open') {
|
|
143
|
+
return { error: true, reason: 'invalid-phase', message: `Conflict is not Open: ${found.status?.phase}` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const updated = clone(found);
|
|
147
|
+
const now = new Date().toISOString();
|
|
148
|
+
let chosenValue;
|
|
149
|
+
let newPhase;
|
|
150
|
+
|
|
151
|
+
if (strategy === 'ignore') {
|
|
152
|
+
newPhase = 'Ignored';
|
|
153
|
+
chosenValue = undefined;
|
|
154
|
+
} else {
|
|
155
|
+
newPhase = 'Resolved';
|
|
156
|
+
if (strategy === 'prefer-external') {
|
|
157
|
+
chosenValue = found.spec.externalValue;
|
|
158
|
+
} else if (strategy === 'prefer-kradle') {
|
|
159
|
+
chosenValue = found.spec.localValue;
|
|
160
|
+
} else if (strategy === 'manual') {
|
|
161
|
+
if (resolvedValue === undefined) {
|
|
162
|
+
return { error: true, reason: 'missing-resolved-value', message: 'resolvedValue is required for manual strategy' };
|
|
163
|
+
}
|
|
164
|
+
chosenValue = resolvedValue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
updated.status = {
|
|
169
|
+
...updated.status,
|
|
170
|
+
phase: newPhase,
|
|
171
|
+
resolvedAt: now,
|
|
172
|
+
strategy,
|
|
173
|
+
chosenValue
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const resolution = { strategy, chosenValue };
|
|
177
|
+
|
|
178
|
+
persist(updated);
|
|
179
|
+
return { conflict: updated, resolution };
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Mark all Open conflicts for a given (resourceRef, fieldPath) pair as Superseded.
|
|
184
|
+
* Used when a new sync event arrives that makes old conflicts irrelevant.
|
|
185
|
+
*
|
|
186
|
+
* @param {{ resourceRef, fieldPath, resources }} opts
|
|
187
|
+
* @returns {{ superseded: object[] }}
|
|
188
|
+
*/
|
|
189
|
+
supersededCheck({ resourceRef, fieldPath, resources = {} }) {
|
|
190
|
+
const conflicts = resources.ExternalSyncConflict || [];
|
|
191
|
+
const now = new Date().toISOString();
|
|
192
|
+
|
|
193
|
+
const superseded = [];
|
|
194
|
+
for (const c of conflicts) {
|
|
195
|
+
if (
|
|
196
|
+
c.spec?.resourceRef === resourceRef &&
|
|
197
|
+
c.spec?.fieldPath === fieldPath &&
|
|
198
|
+
c.status?.phase === 'Open'
|
|
199
|
+
) {
|
|
200
|
+
const updated = clone(c);
|
|
201
|
+
updated.status = {
|
|
202
|
+
...updated.status,
|
|
203
|
+
phase: 'Superseded',
|
|
204
|
+
supersededAt: now
|
|
205
|
+
};
|
|
206
|
+
superseded.push(updated);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { superseded };
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Return all Open (non-resolved, non-ignored, non-superseded) conflicts.
|
|
215
|
+
*
|
|
216
|
+
* @param {{ resources }} opts
|
|
217
|
+
* @returns {{ conflicts: object[] }}
|
|
218
|
+
*/
|
|
219
|
+
getOpenConflicts({ resources = {} } = {}) {
|
|
220
|
+
const conflicts = resources.ExternalSyncConflict || [];
|
|
221
|
+
const open = conflicts.filter((c) => OPEN_PHASES.has(c.status?.phase));
|
|
222
|
+
return { conflicts: open };
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// GitHub App authentication helpers — Slice 3.3a
|
|
2
|
+
// Provides JWT signing (HMAC-SHA256 for test, RSA-SHA256 for production)
|
|
3
|
+
// and installation token exchange. No external dependencies; uses node:crypto.
|
|
4
|
+
|
|
5
|
+
import { createHmac, createSign } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
const GITHUB_API = 'https://api.github.com';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Encode a value as Base64url (RFC 4648 §5, no padding).
|
|
11
|
+
* @param {string|Buffer} data
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
function b64url(data) {
|
|
15
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
16
|
+
return buf.toString('base64url');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a GitHub App JWT.
|
|
21
|
+
*
|
|
22
|
+
* In production, pass a PEM-encoded RSA private key and the function will
|
|
23
|
+
* use RS256. For unit tests, pass any string key; if it does not look like
|
|
24
|
+
* a PEM file the function falls back to HS256 (HMAC-SHA256) so tests can
|
|
25
|
+
* run without real RSA keys.
|
|
26
|
+
*
|
|
27
|
+
* @param {{ appId: string, privateKey: string, expiresInSeconds?: number }} opts
|
|
28
|
+
* @returns {Promise<string>} A signed JWT string.
|
|
29
|
+
*/
|
|
30
|
+
export async function createGitHubJwt({ appId, privateKey, expiresInSeconds = 600 } = {}) {
|
|
31
|
+
if (!appId) throw new Error('createGitHubJwt: appId is required');
|
|
32
|
+
if (!privateKey) throw new Error('createGitHubJwt: privateKey is required');
|
|
33
|
+
|
|
34
|
+
const now = Math.floor(Date.now() / 1000);
|
|
35
|
+
const isRsa = privateKey.includes('-----BEGIN');
|
|
36
|
+
|
|
37
|
+
const alg = isRsa ? 'RS256' : 'HS256';
|
|
38
|
+
|
|
39
|
+
const header = b64url(JSON.stringify({ alg, typ: 'JWT' }));
|
|
40
|
+
const payload = b64url(JSON.stringify({
|
|
41
|
+
iat: now,
|
|
42
|
+
exp: now + expiresInSeconds,
|
|
43
|
+
iss: appId
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const signingInput = `${header}.${payload}`;
|
|
47
|
+
|
|
48
|
+
let signature;
|
|
49
|
+
if (isRsa) {
|
|
50
|
+
const sign = createSign('RSA-SHA256');
|
|
51
|
+
sign.update(signingInput);
|
|
52
|
+
sign.end();
|
|
53
|
+
signature = sign.sign(privateKey, 'base64url');
|
|
54
|
+
} else {
|
|
55
|
+
// HMAC-SHA256 fallback (test mode only)
|
|
56
|
+
const hmac = createHmac('sha256', privateKey);
|
|
57
|
+
hmac.update(signingInput);
|
|
58
|
+
signature = hmac.digest('base64url');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return `${signingInput}.${signature}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Exchange a GitHub App JWT for an installation access token.
|
|
66
|
+
*
|
|
67
|
+
* @param {{ appJwt: string, installationId: string|number, fetchImpl?: Function }} opts
|
|
68
|
+
* @returns {Promise<{ token: string, expiresAt: string }>}
|
|
69
|
+
*/
|
|
70
|
+
export async function exchangeInstallationToken({ appJwt, installationId, fetchImpl = globalThis.fetch } = {}) {
|
|
71
|
+
if (!appJwt) throw new Error('exchangeInstallationToken: appJwt is required');
|
|
72
|
+
if (!installationId) throw new Error('exchangeInstallationToken: installationId is required');
|
|
73
|
+
if (!fetchImpl) throw new Error('exchangeInstallationToken: a fetch implementation is required');
|
|
74
|
+
|
|
75
|
+
const url = `${GITHUB_API}/app/installations/${installationId}/access_tokens`;
|
|
76
|
+
|
|
77
|
+
const response = await fetchImpl(url, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
Accept: 'application/vnd.github+json',
|
|
81
|
+
Authorization: `Bearer ${appJwt}`,
|
|
82
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
83
|
+
'Content-Type': 'application/json'
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(`exchangeInstallationToken: GitHub API returned ${response.status} — authentication or token exchange failed`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = await response.json();
|
|
92
|
+
return {
|
|
93
|
+
token: data.token,
|
|
94
|
+
expiresAt: data.expires_at
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// GitHub CI/CD implementation — Slice 3.3b
|
|
2
|
+
// Implements the cicd interface contract:
|
|
3
|
+
// listWorkflowRuns, listJobs, rerunWorkflow, cancelWorkflow, createCheck, updateCheck
|
|
4
|
+
//
|
|
5
|
+
// All HTTP calls are injected via fetchImpl for full testability.
|
|
6
|
+
|
|
7
|
+
const GITHUB_API = 'https://api.github.com';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{ id: number, name: string, status: string, conclusion: string|null, headBranch: string, headSha: string, htmlUrl: string, createdAt: string, updatedAt: string }} NormalizedWorkflowRun
|
|
11
|
+
* @typedef {{ id: number, name: string, status: string, conclusion: string|null, startedAt: string, completedAt: string, htmlUrl: string }} NormalizedJob
|
|
12
|
+
* @typedef {{ id: number, name: string, status: string, conclusion: string|null, htmlUrl: string }} NormalizedCheckRun
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* GitHub implementation of the cicd interface.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ owner: string, installationToken: string, fetchImpl?: Function }} opts
|
|
19
|
+
*/
|
|
20
|
+
export class GitHubCicd {
|
|
21
|
+
constructor({ owner, installationToken, fetchImpl = globalThis.fetch } = {}) {
|
|
22
|
+
if (!owner) throw new Error('GitHubCicd: owner (org or user) is required');
|
|
23
|
+
if (!installationToken) throw new Error('GitHubCicd: installationToken is required');
|
|
24
|
+
if (!fetchImpl) throw new Error('GitHubCicd: a fetch implementation is required');
|
|
25
|
+
|
|
26
|
+
this.role = 'github-cicd';
|
|
27
|
+
this._owner = owner;
|
|
28
|
+
this._token = installationToken;
|
|
29
|
+
this._fetch = fetchImpl;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Internal helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
_headers() {
|
|
37
|
+
return {
|
|
38
|
+
Accept: 'application/vnd.github+json',
|
|
39
|
+
Authorization: `Bearer ${this._token}`,
|
|
40
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
41
|
+
'Content-Type': 'application/json'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async _request(method, path, body) {
|
|
46
|
+
const url = `${GITHUB_API}${path}`;
|
|
47
|
+
const options = {
|
|
48
|
+
method,
|
|
49
|
+
headers: this._headers(),
|
|
50
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {})
|
|
51
|
+
};
|
|
52
|
+
const response = await this._fetch(url, options);
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new Error(`GitHub ${method} ${path} failed with status ${response.status}`);
|
|
55
|
+
}
|
|
56
|
+
if (response.status === 204) return null;
|
|
57
|
+
return response.json();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_repoPath(repo, suffix = '') {
|
|
61
|
+
return `/repos/${encodeURIComponent(this._owner)}/${encodeURIComponent(repo)}${suffix}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Normalizers
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
_normalizeRun(data) {
|
|
69
|
+
return {
|
|
70
|
+
id: data.id,
|
|
71
|
+
name: data.name ?? '',
|
|
72
|
+
status: data.status ?? '',
|
|
73
|
+
conclusion: data.conclusion ?? null,
|
|
74
|
+
headBranch: data.head_branch ?? '',
|
|
75
|
+
headSha: data.head_sha ?? '',
|
|
76
|
+
htmlUrl: data.html_url ?? '',
|
|
77
|
+
createdAt: data.created_at ?? '',
|
|
78
|
+
updatedAt: data.updated_at ?? ''
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_normalizeJob(data) {
|
|
83
|
+
return {
|
|
84
|
+
id: data.id,
|
|
85
|
+
name: data.name ?? '',
|
|
86
|
+
status: data.status ?? '',
|
|
87
|
+
conclusion: data.conclusion ?? null,
|
|
88
|
+
startedAt: data.started_at ?? '',
|
|
89
|
+
completedAt: data.completed_at ?? '',
|
|
90
|
+
htmlUrl: data.html_url ?? ''
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_normalizeCheckRun(data) {
|
|
95
|
+
return {
|
|
96
|
+
id: data.id,
|
|
97
|
+
name: data.name ?? '',
|
|
98
|
+
status: data.status ?? '',
|
|
99
|
+
conclusion: data.conclusion ?? null,
|
|
100
|
+
htmlUrl: data.html_url ?? ''
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Interface methods
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* List workflow runs for a repository (optionally filtered by workflow file name).
|
|
110
|
+
* @param {{ repo: string, workflowId?: string|number }} opts
|
|
111
|
+
* @returns {Promise<NormalizedWorkflowRun[]>}
|
|
112
|
+
*/
|
|
113
|
+
async listWorkflowRuns({ repo, workflowId } = {}) {
|
|
114
|
+
const path = workflowId
|
|
115
|
+
? this._repoPath(repo, `/actions/workflows/${encodeURIComponent(workflowId)}/runs`)
|
|
116
|
+
: this._repoPath(repo, '/actions/runs');
|
|
117
|
+
const data = await this._request('GET', path);
|
|
118
|
+
const runs = data?.workflow_runs ?? data ?? [];
|
|
119
|
+
return runs.map(r => this._normalizeRun(r));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* List jobs for a specific workflow run.
|
|
124
|
+
* @param {{ repo: string, runId: number }} opts
|
|
125
|
+
* @returns {Promise<NormalizedJob[]>}
|
|
126
|
+
*/
|
|
127
|
+
async listJobs({ repo, runId } = {}) {
|
|
128
|
+
const data = await this._request('GET', this._repoPath(repo, `/actions/runs/${runId}/jobs`));
|
|
129
|
+
const jobs = data?.jobs ?? data ?? [];
|
|
130
|
+
return jobs.map(j => this._normalizeJob(j));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Trigger a re-run of a workflow run.
|
|
135
|
+
* @param {{ repo: string, runId: number }} opts
|
|
136
|
+
* @returns {Promise<{ triggered: boolean, runId: number }>}
|
|
137
|
+
*/
|
|
138
|
+
async rerunWorkflow({ repo, runId } = {}) {
|
|
139
|
+
await this._request('POST', this._repoPath(repo, `/actions/runs/${runId}/rerun`));
|
|
140
|
+
return { triggered: true, runId };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Cancel a workflow run.
|
|
145
|
+
* @param {{ repo: string, runId: number }} opts
|
|
146
|
+
* @returns {Promise<{ cancelled: boolean, runId: number }>}
|
|
147
|
+
*/
|
|
148
|
+
async cancelWorkflow({ repo, runId } = {}) {
|
|
149
|
+
await this._request('POST', this._repoPath(repo, `/actions/runs/${runId}/cancel`));
|
|
150
|
+
return { cancelled: true, runId };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create a check run on a commit.
|
|
155
|
+
* @param {{ repo: string, name: string, headSha: string, status?: string, conclusion?: string, detailsUrl?: string, output?: object }} opts
|
|
156
|
+
* @returns {Promise<NormalizedCheckRun>}
|
|
157
|
+
*/
|
|
158
|
+
async createCheck({ repo, name, headSha, status = 'queued', conclusion, detailsUrl, output } = {}) {
|
|
159
|
+
const payload = { name, head_sha: headSha, status };
|
|
160
|
+
if (conclusion !== undefined) payload.conclusion = conclusion;
|
|
161
|
+
if (detailsUrl !== undefined) payload.details_url = detailsUrl;
|
|
162
|
+
if (output !== undefined) payload.output = output;
|
|
163
|
+
const data = await this._request('POST', this._repoPath(repo, '/check-runs'), payload);
|
|
164
|
+
return this._normalizeCheckRun(data);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Update an existing check run.
|
|
169
|
+
* @param {{ repo: string, checkRunId: number, status?: string, conclusion?: string, output?: object }} opts
|
|
170
|
+
* @returns {Promise<NormalizedCheckRun>}
|
|
171
|
+
*/
|
|
172
|
+
async updateCheck({ repo, checkRunId, status, conclusion, output } = {}) {
|
|
173
|
+
const payload = {};
|
|
174
|
+
if (status !== undefined) payload.status = status;
|
|
175
|
+
if (conclusion !== undefined) payload.conclusion = conclusion;
|
|
176
|
+
if (output !== undefined) payload.output = output;
|
|
177
|
+
const data = await this._request('PATCH', this._repoPath(repo, `/check-runs/${checkRunId}`), payload);
|
|
178
|
+
return this._normalizeCheckRun(data);
|
|
179
|
+
}
|
|
180
|
+
}
|