@a5c-ai/krate 5.0.1-staging.04a3db697
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 +183 -0
- package/bin/krate-demo.mjs +23 -0
- package/bin/krate-server.mjs +14 -0
- package/dist/krate-controller-ui.json +3067 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +2955 -0
- package/dist/krate-summary.json +722 -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-krate-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/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/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/krate-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/product-requirements.md +62 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/system-requirements.md +90 -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/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 +63 -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 +93 -0
- package/scripts/validate-ui.mjs +236 -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 +209 -0
- package/src/agent-gateway-config-controller.js +147 -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 +280 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-provider-config-controller.js +150 -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 +347 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +321 -0
- package/src/agent-workspace-controller.js +447 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +541 -0
- package/src/argocd-gitops.js +43 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +307 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +50 -0
- package/src/controller-ui.js +551 -0
- package/src/data-plane.js +178 -0
- package/src/event-bus.js +61 -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 +161 -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 +95 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/hooks-events.js +63 -0
- package/src/http-server.js +377 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +55 -0
- package/src/kubernetes-controller-async.js +511 -0
- package/src/kubernetes-controller.js +878 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +221 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -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 +315 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-memory-controller.test.js +308 -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 +204 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +228 -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 +221 -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 +211 -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/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/deployment.test.js +396 -0
- package/tests/e2e/lifecycle.test.js +117 -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 +340 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +215 -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/integration/full-flow.test.js +266 -0
- package/tests/krate.test.js +727 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +247 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
package/src/runtime.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { ControlPlane } from './control-plane.js';
|
|
2
|
+
import { GiteaGitService, GiteaRepositoryStore } from './data-plane.js';
|
|
3
|
+
import { WebhookBus } from './hooks-events.js';
|
|
4
|
+
import { createAdmissionPolicy, mapOidcIdentity } from './identity-policy.js';
|
|
5
|
+
import { createInviteResource, createTeamResource, createAuthProviderResources, mapLoginProfileToKrateIdentity } from './auth.js';
|
|
6
|
+
import { createResource, clone } from './resource-model.js';
|
|
7
|
+
import { RunnerScheduler } from './runners-ci.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_ORG = 'default';
|
|
10
|
+
const DEFAULT_NAMESPACE = 'krate-org-default';
|
|
11
|
+
|
|
12
|
+
export function createDefaultKrateUsers() {
|
|
13
|
+
return {
|
|
14
|
+
platformEngineer: mapOidcIdentity({ subject: 'user:platform', email: 'platform@example.com', groups: ['krate:platform-engineers'] }),
|
|
15
|
+
developer: mapOidcIdentity({ subject: 'user:dev', email: 'dev@example.com', groups: ['krate:developers'] }),
|
|
16
|
+
repoAdmin: mapOidcIdentity({ subject: 'user:admin', email: 'admin@example.com', groups: ['krate:repo-admins'] })
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class KrateRuntime {
|
|
21
|
+
constructor({ organizationRef = DEFAULT_ORG, namespace = DEFAULT_NAMESPACE, users = createDefaultKrateUsers(), controlPlane, git, runners, webhooks } = {}) {
|
|
22
|
+
this.organizationRef = organizationRef;
|
|
23
|
+
this.namespace = namespace;
|
|
24
|
+
this.users = users;
|
|
25
|
+
this.controlPlane = controlPlane || new ControlPlane();
|
|
26
|
+
this.controlPlane.addAdmissionPolicy(createAdmissionPolicy({
|
|
27
|
+
name: 'pr-title-required',
|
|
28
|
+
mode: 'enforce',
|
|
29
|
+
match: ({ resource }) => resource.kind === 'PullRequest',
|
|
30
|
+
validate: ({ resource }) => Boolean(resource.spec.title && resource.spec.title.length >= 8),
|
|
31
|
+
message: 'PullRequest spec.title must be descriptive'
|
|
32
|
+
}));
|
|
33
|
+
this.git = git || new GiteaGitService({ controlPlane: this.controlPlane, stores: [new GiteaRepositoryStore({ name: 'gitea-primary', receivePackReady: true })] });
|
|
34
|
+
this.runners = runners || new RunnerScheduler({ controlPlane: this.controlPlane });
|
|
35
|
+
this.webhooks = webhooks || new WebhookBus({ controlPlane: this.controlPlane });
|
|
36
|
+
this.seeded = false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static fromSnapshot(snapshot, options = {}) {
|
|
40
|
+
const runtime = new KrateRuntime(options);
|
|
41
|
+
runtime.importSnapshot(snapshot);
|
|
42
|
+
return runtime;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
bootstrap({ repository = 'krate-demo', webhookUrl = 'https://hooks.example.test/krate' } = {}) {
|
|
46
|
+
if (this.seeded) return this.snapshot();
|
|
47
|
+
const { platformEngineer, repoAdmin } = this.users;
|
|
48
|
+
this.git.createRepository({ name: repository, namespace: this.namespace, organizationRef: this.organizationRef }, repoAdmin);
|
|
49
|
+
this.controlPlane.create(createResource('BranchProtection', { name: 'main-protection', namespace: this.namespace }, { organizationRef: this.organizationRef, refs: ['refs/heads/main'], requirePullRequest: true, requiredChecks: ['test'], requiredApprovals: 1 }), repoAdmin);
|
|
50
|
+
this.controlPlane.create(createResource('RefPolicy', { name: 'deny-internal-refs', namespace: this.namespace }, { organizationRef: this.organizationRef, deny: ['refs/internal/'] }), repoAdmin);
|
|
51
|
+
this.runners.createRunnerPool({ name: 'trusted-linux', namespace: this.namespace, organizationRef: this.organizationRef, warmReplicas: 1, maxReplicas: 4 }, platformEngineer);
|
|
52
|
+
this.webhooks.subscribe({ name: 'chatops', namespace: this.namespace, organizationRef: this.organizationRef, url: webhookUrl, events: ['pullrequest.created', 'pullrequest.merged'] }, repoAdmin);
|
|
53
|
+
this.controlPlane.create(createTeamResource({ name: 'maintainers', organizationRef: this.organizationRef, displayName: 'Maintainers', members: ['admin'], maintainers: ['admin'], repositoryGrants: [{ repository, permission: 'admin' }], namespace: this.namespace }), platformEngineer);
|
|
54
|
+
const identity = mapLoginProfileToKrateIdentity({ provider: 'sso', subject: 'user:admin', email: 'admin@example.com', displayName: 'Admin', username: 'admin', teams: ['maintainers'], admin: true, namespace: this.namespace, organizationRef: this.organizationRef });
|
|
55
|
+
this.controlPlane.create(identity.user, platformEngineer);
|
|
56
|
+
this.controlPlane.create(identity.mapping, platformEngineer);
|
|
57
|
+
this.controlPlane.create(createInviteResource({ email: 'new-user@example.com', role: 'member', teams: ['maintainers'], invitedBy: 'admin', namespace: this.namespace, organizationRef: this.organizationRef }), repoAdmin);
|
|
58
|
+
for (const provider of createAuthProviderResources(undefined, this.namespace, this.organizationRef)) this.controlPlane.create(provider, platformEngineer);
|
|
59
|
+
this.seeded = true;
|
|
60
|
+
return this.snapshot();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
exportSnapshot() {
|
|
64
|
+
return {
|
|
65
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
66
|
+
kind: 'KrateRuntimeSnapshot',
|
|
67
|
+
namespace: this.namespace,
|
|
68
|
+
organizationRef: this.organizationRef,
|
|
69
|
+
seeded: this.seeded,
|
|
70
|
+
controlPlane: this.controlPlane.exportSnapshot(),
|
|
71
|
+
git: this.git.snapshot?.() || null
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
importSnapshot(snapshot) {
|
|
76
|
+
const controlPlaneSnapshot = snapshot?.controlPlane || snapshot;
|
|
77
|
+
this.controlPlane.importSnapshot(controlPlaneSnapshot);
|
|
78
|
+
if (snapshot?.git) this.git.importSnapshot(snapshot.git);
|
|
79
|
+
this.seeded = Boolean(snapshot?.seeded || this.controlPlane.list('Repository', { namespace: this.namespace }).items.length);
|
|
80
|
+
return this.snapshot();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
createRepository({ name, visibility = 'private' }, user = this.users.repoAdmin) {
|
|
84
|
+
return this.git.createRepository({ name, namespace: this.namespace, organizationRef: this.organizationRef, visibility }, user);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
createPullRequest({ repository, sourceRef = 'refs/heads/feature', targetRef = 'refs/heads/main', title, actor = this.users.developer, fork = false }) {
|
|
88
|
+
this.#requireRepository(repository);
|
|
89
|
+
const index = this.controlPlane.list('PullRequest', { namespace: this.namespace }).items.length + 1;
|
|
90
|
+
const name = `pr-${index}`;
|
|
91
|
+
const pullRequest = this.controlPlane.create(createResource('PullRequest', { name, namespace: this.namespace, labels: { repository } }, {
|
|
92
|
+
organizationRef: this.organizationRef,
|
|
93
|
+
repository,
|
|
94
|
+
sourceRef,
|
|
95
|
+
targetRef,
|
|
96
|
+
title,
|
|
97
|
+
author: actor.name,
|
|
98
|
+
checks: [],
|
|
99
|
+
requiredApprovals: this.#requiredApprovals(targetRef)
|
|
100
|
+
}, { phase: 'Open', approvals: 0, mergeable: false }), actor);
|
|
101
|
+
const pipelineRun = this.runners.startPipeline({ name: `pipeline-${name}`, namespace: this.namespace, organizationRef: this.organizationRef, repository, ref: `refs/pull/${index}/head`, actor, fork, steps: ['checkout', 'test'] }, actor);
|
|
102
|
+
this.webhooks.deliver({ subscriptionName: 'chatops', namespace: this.namespace, organizationRef: this.organizationRef, eventType: 'pullrequest.created', payload: { pullRequest: name, repository, title } }, this.users.repoAdmin);
|
|
103
|
+
return { pullRequest, pipeline: pipelineRun.pipeline, jobs: pipelineRun.jobs };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
completePipeline({ pipeline, phase = 'Succeeded', failedStep = null }, user = this.users.developer) {
|
|
107
|
+
const existing = this.controlPlane.get('Pipeline', this.namespace, pipeline);
|
|
108
|
+
if (!existing) throw new Error(`Pipeline ${pipeline} not found`);
|
|
109
|
+
const jobs = this.controlPlane.list('Job', { namespace: this.namespace, labels: { pipeline } }).items.map((job) => {
|
|
110
|
+
const jobPhase = failedStep && job.spec.step === failedStep ? 'Failed' : phase;
|
|
111
|
+
return this.controlPlane.patchStatus('Job', this.namespace, job.metadata.name, { phase: jobPhase, finishedAt: new Date().toISOString() }, user);
|
|
112
|
+
});
|
|
113
|
+
const pipelinePhase = jobs.some((job) => job.status.phase === 'Failed') ? 'Failed' : phase;
|
|
114
|
+
const updatedPipeline = this.controlPlane.patchStatus('Pipeline', this.namespace, pipeline, { phase: pipelinePhase, currentStep: null, finishedAt: new Date().toISOString() }, user);
|
|
115
|
+
this.#refreshPullRequestMergeability(existing.spec.repository);
|
|
116
|
+
return { pipeline: updatedPipeline, jobs };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
addReview({ pullRequest, decision = 'approved', body = 'Looks good', reviewer = this.users.repoAdmin }) {
|
|
120
|
+
const pr = this.#requirePullRequest(pullRequest);
|
|
121
|
+
const reviewCount = this.controlPlane.list('Review', { namespace: this.namespace, labels: { pullRequest } }).items.length + 1;
|
|
122
|
+
const review = this.controlPlane.create(createResource('Review', { name: `${pullRequest}-review-${reviewCount}`, namespace: this.namespace, labels: { pullRequest, decision } }, {
|
|
123
|
+
organizationRef: this.organizationRef,
|
|
124
|
+
pullRequest,
|
|
125
|
+
repository: pr.spec.repository,
|
|
126
|
+
reviewer: reviewer.name,
|
|
127
|
+
decision,
|
|
128
|
+
body
|
|
129
|
+
}, { phase: decision === 'approved' ? 'Approved' : 'ChangesRequested' }), reviewer);
|
|
130
|
+
this.#refreshPullRequestMergeability(pr.spec.repository);
|
|
131
|
+
return review;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
mergePullRequest({ pullRequest, actor = this.users.repoAdmin }) {
|
|
135
|
+
const pr = this.#requirePullRequest(pullRequest);
|
|
136
|
+
const refreshed = this.#refreshPullRequestMergeability(pr.spec.repository).find((candidate) => candidate.metadata.name === pullRequest) || pr;
|
|
137
|
+
if (!refreshed.status.mergeable) throw new Error(`PullRequest ${pullRequest} is not mergeable`);
|
|
138
|
+
const gitEvent = this.git.receivePack({ repository: refreshed.spec.repository, namespace: this.namespace, ref: refreshed.spec.targetRef, newRev: this.#syntheticRevision(pullRequest), actor });
|
|
139
|
+
const merged = this.controlPlane.patchStatus('PullRequest', this.namespace, pullRequest, { phase: 'Merged', mergeable: false, mergedAt: new Date().toISOString(), mergeCommit: gitEvent.newRev }, actor);
|
|
140
|
+
const delivery = this.webhooks.deliver({ subscriptionName: 'chatops', namespace: this.namespace, organizationRef: this.organizationRef, eventType: 'pullrequest.merged', payload: { pullRequest, repository: refreshed.spec.repository, mergeCommit: gitEvent.newRev } }, this.users.repoAdmin);
|
|
141
|
+
return { pullRequest: merged, gitEvent, delivery };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
snapshot() {
|
|
145
|
+
const kinds = ['Organization', 'User', 'Team', 'Invite', 'IdentityMapping', 'AuthProvider', 'Repository', 'SSHKey', 'RepositoryPermission', 'BranchProtection', 'RefPolicy', 'RunnerPool', 'PullRequest', 'Issue', 'Review', 'Pipeline', 'Job', 'WebhookSubscription', 'WebhookDelivery'];
|
|
146
|
+
const resources = Object.fromEntries(kinds.map((kind) => [kind, this.controlPlane.list(kind, { namespace: this.namespace }).items]));
|
|
147
|
+
return {
|
|
148
|
+
namespace: this.namespace,
|
|
149
|
+
export: this.exportSnapshot(),
|
|
150
|
+
storage: this.controlPlane.storageReport(),
|
|
151
|
+
resources,
|
|
152
|
+
events: clone(this.controlPlane.events),
|
|
153
|
+
auditLog: clone(this.controlPlane.auditLog)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#requireRepository(repository) {
|
|
158
|
+
const existing = this.controlPlane.get('Repository', this.namespace, repository);
|
|
159
|
+
if (!existing) throw new Error(`Repository ${repository} not found`);
|
|
160
|
+
return existing;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#requirePullRequest(pullRequest) {
|
|
164
|
+
const existing = this.controlPlane.get('PullRequest', this.namespace, pullRequest);
|
|
165
|
+
if (!existing) throw new Error(`PullRequest ${pullRequest} not found`);
|
|
166
|
+
return existing;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#requiredApprovals(targetRef) {
|
|
170
|
+
const branchProtection = this.controlPlane.list('BranchProtection', { namespace: this.namespace }).items.find((policy) => (policy.spec.refs || []).includes(targetRef));
|
|
171
|
+
return branchProtection?.spec.requiredApprovals || 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#refreshPullRequestMergeability(repository) {
|
|
175
|
+
const pullRequests = this.controlPlane.list('PullRequest', { namespace: this.namespace, labels: { repository } }).items;
|
|
176
|
+
return pullRequests.map((pr) => {
|
|
177
|
+
if (pr.status.phase !== 'Open') return pr;
|
|
178
|
+
const reviews = this.controlPlane.list('Review', { namespace: this.namespace, labels: { pullRequest: pr.metadata.name } }).items;
|
|
179
|
+
const approvals = reviews.filter((review) => review.spec.decision === 'approved').length;
|
|
180
|
+
const pipelines = this.controlPlane.list('Pipeline', { namespace: this.namespace, labels: { repository } }).items.filter((pipeline) => pipeline.metadata.name === `pipeline-${pr.metadata.name}`);
|
|
181
|
+
const checksPassed = pipelines.length > 0 && pipelines.every((pipeline) => pipeline.status.phase === 'Succeeded');
|
|
182
|
+
return this.controlPlane.patchStatus('PullRequest', this.namespace, pr.metadata.name, { approvals, checksPassed, mergeable: approvals >= (pr.spec.requiredApprovals || 0) && checksPassed }, this.users.repoAdmin);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#syntheticRevision(seed) {
|
|
187
|
+
const source = Buffer.from(`${seed}:${Date.now()}`).toString('hex');
|
|
188
|
+
return source.padEnd(40, '0').slice(0, 40);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function createKrateRuntime(options = {}) {
|
|
193
|
+
const runtime = new KrateRuntime(options);
|
|
194
|
+
if (options.bootstrap !== false) runtime.bootstrap(options.bootstrapOptions);
|
|
195
|
+
return runtime;
|
|
196
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot cache with:
|
|
3
|
+
* - Per-org storage (multiple orgs cached simultaneously)
|
|
4
|
+
* - stale-while-revalidate: return stale data immediately, refresh in background
|
|
5
|
+
* - Configurable TTL via KRATE_SNAPSHOT_CACHE_TTL_MS
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const CACHE_TTL_MS = Number(process.env.KRATE_SNAPSHOT_CACHE_TTL_MS || 30_000);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Per-org cache map. Key = org string (or '' for no-org).
|
|
12
|
+
* Value = { data, timestamp, revalidating: boolean }
|
|
13
|
+
*/
|
|
14
|
+
const orgCacheMap = new Map();
|
|
15
|
+
|
|
16
|
+
// Legacy single-org cache kept for backward compatibility with controller-client.js
|
|
17
|
+
let snapshotCache = { data: null, timestamp: 0, org: null };
|
|
18
|
+
|
|
19
|
+
export function getSnapshotCache() {
|
|
20
|
+
return snapshotCache;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function setSnapshotCache(data, org) {
|
|
24
|
+
snapshotCache = { data, timestamp: Date.now(), org };
|
|
25
|
+
// Also update per-org map
|
|
26
|
+
const key = org ?? '';
|
|
27
|
+
const entry = orgCacheMap.get(key) || {};
|
|
28
|
+
orgCacheMap.set(key, { ...entry, data, timestamp: Date.now(), revalidating: false });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function clearSnapshotCache() {
|
|
32
|
+
snapshotCache = { data: null, timestamp: 0, org: null };
|
|
33
|
+
orgCacheMap.clear();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Per-org cache API
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the cached snapshot for a specific org.
|
|
42
|
+
* Returns null when nothing is cached or when the TTL has expired.
|
|
43
|
+
*
|
|
44
|
+
* @param {string|null} org
|
|
45
|
+
* @returns {{ data: object, timestamp: number, revalidating: boolean } | null}
|
|
46
|
+
*/
|
|
47
|
+
export function getOrgCache(org) {
|
|
48
|
+
const key = org ?? '';
|
|
49
|
+
const entry = orgCacheMap.get(key);
|
|
50
|
+
if (!entry || !entry.data) return null;
|
|
51
|
+
return entry;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Store a snapshot for a specific org.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} data
|
|
58
|
+
* @param {string|null} org
|
|
59
|
+
*/
|
|
60
|
+
export function setOrgCache(data, org) {
|
|
61
|
+
const key = org ?? '';
|
|
62
|
+
orgCacheMap.set(key, { data, timestamp: Date.now(), revalidating: false });
|
|
63
|
+
// Keep legacy cache in sync for the most recently written org
|
|
64
|
+
snapshotCache = { data, timestamp: Date.now(), org };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Clear the cache entry for a specific org only.
|
|
69
|
+
*
|
|
70
|
+
* @param {string|null} org
|
|
71
|
+
*/
|
|
72
|
+
export function clearOrgCache(org) {
|
|
73
|
+
orgCacheMap.delete(org ?? '');
|
|
74
|
+
if (snapshotCache.org === org) {
|
|
75
|
+
snapshotCache = { data: null, timestamp: 0, org: null };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Return all orgs currently in the cache (for introspection / debugging).
|
|
81
|
+
*
|
|
82
|
+
* @returns {string[]}
|
|
83
|
+
*/
|
|
84
|
+
export function cachedOrgs() {
|
|
85
|
+
return [...orgCacheMap.keys()];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// stale-while-revalidate helpers
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check whether the cache entry for the given org is fresh.
|
|
94
|
+
*
|
|
95
|
+
* @param {string|null} org
|
|
96
|
+
* @param {number} [ttlMs] - defaults to CACHE_TTL_MS
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
export function isCacheFresh(org, ttlMs = CACHE_TTL_MS) {
|
|
100
|
+
const entry = getOrgCache(org);
|
|
101
|
+
if (!entry) return false;
|
|
102
|
+
return (Date.now() - entry.timestamp) < ttlMs;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* stale-while-revalidate: return stale data immediately if available, then
|
|
107
|
+
* trigger revalidateFn() in the background to refresh the cache entry.
|
|
108
|
+
*
|
|
109
|
+
* @param {string|null} org - Organization key for the cache
|
|
110
|
+
* @param {Function} revalidateFn - Async function that returns fresh data
|
|
111
|
+
* @param {object} [swrOptions]
|
|
112
|
+
* @param {number} [swrOptions.ttlMs] - Fresh TTL in ms (default: CACHE_TTL_MS)
|
|
113
|
+
* @param {number} [swrOptions.staleMs] - Max staleness before we block on revalidate (default: 5 × ttlMs)
|
|
114
|
+
* @returns {Promise<object>} Either the stale cached value or the freshly fetched one
|
|
115
|
+
*/
|
|
116
|
+
export async function staleWhileRevalidate(org, revalidateFn, swrOptions = {}) {
|
|
117
|
+
const ttlMs = swrOptions.ttlMs ?? CACHE_TTL_MS;
|
|
118
|
+
const staleMs = swrOptions.staleMs ?? ttlMs * 5;
|
|
119
|
+
const key = org ?? '';
|
|
120
|
+
const entry = orgCacheMap.get(key);
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
|
|
123
|
+
const isFresh = entry && entry.data && (now - entry.timestamp) < ttlMs;
|
|
124
|
+
const isStale = entry && entry.data && (now - entry.timestamp) < staleMs;
|
|
125
|
+
|
|
126
|
+
if (isFresh) {
|
|
127
|
+
// Cache is fresh: return immediately
|
|
128
|
+
return entry.data;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (isStale && !entry.revalidating) {
|
|
132
|
+
// Stale but still usable: return immediately and revalidate in background
|
|
133
|
+
orgCacheMap.set(key, { ...entry, revalidating: true });
|
|
134
|
+
Promise.resolve().then(async () => {
|
|
135
|
+
try {
|
|
136
|
+
const fresh = await revalidateFn();
|
|
137
|
+
setOrgCache(fresh, org);
|
|
138
|
+
} catch {
|
|
139
|
+
// On background refresh error, clear the revalidating flag so a future
|
|
140
|
+
// request can try again
|
|
141
|
+
const current = orgCacheMap.get(key);
|
|
142
|
+
if (current) orgCacheMap.set(key, { ...current, revalidating: false });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return entry.data;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isStale && entry.revalidating) {
|
|
149
|
+
// Another caller is already refreshing: return stale data now
|
|
150
|
+
return entry.data;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// No usable cache: block on the revalidation
|
|
154
|
+
const fresh = await revalidateFn();
|
|
155
|
+
setOrgCache(fresh, org);
|
|
156
|
+
return fresh;
|
|
157
|
+
}
|
package/src/web-ui.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createResource } from './resource-model.js';
|
|
2
|
+
import { toResourceYaml } from './identity-policy.js';
|
|
3
|
+
|
|
4
|
+
export function createPullRequestReviewModel({ pullRequest, changedFiles = [], pipelineRuns = [] }) {
|
|
5
|
+
return { layout: 'three-pane-review', panes: ['file-tree', 'diff-and-comments', 'conversation-ci'], keyboardShortcuts: ['j/k file navigation', 'n/p comment navigation', 'a add suggestion', 'm merge'], pullRequest, changedFiles, pipelineRuns, yaml: toResourceYaml(pullRequest) };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createFailingRunModel({ pipeline, jobs }) {
|
|
9
|
+
const failedJobs = jobs.filter((job) => job.status.phase === 'Failed');
|
|
10
|
+
return { layout: 'live-run-debugger', stream: 'sse', pipeline, failedJobs, actions: ['copy failure', 'find similar runs', 'rerun from step'], similarRunSelector: failedJobs.map((job) => job.metadata.labels).filter(Boolean) };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createRunnerPoolEditor(pool) {
|
|
14
|
+
return { layout: 'split-form-yaml', fields: ['image', 'resources', 'nodeSelector', 'warmReplicas', 'maxReplicas', 'trustTier', 'cache'], resource: pool, yaml: toResourceYaml(pool), saveModes: ['apply', 'copy kubectl', 'open platform-config PR'] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createWebhookInspector({ subscription, deliveries }) {
|
|
18
|
+
return { layout: 'webhook-inspector', subscription, deliveries, columns: ['phase', 'latency', 'attempts', 'response', 'signature'], actions: ['send test delivery', 'inspect headers/body/response', 'replay'] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createPolicyRolloutModel(policy) {
|
|
22
|
+
return { layout: 'policy-authoring', modes: ['template', 'CEL/raw'], rollout: ['preview', 'audit', 'enforce'], policy, yaml: toResourceYaml(policy) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createTriageView({ name, namespace = 'krate-org-default', organizationRef = 'default', selector }) {
|
|
26
|
+
return createResource('View', { name, namespace, labels: { purpose: 'triage' } }, { organizationRef, selector, columns: ['kind', 'repository', 'priority', 'assignee', 'status'], shareable: true }, { saved: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createDashboard({ repositories, pullRequests, pipelines, runnerPools, webhookDeliveries }) {
|
|
30
|
+
return {
|
|
31
|
+
product: 'Krate',
|
|
32
|
+
principles: ['Kubernetes is the backend', 'CRDs are contracts', 'GitOps transparency'],
|
|
33
|
+
repositories,
|
|
34
|
+
pullRequests,
|
|
35
|
+
pipelines,
|
|
36
|
+
runnerPools,
|
|
37
|
+
webhookDeliveries,
|
|
38
|
+
excellentFlows: ['Open and review a PR', 'Debug a failing run', 'Configure a runner pool', 'Add a webhook and verify it works', 'Write a PR policy with audit-to-enforce rollout', 'Cross-repo triage with saved filters']
|
|
39
|
+
};
|
|
40
|
+
}
|