@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// GitHub Git Forge implementation — Slice 3.3a
|
|
2
|
+
// Implements the gitForge interface contract:
|
|
3
|
+
// listRepositories, getPullRequest, createPullRequest, mergePullRequest,
|
|
4
|
+
// listRefs, syncDeployKeys, syncBranchProtection
|
|
5
|
+
//
|
|
6
|
+
// All HTTP calls are injected via fetchImpl for full testability.
|
|
7
|
+
|
|
8
|
+
const GITHUB_API = 'https://api.github.com';
|
|
9
|
+
const VALID_MERGE_METHODS = new Set(['merge', 'squash', 'rebase']);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {{ name: string, fullName: string, private: boolean, defaultBranch: string, cloneUrl: string }} NormalizedRepo
|
|
13
|
+
* @typedef {{ number: number, title: string, state: string, head: object, base: object, body: string, merged: boolean, htmlUrl: string }} NormalizedPR
|
|
14
|
+
* @typedef {{ name: string, sha: string, protected: boolean }} NormalizedBranch
|
|
15
|
+
* @typedef {{ name: string, sha: string }} NormalizedTag
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* GitHub implementation of the git forge interface.
|
|
20
|
+
*
|
|
21
|
+
* @param {{ owner: string, installationToken: string, fetchImpl?: Function }} opts
|
|
22
|
+
*/
|
|
23
|
+
export class GitHubGitForge {
|
|
24
|
+
constructor({ owner, installationToken, fetchImpl = globalThis.fetch } = {}) {
|
|
25
|
+
if (!owner) throw new Error('GitHubGitForge: owner (org or user) is required');
|
|
26
|
+
if (!installationToken) throw new Error('GitHubGitForge: installationToken is required');
|
|
27
|
+
if (!fetchImpl) throw new Error('GitHubGitForge: a fetch implementation is required');
|
|
28
|
+
|
|
29
|
+
this.role = 'github-git-forge';
|
|
30
|
+
this._owner = owner;
|
|
31
|
+
this._token = installationToken;
|
|
32
|
+
this._fetch = fetchImpl;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Internal helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
_headers() {
|
|
40
|
+
return {
|
|
41
|
+
Accept: 'application/vnd.github+json',
|
|
42
|
+
Authorization: `Bearer ${this._token}`,
|
|
43
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
44
|
+
'Content-Type': 'application/json'
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async _request(method, path, body) {
|
|
49
|
+
const url = `${GITHUB_API}${path}`;
|
|
50
|
+
const options = {
|
|
51
|
+
method,
|
|
52
|
+
headers: this._headers(),
|
|
53
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {})
|
|
54
|
+
};
|
|
55
|
+
const response = await this._fetch(url, options);
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`GitHub ${method} ${path} failed with status ${response.status}`);
|
|
58
|
+
}
|
|
59
|
+
if (response.status === 204) return null;
|
|
60
|
+
return response.json();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_repoPath(repo, suffix = '') {
|
|
64
|
+
return `/repos/${encodeURIComponent(this._owner)}/${encodeURIComponent(repo)}${suffix}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Interface methods
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* List all repositories for the installation.
|
|
73
|
+
* @returns {Promise<NormalizedRepo[]>}
|
|
74
|
+
*/
|
|
75
|
+
async listRepositories() {
|
|
76
|
+
const data = await this._request('GET', `/installation/repositories`);
|
|
77
|
+
const repos = data?.repositories ?? data ?? [];
|
|
78
|
+
return repos.map(r => ({
|
|
79
|
+
id: r.id,
|
|
80
|
+
name: r.name,
|
|
81
|
+
fullName: r.full_name,
|
|
82
|
+
private: r.private,
|
|
83
|
+
defaultBranch: r.default_branch,
|
|
84
|
+
cloneUrl: r.clone_url,
|
|
85
|
+
sshUrl: r.ssh_url
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get a single pull request by number.
|
|
91
|
+
* @param {{ repo: string, pullNumber: number }} opts
|
|
92
|
+
* @returns {Promise<NormalizedPR>}
|
|
93
|
+
*/
|
|
94
|
+
async getPullRequest({ repo, pullNumber }) {
|
|
95
|
+
const data = await this._request('GET', this._repoPath(repo, `/pulls/${pullNumber}`));
|
|
96
|
+
return this._normalizePR(data);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Create a pull request.
|
|
101
|
+
* @param {{ repo: string, title: string, head: string, base: string, body?: string }} opts
|
|
102
|
+
* @returns {Promise<NormalizedPR>}
|
|
103
|
+
*/
|
|
104
|
+
async createPullRequest({ repo, title, head, base, body = '' }) {
|
|
105
|
+
const data = await this._request('POST', this._repoPath(repo, '/pulls'), {
|
|
106
|
+
title,
|
|
107
|
+
head,
|
|
108
|
+
base,
|
|
109
|
+
body
|
|
110
|
+
});
|
|
111
|
+
return this._normalizePR(data);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Merge a pull request.
|
|
116
|
+
* @param {{ repo: string, pullNumber: number, mergeMethod?: 'merge'|'squash'|'rebase', commitTitle?: string }} opts
|
|
117
|
+
* @returns {Promise<{ merged: boolean, sha: string, message: string }>}
|
|
118
|
+
*/
|
|
119
|
+
async mergePullRequest({ repo, pullNumber, mergeMethod = 'merge', commitTitle } = {}) {
|
|
120
|
+
if (!VALID_MERGE_METHODS.has(mergeMethod)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`GitHubGitForge.mergePullRequest: invalid mergeMethod "${mergeMethod}". ` +
|
|
123
|
+
`Valid values are: ${[...VALID_MERGE_METHODS].join(', ')}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const payload = { merge_method: mergeMethod };
|
|
127
|
+
if (commitTitle) payload.commit_title = commitTitle;
|
|
128
|
+
const data = await this._request('PUT', this._repoPath(repo, `/pulls/${pullNumber}/merge`), payload);
|
|
129
|
+
return {
|
|
130
|
+
merged: data?.merged ?? true,
|
|
131
|
+
sha: data?.sha ?? '',
|
|
132
|
+
message: data?.message ?? ''
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* List branches and tags for a repository.
|
|
138
|
+
* @param {{ repo: string }} opts
|
|
139
|
+
* @returns {Promise<{ branches: NormalizedBranch[], tags: NormalizedTag[] }>}
|
|
140
|
+
*/
|
|
141
|
+
async listRefs({ repo }) {
|
|
142
|
+
const [branchData, tagData] = await Promise.all([
|
|
143
|
+
this._request('GET', this._repoPath(repo, '/branches')),
|
|
144
|
+
this._request('GET', this._repoPath(repo, '/tags'))
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
const branches = (branchData ?? []).map(b => ({
|
|
148
|
+
name: b.name,
|
|
149
|
+
sha: b.commit?.sha ?? b.sha,
|
|
150
|
+
protected: b.protected ?? false
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
const tags = (tagData ?? []).map(t => ({
|
|
154
|
+
name: t.name,
|
|
155
|
+
sha: t.commit?.sha ?? t.sha
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
return { branches, tags };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Sync deploy keys: add missing, remove extra.
|
|
163
|
+
* @param {{ repo: string, desiredKeys: Array<{ title: string, key: string, readOnly: boolean }> }} opts
|
|
164
|
+
* @returns {Promise<{ added: number, removed: number }>}
|
|
165
|
+
*/
|
|
166
|
+
async syncDeployKeys({ repo, desiredKeys = [] }) {
|
|
167
|
+
const current = await this._request('GET', this._repoPath(repo, '/keys'));
|
|
168
|
+
const currentList = current ?? [];
|
|
169
|
+
|
|
170
|
+
// Find keys to remove (title not in desired)
|
|
171
|
+
const desiredTitles = new Set(desiredKeys.map(k => k.title));
|
|
172
|
+
const toRemove = currentList.filter(k => !desiredTitles.has(k.title));
|
|
173
|
+
|
|
174
|
+
// Find keys to add (title not in current)
|
|
175
|
+
const currentTitles = new Set(currentList.map(k => k.title));
|
|
176
|
+
const toAdd = desiredKeys.filter(k => !currentTitles.has(k.title));
|
|
177
|
+
|
|
178
|
+
// Perform deletions
|
|
179
|
+
await Promise.all(toRemove.map(k =>
|
|
180
|
+
this._request('DELETE', this._repoPath(repo, `/keys/${k.id}`))
|
|
181
|
+
));
|
|
182
|
+
|
|
183
|
+
// Perform additions
|
|
184
|
+
await Promise.all(toAdd.map(k =>
|
|
185
|
+
this._request('POST', this._repoPath(repo, '/keys'), {
|
|
186
|
+
title: k.title,
|
|
187
|
+
key: k.key,
|
|
188
|
+
read_only: k.readOnly ?? true
|
|
189
|
+
})
|
|
190
|
+
));
|
|
191
|
+
|
|
192
|
+
return { added: toAdd.length, removed: toRemove.length };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Sync branch protection rules.
|
|
197
|
+
* @param {{ repo: string, branch: string, requiredReviews?: number, requiredStatusChecks?: string[], dismissStaleReviews?: boolean, enforceAdmins?: boolean }} opts
|
|
198
|
+
* @returns {Promise<object>}
|
|
199
|
+
*/
|
|
200
|
+
async syncBranchProtection({
|
|
201
|
+
repo,
|
|
202
|
+
branch = 'main',
|
|
203
|
+
requiredReviews = 1,
|
|
204
|
+
requiredStatusChecks = [],
|
|
205
|
+
dismissStaleReviews = false,
|
|
206
|
+
enforceAdmins = false
|
|
207
|
+
} = {}) {
|
|
208
|
+
const payload = {
|
|
209
|
+
required_status_checks: requiredStatusChecks.length > 0
|
|
210
|
+
? { strict: true, contexts: requiredStatusChecks }
|
|
211
|
+
: null,
|
|
212
|
+
enforce_admins: enforceAdmins,
|
|
213
|
+
required_pull_request_reviews: {
|
|
214
|
+
dismiss_stale_reviews: dismissStaleReviews,
|
|
215
|
+
require_code_owner_reviews: false,
|
|
216
|
+
required_approving_review_count: requiredReviews
|
|
217
|
+
},
|
|
218
|
+
restrictions: null
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return this._request('PUT', this._repoPath(repo, `/branches/${encodeURIComponent(branch)}/protection`), payload);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Internal normalizer
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
_normalizePR(data) {
|
|
229
|
+
return {
|
|
230
|
+
number: data.number,
|
|
231
|
+
title: data.title,
|
|
232
|
+
state: data.state,
|
|
233
|
+
head: { ref: data.head?.ref, sha: data.head?.sha },
|
|
234
|
+
base: { ref: data.base?.ref, sha: data.base?.sha },
|
|
235
|
+
body: data.body ?? '',
|
|
236
|
+
merged: data.merged ?? false,
|
|
237
|
+
htmlUrl: data.html_url ?? ''
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// GitHub External Provider — Slice 3.3a / 3.3b
|
|
2
|
+
// Entry point for the GitHub provider adapter.
|
|
3
|
+
// Re-exports auth helpers, git forge class, issue tracking, CI/CD, boundary objects, and provider factory.
|
|
4
|
+
|
|
5
|
+
export { createGitHubJwt, exchangeInstallationToken } from './auth.js';
|
|
6
|
+
|
|
7
|
+
import { GitHubGitForge } from './git-forge.js';
|
|
8
|
+
import { GitHubIssueTracking } from './issue-tracking.js';
|
|
9
|
+
import { GitHubCicd } from './cicd.js';
|
|
10
|
+
|
|
11
|
+
export { GitHubGitForge };
|
|
12
|
+
export { GitHubIssueTracking };
|
|
13
|
+
export { GitHubCicd };
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Boundary declarations
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export const GITHUB_GIT_FORGE_BOUNDARY = Object.freeze({
|
|
20
|
+
role: 'github-git-forge',
|
|
21
|
+
scope: 'GitHub App authentication (JWT signing, installation token exchange) and Git forge operations (repositories, PRs, refs, deploy keys, branch protection)',
|
|
22
|
+
owns: [
|
|
23
|
+
'GitHub App JWT creation',
|
|
24
|
+
'installation token exchange',
|
|
25
|
+
'repository listing',
|
|
26
|
+
'pull request lifecycle',
|
|
27
|
+
'ref enumeration',
|
|
28
|
+
'deploy key synchronisation',
|
|
29
|
+
'branch protection synchronisation'
|
|
30
|
+
],
|
|
31
|
+
delegatesTo: [],
|
|
32
|
+
mustNotOwn: [
|
|
33
|
+
'GitHub secret storage',
|
|
34
|
+
'Kubernetes resources',
|
|
35
|
+
'CI pipeline execution',
|
|
36
|
+
'webhook delivery'
|
|
37
|
+
]
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const GITHUB_ISSUE_TRACKING_BOUNDARY = Object.freeze({
|
|
41
|
+
role: 'github-issue-tracking',
|
|
42
|
+
scope: 'GitHub Issues API: listing, creating, updating, closing issues and managing issue comments',
|
|
43
|
+
owns: [
|
|
44
|
+
'issue listing',
|
|
45
|
+
'issue creation',
|
|
46
|
+
'issue updates',
|
|
47
|
+
'issue closing',
|
|
48
|
+
'comment listing',
|
|
49
|
+
'comment creation'
|
|
50
|
+
],
|
|
51
|
+
delegatesTo: [],
|
|
52
|
+
mustNotOwn: [
|
|
53
|
+
'GitHub secret storage',
|
|
54
|
+
'Kubernetes resources',
|
|
55
|
+
'CI pipeline execution',
|
|
56
|
+
'webhook delivery',
|
|
57
|
+
'pull requests',
|
|
58
|
+
'branch protection'
|
|
59
|
+
]
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const GITHUB_CICD_BOUNDARY = Object.freeze({
|
|
63
|
+
role: 'github-cicd',
|
|
64
|
+
scope: 'GitHub Actions API: workflow runs, jobs, rerun/cancel operations, and check runs',
|
|
65
|
+
owns: [
|
|
66
|
+
'workflow run listing',
|
|
67
|
+
'job listing',
|
|
68
|
+
'workflow rerun',
|
|
69
|
+
'workflow cancellation',
|
|
70
|
+
'check run creation',
|
|
71
|
+
'check run updates'
|
|
72
|
+
],
|
|
73
|
+
delegatesTo: [],
|
|
74
|
+
mustNotOwn: [
|
|
75
|
+
'GitHub secret storage',
|
|
76
|
+
'Kubernetes resources',
|
|
77
|
+
'webhook delivery',
|
|
78
|
+
'issue tracking',
|
|
79
|
+
'pull requests'
|
|
80
|
+
]
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Provider factory
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create a GitHub ExternalProviderAdapter.
|
|
89
|
+
*
|
|
90
|
+
* Returns a lightweight descriptor that carries GitHub App credentials and
|
|
91
|
+
* exposes factory methods for constructing GitHubGitForge, GitHubIssueTracking,
|
|
92
|
+
* and GitHubCicd instances bound to specific installation tokens.
|
|
93
|
+
*
|
|
94
|
+
* @param {{ appId: string, privateKey: string, installationId?: string, fetchImpl?: Function }} opts
|
|
95
|
+
* @returns {{ type: string, config: object, createForge: Function, createIssueTracker: Function, createCicd: Function }}
|
|
96
|
+
*/
|
|
97
|
+
export function createGitHubProvider({ appId, privateKey, installationId, fetchImpl } = {}) {
|
|
98
|
+
if (!appId) throw new Error('createGitHubProvider: appId is required');
|
|
99
|
+
if (!privateKey) throw new Error('createGitHubProvider: privateKey is required');
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
type: 'github',
|
|
103
|
+
config: Object.freeze({ appId, installationId }),
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Construct a GitHubGitForge for the given owner and installation token.
|
|
107
|
+
* @param {{ owner: string, installationToken: string, fetchImpl?: Function }} forgeOpts
|
|
108
|
+
* @returns {GitHubGitForge}
|
|
109
|
+
*/
|
|
110
|
+
createForge({ owner, installationToken, fetchImpl: forgeFetch } = {}) {
|
|
111
|
+
return new GitHubGitForge({
|
|
112
|
+
owner,
|
|
113
|
+
installationToken,
|
|
114
|
+
fetchImpl: forgeFetch ?? fetchImpl ?? globalThis.fetch
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Construct a GitHubIssueTracking for the given owner and installation token.
|
|
120
|
+
* @param {{ owner: string, installationToken: string, fetchImpl?: Function }} opts
|
|
121
|
+
* @returns {GitHubIssueTracking}
|
|
122
|
+
*/
|
|
123
|
+
createIssueTracker({ owner, installationToken, fetchImpl: trackerFetch } = {}) {
|
|
124
|
+
return new GitHubIssueTracking({
|
|
125
|
+
owner,
|
|
126
|
+
installationToken,
|
|
127
|
+
fetchImpl: trackerFetch ?? fetchImpl ?? globalThis.fetch
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Construct a GitHubCicd for the given owner and installation token.
|
|
133
|
+
* @param {{ owner: string, installationToken: string, fetchImpl?: Function }} opts
|
|
134
|
+
* @returns {GitHubCicd}
|
|
135
|
+
*/
|
|
136
|
+
createCicd({ owner, installationToken, fetchImpl: cicdFetch } = {}) {
|
|
137
|
+
return new GitHubCicd({
|
|
138
|
+
owner,
|
|
139
|
+
installationToken,
|
|
140
|
+
fetchImpl: cicdFetch ?? fetchImpl ?? globalThis.fetch
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|