@agilno-tech/rivet 0.1.0-alpha.0
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/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +59 -0
- package/bin/cli.js +42 -0
- package/dist/agents/backend-django-agent/SKILL.md +35 -0
- package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
- package/dist/agents/boss-agent/SKILL.md +28 -0
- package/dist/agents/bounded-worker/SKILL.md +28 -0
- package/dist/agents/data-performance-agent/SKILL.md +32 -0
- package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
- package/dist/agents/devops-agent/SKILL.md +34 -0
- package/dist/agents/engineering-manager/SKILL.md +28 -0
- package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
- package/dist/agents/frontend-web-agent/SKILL.md +31 -0
- package/dist/agents/incident-response-agent/SKILL.md +42 -0
- package/dist/agents/jest-agent/SKILL.md +77 -0
- package/dist/agents/mobile-agent/SKILL.md +32 -0
- package/dist/agents/playwright-agent/SKILL.md +54 -0
- package/dist/agents/product-design-manager/SKILL.md +27 -0
- package/dist/agents/qa-agent/SKILL.md +29 -0
- package/dist/agents/quality-manager/SKILL.md +28 -0
- package/dist/agents/vitest-agent/SKILL.md +77 -0
- package/dist/governance/pre-push-rules.md +75 -0
- package/dist/governance/prompt-hygiene.md +22 -0
- package/dist/governance/review-checklist.md +24 -0
- package/dist/governance/safety-and-data.md +27 -0
- package/dist/governance/usage-rules.md +21 -0
- package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
- package/dist/mandatory/agentic-goal/SKILL.md +42 -0
- package/dist/mandatory/agentic-status/SKILL.md +74 -0
- package/dist/mandatory/apply-design/SKILL.md +228 -0
- package/dist/mandatory/check-ac/SKILL.md +124 -0
- package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
- package/dist/mandatory/design/SKILL.md +312 -0
- package/dist/mandatory/feature-workflow/SKILL.md +87 -0
- package/dist/mandatory/hotfix/SKILL.md +195 -0
- package/dist/mandatory/pre-push/SKILL.md +272 -0
- package/dist/mandatory/project-context/SKILL.md +180 -0
- package/dist/mandatory/release-docs/SKILL.md +191 -0
- package/dist/mandatory/review-pr/SKILL.md +323 -0
- package/dist/mandatory/security-review/SKILL.md +157 -0
- package/dist/skills/api-contract/SKILL.md +77 -0
- package/dist/skills/backend-django/SKILL.md +27 -0
- package/dist/skills/backend-nestjs/SKILL.md +65 -0
- package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
- package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
- package/dist/skills/database-migration/SKILL.md +97 -0
- package/dist/skills/debugging/SKILL.md +32 -0
- package/dist/skills/devops-infra/SKILL.md +26 -0
- package/dist/skills/documentation/SKILL.md +24 -0
- package/dist/skills/frontend-nextjs/SKILL.md +27 -0
- package/dist/skills/incident-postmortem/SKILL.md +38 -0
- package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
- package/dist/skills/mobile-react-native/SKILL.md +37 -0
- package/dist/skills/postgres-analytics/SKILL.md +29 -0
- package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
- package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
- package/dist/skills/refactoring/SKILL.md +32 -0
- package/dist/skills/sprint-planning/SKILL.md +32 -0
- package/dist/skills/testing-quality/SKILL.md +62 -0
- package/dist/v2/protocols/agent-orchestration.md +49 -0
- package/dist/v2/protocols/delivery.md +45 -0
- package/dist/v2/protocols/design-authority.md +29 -0
- package/dist/v2/protocols/goal-graph.md +44 -0
- package/dist/v2/protocols/pattern-first-development.md +29 -0
- package/dist/v2/protocols/qa-evidence.md +29 -0
- package/dist/v2/protocols/security.md +29 -0
- package/dist/v2/schemas/event.schema.json +86 -0
- package/dist/v2/schemas/evidence.schema.json +94 -0
- package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
- package/dist/v2/schemas/feature-plan.schema.json +95 -0
- package/dist/v2/schemas/goal-graph.schema.json +61 -0
- package/dist/v2/schemas/integration.schema.json +119 -0
- package/dist/v2/schemas/orchestration.schema.json +69 -0
- package/dist/v2/schemas/project.schema.json +209 -0
- package/dist/v2/schemas/providers.schema.json +142 -0
- package/dist/v2/schemas/quality.schema.json +50 -0
- package/dist/v2/schemas/work-action.schema.json +44 -0
- package/dist/v2/schemas/work-request.schema.json +253 -0
- package/dist/v2/templates/evidence/qa-bundle.json +194 -0
- package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
- package/dist/v2/templates/harness/SKILL.md +90 -0
- package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
- package/dist/v2/templates/project/.rivet/project.yaml +14 -0
- package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
- package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
- package/package.json +58 -0
- package/protocols/agent-orchestration.md +49 -0
- package/protocols/delivery.md +45 -0
- package/protocols/design-authority.md +29 -0
- package/protocols/goal-graph.md +44 -0
- package/protocols/pattern-first-development.md +29 -0
- package/protocols/qa-evidence.md +29 -0
- package/protocols/security.md +29 -0
- package/schemas/event.schema.json +86 -0
- package/schemas/evidence.schema.json +94 -0
- package/schemas/feature-decomposition.schema.json +48 -0
- package/schemas/feature-plan.schema.json +95 -0
- package/schemas/goal-graph.schema.json +61 -0
- package/schemas/integration.schema.json +119 -0
- package/schemas/orchestration.schema.json +69 -0
- package/schemas/project.schema.json +209 -0
- package/schemas/providers.schema.json +142 -0
- package/schemas/quality.schema.json +50 -0
- package/schemas/work-action.schema.json +44 -0
- package/schemas/work-request.schema.json +253 -0
- package/src/adapters/confluence.js +138 -0
- package/src/adapters/contract.js +657 -0
- package/src/adapters/factory.js +122 -0
- package/src/adapters/figma.js +178 -0
- package/src/adapters/fixtures.js +183 -0
- package/src/adapters/github.js +314 -0
- package/src/adapters/http.js +704 -0
- package/src/adapters/jira.js +195 -0
- package/src/adapters/linear.js +171 -0
- package/src/adapters/node-transport.js +59 -0
- package/src/cli/integration-setup-prompt.js +32 -0
- package/src/cli/interrupt.js +25 -0
- package/src/cli/main.js +596 -0
- package/src/cli/output.js +174 -0
- package/src/cli/parse-args.js +246 -0
- package/src/cli/project-discovery.js +70 -0
- package/src/cli/task-confirmation.js +38 -0
- package/src/cli/task-presentation.js +22 -0
- package/src/clients/claude.js +214 -0
- package/src/clients/codex.js +180 -0
- package/src/clients/compatibility.js +23 -0
- package/src/clients/contract.js +224 -0
- package/src/clients/fake.js +170 -0
- package/src/clients/process-runner.js +657 -0
- package/src/clients/result-contract.js +78 -0
- package/src/commands/delivery-publish.js +80 -0
- package/src/commands/delivery-remote.js +230 -0
- package/src/commands/delivery-review-update.js +39 -0
- package/src/commands/delivery-tracker.js +94 -0
- package/src/commands/delivery-transition.js +79 -0
- package/src/commands/delivery.js +223 -0
- package/src/commands/dependency-approval.js +21 -0
- package/src/commands/doctor.js +191 -0
- package/src/commands/evidence.js +44 -0
- package/src/commands/feature.js +212 -0
- package/src/commands/goals.js +231 -0
- package/src/commands/human-run.js +208 -0
- package/src/commands/human-task.js +278 -0
- package/src/commands/init.js +848 -0
- package/src/commands/install.js +814 -0
- package/src/commands/integration-setup.js +65 -0
- package/src/commands/integrations.js +54 -0
- package/src/commands/models.js +158 -0
- package/src/commands/orchestrate.js +61 -0
- package/src/commands/preflight.js +125 -0
- package/src/commands/protocols.js +228 -0
- package/src/commands/repositories.js +77 -0
- package/src/commands/setup-remote.js +41 -0
- package/src/commands/setup.js +214 -0
- package/src/commands/status.js +224 -0
- package/src/commands/support.js +44 -0
- package/src/commands/task-completion.js +104 -0
- package/src/commands/uninstall.js +234 -0
- package/src/commands/verify.js +171 -0
- package/src/commands/work.js +220 -0
- package/src/commands/worktrees.js +52 -0
- package/src/config/command-readiness.js +218 -0
- package/src/config/commands.js +206 -0
- package/src/config/defaults.js +55 -0
- package/src/config/load.js +230 -0
- package/src/config/validate.js +560 -0
- package/src/delivery/bitbucket-review.js +123 -0
- package/src/delivery/branch-publication.js +23 -0
- package/src/delivery/contract.js +379 -0
- package/src/delivery/github-deployment.js +132 -0
- package/src/delivery/github-review.js +5 -0
- package/src/delivery/github.js +382 -0
- package/src/delivery/gitlab-review.js +5 -0
- package/src/delivery/gitlab.js +395 -0
- package/src/delivery/prepare.js +90 -0
- package/src/delivery/publication-process.js +33 -0
- package/src/delivery/publication-transport.js +112 -0
- package/src/delivery/review-request.js +184 -0
- package/src/delivery/review-update.js +74 -0
- package/src/delivery/service.js +400 -0
- package/src/delivery/store.js +76 -0
- package/src/delivery/tracker-target.js +51 -0
- package/src/delivery/tracker-transition.js +158 -0
- package/src/delivery/tracker.js +164 -0
- package/src/discovery/git.js +85 -0
- package/src/discovery/portable.js +108 -0
- package/src/discovery/project.js +345 -0
- package/src/discovery/tools.js +145 -0
- package/src/evaluations/approval.js +20 -0
- package/src/evaluations/cost-policy.js +20 -0
- package/src/evaluations/harness-attempt.js +113 -0
- package/src/evaluations/live-runner.js +55 -0
- package/src/evaluations/profile.js +28 -0
- package/src/evaluations/report.js +11 -0
- package/src/evaluations/text-attempt.js +18 -0
- package/src/evidence/checksum.js +87 -0
- package/src/evidence/collect.js +727 -0
- package/src/evidence/validate.js +354 -0
- package/src/feature/accepted-integration.js +46 -0
- package/src/feature/actions.js +37 -0
- package/src/feature/client-profile.js +56 -0
- package/src/feature/decomposition-contract.js +58 -0
- package/src/feature/host-execution.js +633 -0
- package/src/feature/host-lock-recovery.js +96 -0
- package/src/feature/host-run-lock.js +32 -0
- package/src/feature/local-approval.js +183 -0
- package/src/feature/plan-contract.js +215 -0
- package/src/feature/planner.js +253 -0
- package/src/feature/run-store.js +241 -0
- package/src/feature/runtime-bridge.js +840 -0
- package/src/feature/verification-report.js +168 -0
- package/src/feature/workflow.js +375 -0
- package/src/git/client.js +509 -0
- package/src/git/integration-worktree.js +153 -0
- package/src/git/reconcile.js +222 -0
- package/src/git/reservations.js +450 -0
- package/src/git/worktrees.js +478 -0
- package/src/graph/completion.js +189 -0
- package/src/graph/fixtures.js +116 -0
- package/src/graph/reducer.js +124 -0
- package/src/graph/scheduler.js +252 -0
- package/src/graph/validate.js +106 -0
- package/src/install/managed.js +579 -0
- package/src/install/project-reference.js +33 -0
- package/src/install/project-runtime.js +142 -0
- package/src/install/runtime-integrity.cjs +141 -0
- package/src/integrations/capabilities.js +69 -0
- package/src/integrations/host-observation.js +26 -0
- package/src/integrations/registry.js +56 -0
- package/src/models/delegate.js +102 -0
- package/src/models/profiles.js +53 -0
- package/src/models/protocols.js +208 -0
- package/src/models/registry.js +83 -0
- package/src/models/transport.js +59 -0
- package/src/policy/approvals.js +251 -0
- package/src/policy/authority.js +282 -0
- package/src/policy/budget.js +149 -0
- package/src/policy/command-bootstrap.js +219 -0
- package/src/policy/commands.js +803 -0
- package/src/prompts/launch-contract.js +43 -0
- package/src/prompts/planning-contract.js +92 -0
- package/src/protocols/presentation.js +53 -0
- package/src/protocols/project.js +289 -0
- package/src/quality/runner.js +497 -0
- package/src/quality/traceability.js +215 -0
- package/src/repositories/identity.js +59 -0
- package/src/repositories/index.js +2 -0
- package/src/repositories/provider.js +210 -0
- package/src/runtime/application.js +353 -0
- package/src/runtime/dependency-directory.js +33 -0
- package/src/runtime/harness-discovery.js +121 -0
- package/src/runtime/heartbeat.js +47 -0
- package/src/runtime/instance-store.js +112 -0
- package/src/runtime/orchestrator.js +1133 -0
- package/src/runtime/portable-dependencies.js +94 -0
- package/src/runtime/recovery.js +190 -0
- package/src/runtime/retry.js +54 -0
- package/src/runtime/supervisor.js +166 -0
- package/src/runtime/worktree-bootstrap.js +169 -0
- package/src/state/event-store.js +333 -0
- package/src/state/lock.js +266 -0
- package/src/state/paths.js +274 -0
- package/src/state/redact.js +106 -0
- package/src/state/snapshot-store.js +310 -0
- package/src/status/public/app.js +96 -0
- package/src/status/public/index.html +37 -0
- package/src/status/public/styles.css +50 -0
- package/src/status/server.js +323 -0
- package/src/status/view-model.js +356 -0
- package/src/support/bundle.js +195 -0
- package/src/support/failure-report.js +96 -0
- package/src/work-request/contract.js +250 -0
- package/src/work-request/host.js +78 -0
- package/src/work-request/local.js +163 -0
- package/src/work-request/tracker.js +105 -0
- package/templates/evidence/qa-bundle.json +194 -0
- package/templates/github-actions/rivet-deploy.yml +98 -0
- package/templates/harness/SKILL.md +90 -0
- package/templates/project/.rivet/orchestration.yaml +42 -0
- package/templates/project/.rivet/project.yaml +14 -0
- package/templates/project/.rivet/providers.yaml +8 -0
- package/templates/project/.rivet/quality.yaml +16 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { claimApproval } from '../policy/approvals.js';
|
|
2
|
+
import { assertQualityRun } from './runner.js';
|
|
3
|
+
|
|
4
|
+
const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
5
|
+
const AC_ID = /^[A-Z][A-Z0-9]+-[1-9][0-9]*-AC[1-9][0-9]*$/;
|
|
6
|
+
const traceabilityResults = new WeakSet();
|
|
7
|
+
|
|
8
|
+
export class TraceabilityError extends Error {
|
|
9
|
+
constructor(reason = 'invalid-traceability-input') {
|
|
10
|
+
super('Acceptance traceability input is invalid.');
|
|
11
|
+
this.name = 'TraceabilityError';
|
|
12
|
+
this.code = 'ERR_TRACEABILITY';
|
|
13
|
+
this.safeMessage = this.message;
|
|
14
|
+
this.details = Object.freeze({ reason });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fail(reason) { throw new TraceabilityError(reason); }
|
|
19
|
+
|
|
20
|
+
function capture(value, allowed, required = []) {
|
|
21
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail('invalid-traceability-input');
|
|
22
|
+
let keys;
|
|
23
|
+
try { keys = Reflect.ownKeys(value); } catch { fail('invalid-traceability-input'); }
|
|
24
|
+
if (keys.some(key => typeof key !== 'string' || !allowed.has(key))) fail('invalid-traceability-input');
|
|
25
|
+
const result = Object.create(null);
|
|
26
|
+
for (const key of keys) {
|
|
27
|
+
let descriptor;
|
|
28
|
+
try { descriptor = Object.getOwnPropertyDescriptor(value, key); } catch { fail('invalid-traceability-input'); }
|
|
29
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail('invalid-traceability-input');
|
|
30
|
+
result[key] = descriptor.value;
|
|
31
|
+
}
|
|
32
|
+
if (required.some(key => !Object.hasOwn(result, key))) fail('invalid-traceability-input');
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function array(value, maximum, convert) {
|
|
37
|
+
try {
|
|
38
|
+
if (!Array.isArray(value)) fail('invalid-traceability-input');
|
|
39
|
+
const length = Object.getOwnPropertyDescriptor(value, 'length')?.value;
|
|
40
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > maximum
|
|
41
|
+
|| Reflect.ownKeys(value).length !== length + 1) fail('invalid-traceability-input');
|
|
42
|
+
const result = [];
|
|
43
|
+
for (let index = 0; index < length; index += 1) {
|
|
44
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
45
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail('invalid-traceability-input');
|
|
46
|
+
result.push(convert(descriptor.value));
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof TraceabilityError) throw error;
|
|
51
|
+
fail('invalid-traceability-input');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function id(value) {
|
|
56
|
+
if (typeof value !== 'string' || value.length > 64 || !ID.test(value)) fail('invalid-traceability-input');
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function acId(value) {
|
|
61
|
+
if (typeof value !== 'string' || value.length > 100 || !AC_ID.test(value)) fail('invalid-traceability-input');
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function criterion(value) {
|
|
66
|
+
const input = capture(value, new Set(['id', 'inScope']), ['id', 'inScope']);
|
|
67
|
+
if (typeof input.inScope !== 'boolean') fail('invalid-traceability-input');
|
|
68
|
+
return Object.freeze({ id: acId(input.id), inScope: input.inScope });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function manualEvidence(value) {
|
|
72
|
+
const input = capture(value, new Set([
|
|
73
|
+
'id', 'acceptanceCriterion', 'expectedApproverId', 'approval',
|
|
74
|
+
]), ['id', 'acceptanceCriterion', 'expectedApproverId', 'approval']);
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
id: id(input.id),
|
|
77
|
+
acceptanceCriterion: acId(input.acceptanceCriterion),
|
|
78
|
+
expectedApproverId: id(input.expectedApproverId),
|
|
79
|
+
approval: input.approval,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function assertTraceabilityResult(value) {
|
|
84
|
+
if (!traceabilityResults.has(value)) fail('invalid-traceability-input');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function immutable(value) {
|
|
88
|
+
if (Array.isArray(value)) return Object.freeze(value.map(immutable));
|
|
89
|
+
if (value && typeof value === 'object') {
|
|
90
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, child]) => [key, immutable(child)])));
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function validateTraceability(input, options = {}) {
|
|
96
|
+
const value = capture(input, new Set([
|
|
97
|
+
'subjectId', 'qualityRun', 'acceptanceCriteria', 'manualReviews',
|
|
98
|
+
]), ['subjectId', 'qualityRun', 'acceptanceCriteria', 'manualReviews']);
|
|
99
|
+
const configured = capture(options, new Set(['approvalRegistry', 'nowMs']), ['approvalRegistry', 'nowMs']);
|
|
100
|
+
const subjectId = id(value.subjectId);
|
|
101
|
+
try { assertQualityRun(value.qualityRun); } catch { fail('invalid-traceability-input'); }
|
|
102
|
+
if (!Number.isSafeInteger(configured.nowMs) || configured.nowMs < 0) fail('invalid-traceability-input');
|
|
103
|
+
const criteria = array(value.acceptanceCriteria, 2_000, criterion);
|
|
104
|
+
const tests = value.qualityRun.tests;
|
|
105
|
+
const manualReviews = array(value.manualReviews, 2_000, manualEvidence);
|
|
106
|
+
if (criteria.length === 0
|
|
107
|
+
|| new Set(criteria.map(item => item.id.toLowerCase())).size !== criteria.length
|
|
108
|
+
|| new Set(tests.map(item => item.id.toLowerCase())).size !== tests.length
|
|
109
|
+
|| new Set(manualReviews.map(item => item.id.toLowerCase())).size !== manualReviews.length
|
|
110
|
+
|| new Set(manualReviews.map(item => item.acceptanceCriterion.toLowerCase())).size !== manualReviews.length) {
|
|
111
|
+
fail('invalid-traceability-input');
|
|
112
|
+
}
|
|
113
|
+
const known = new Set(criteria.map(item => item.id));
|
|
114
|
+
if (tests.some(item => item.acceptanceCriteria.some(ac => !known.has(ac)))
|
|
115
|
+
|| manualReviews.some(item => !known.has(item.acceptanceCriterion))) fail('unknown-acceptance-criterion');
|
|
116
|
+
|
|
117
|
+
const manualRequired = new Set(criteria
|
|
118
|
+
.filter(item => item.inScope && !tests.some(test => test.status === 'passed' && test.acceptanceCriteria.includes(item.id)))
|
|
119
|
+
.map(item => item.id));
|
|
120
|
+
if (manualReviews.some(review => !manualRequired.has(review.acceptanceCriterion))) {
|
|
121
|
+
fail('invalid-traceability-input');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const approvedManual = new Map();
|
|
125
|
+
const approvalErrors = new Map();
|
|
126
|
+
const claims = [];
|
|
127
|
+
for (const review of manualReviews) {
|
|
128
|
+
let claim = { valid: false };
|
|
129
|
+
try {
|
|
130
|
+
claim = claimApproval(review.approval, {
|
|
131
|
+
subjectId,
|
|
132
|
+
action: 'quality.manual-review',
|
|
133
|
+
resource: 'acceptance:' + review.acceptanceCriterion + ':manual-review:' + review.id,
|
|
134
|
+
policyId: 'quality.acceptance',
|
|
135
|
+
}, {
|
|
136
|
+
registry: configured.approvalRegistry,
|
|
137
|
+
expectedApproverId: review.expectedApproverId,
|
|
138
|
+
requireHumanApprover: true,
|
|
139
|
+
requireSingleUse: true,
|
|
140
|
+
nowMs: configured.nowMs,
|
|
141
|
+
});
|
|
142
|
+
} catch { claim = { valid: false }; }
|
|
143
|
+
if (claim.valid) {
|
|
144
|
+
claims.push({ claim, review });
|
|
145
|
+
approvedManual.set(review.acceptanceCriterion, review);
|
|
146
|
+
}
|
|
147
|
+
else approvalErrors.set(review.acceptanceCriterion, 'Acceptance criterion ' + review.acceptanceCriterion + ' lacks exact human approval.');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const coverage = [];
|
|
151
|
+
const errors = [];
|
|
152
|
+
const outOfScope = [];
|
|
153
|
+
for (const criterionEntry of [...criteria].sort((left, right) => left.id.localeCompare(right.id))) {
|
|
154
|
+
if (!criterionEntry.inScope) {
|
|
155
|
+
outOfScope.push(criterionEntry.id);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const passed = tests
|
|
159
|
+
.filter(item => item.status === 'passed' && item.acceptanceCriteria.includes(criterionEntry.id))
|
|
160
|
+
.sort((left, right) => left.id.localeCompare(right.id))[0];
|
|
161
|
+
if (passed) {
|
|
162
|
+
coverage.push({
|
|
163
|
+
acceptanceCriterion: criterionEntry.id,
|
|
164
|
+
method: 'test',
|
|
165
|
+
evidenceId: passed.id,
|
|
166
|
+
gateId: passed.gateId,
|
|
167
|
+
resultPath: passed.resultPath,
|
|
168
|
+
resultSha256: passed.resultSha256,
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const manual = approvedManual.get(criterionEntry.id);
|
|
173
|
+
if (manual) {
|
|
174
|
+
coverage.push({
|
|
175
|
+
acceptanceCriterion: criterionEntry.id,
|
|
176
|
+
method: 'manual-review',
|
|
177
|
+
evidenceId: manual.id,
|
|
178
|
+
approvalReceiptId: manual.approval.id,
|
|
179
|
+
approverId: manual.approval.approverId,
|
|
180
|
+
approverPrincipal: manual.approval.approverPrincipal,
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
errors.push(approvalErrors.get(criterionEntry.id)
|
|
185
|
+
?? 'Acceptance criterion ' + criterionEntry.id + ' lacks a passed deterministic test or exact human approval.');
|
|
186
|
+
}
|
|
187
|
+
if (errors.length > 0) {
|
|
188
|
+
for (const entry of claims) entry.claim.release();
|
|
189
|
+
} else {
|
|
190
|
+
const finalized = [];
|
|
191
|
+
try {
|
|
192
|
+
for (const entry of claims) {
|
|
193
|
+
if (!entry.claim.finalize()) fail('approval-transaction-failed');
|
|
194
|
+
finalized.push(entry);
|
|
195
|
+
}
|
|
196
|
+
for (const entry of claims) {
|
|
197
|
+
if (!entry.claim.publish()) fail('approval-transaction-failed');
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
for (const entry of finalized) entry.claim.rollback();
|
|
201
|
+
for (const entry of claims) if (!finalized.includes(entry)) entry.claim.release();
|
|
202
|
+
if (error instanceof TraceabilityError) throw error;
|
|
203
|
+
fail('approval-transaction-failed');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const output = immutable({
|
|
207
|
+
valid: errors.length === 0,
|
|
208
|
+
commitSha: value.qualityRun.commitSha,
|
|
209
|
+
coverage,
|
|
210
|
+
outOfScope,
|
|
211
|
+
errors,
|
|
212
|
+
});
|
|
213
|
+
traceabilityResults.add(output);
|
|
214
|
+
return output;
|
|
215
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { boundedString, captureRecord, failProvider } from '../adapters/contract.js';
|
|
3
|
+
import { runArgv } from '../discovery/tools.js';
|
|
4
|
+
|
|
5
|
+
const HOSTS = Object.freeze({ 'github.com': 'github', 'bitbucket.org': 'bitbucket', 'gitlab.com': 'gitlab' });
|
|
6
|
+
const SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,99}$/;
|
|
7
|
+
|
|
8
|
+
export function parseRepositoryRemote(input) {
|
|
9
|
+
const value = boundedString(input, 2048);
|
|
10
|
+
// Parse before URL normalization, which would otherwise silently remove traversal.
|
|
11
|
+
const match = /^(?:https:\/\/|ssh:\/\/git@)([^/:?#]+)\/([^?#]+)$/.exec(value)
|
|
12
|
+
?? /^git@([^/:?#]+):([^?#]+)$/.exec(value);
|
|
13
|
+
if (!match || !Object.hasOwn(HOSTS, match[1])) failProvider('invalid-request');
|
|
14
|
+
const host = match[1];
|
|
15
|
+
const fullName = match[2].replace(/\.git$/, '');
|
|
16
|
+
const segments = fullName.split('/');
|
|
17
|
+
if (segments.length < 2 || segments.length > 20 || fullName.length > 255
|
|
18
|
+
|| (host !== 'gitlab.com' && segments.length !== 2)
|
|
19
|
+
|| segments.some(part => !SEGMENT.test(part) || part === '.' || part === '..' || part.endsWith('.'))) failProvider('invalid-request');
|
|
20
|
+
return Object.freeze({ provider: HOSTS[host], host, namespace: segments.slice(0, -1).join('/'), name: segments.at(-1), fullName, url: `https://${host}/${fullName}` });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function selectRepositoryRemote(input, options = {}) {
|
|
24
|
+
const { remoteName } = captureRecord(options, new Set(['remoteName']), []);
|
|
25
|
+
if (remoteName !== undefined) boundedString(remoteName, 100, /^[A-Za-z0-9][A-Za-z0-9._-]*$/);
|
|
26
|
+
if (!Array.isArray(input) || input.length < 1 || input.length > 100) failProvider('invalid-request');
|
|
27
|
+
const candidates = input.map(value => {
|
|
28
|
+
const remote = captureRecord(value, new Set(['name', 'url']), ['name', 'url']);
|
|
29
|
+
boundedString(remote.name, 100, /^[A-Za-z0-9][A-Za-z0-9._-]*$/);
|
|
30
|
+
return remote;
|
|
31
|
+
}).filter(remote => remoteName === undefined || remote.name === remoteName);
|
|
32
|
+
if (candidates.length === 0) failProvider('invalid-request');
|
|
33
|
+
const parsed = candidates.map(remote => ({ ...parseRepositoryRemote(remote.url), remoteName: remote.name }));
|
|
34
|
+
if (new Set(parsed.map(remote => remote.url)).size !== 1) failProvider('invalid-request');
|
|
35
|
+
return Object.freeze(parsed[0]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function discoverRepositoryRemotes(projectRoot, { runner = runArgv } = {}) {
|
|
39
|
+
let result;
|
|
40
|
+
try { result = await runner('git', ['remote', '-v'], { cwd: resolve(projectRoot), shell: false, timeoutMs: 3000, maxOutputBytes: 32768 }); }
|
|
41
|
+
catch { failProvider('invalid-request'); }
|
|
42
|
+
if (result?.code !== 0 || result.timedOut === true || result.truncated?.stdout === true || typeof result.stdout !== 'string' || Buffer.byteLength(result.stdout) > 32768) failProvider('invalid-request');
|
|
43
|
+
const remotes = [];
|
|
44
|
+
for (const line of result.stdout.trim().split('\n').filter(Boolean)) {
|
|
45
|
+
const match = /^(\S+)\s+(\S+) \((fetch|push)\)$/.exec(line);
|
|
46
|
+
if (!match) failProvider('invalid-request');
|
|
47
|
+
if (match[3] === 'fetch') remotes.push(Object.freeze({ name: boundedString(match[1], 100, /^[A-Za-z0-9][A-Za-z0-9._-]*$/), url: boundedString(match[2], 2048) }));
|
|
48
|
+
}
|
|
49
|
+
if (remotes.length > 100) failProvider('invalid-request');
|
|
50
|
+
return Object.freeze(remotes);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Explicit per-operation choices do not rewrite the tracked onboarding preference.
|
|
54
|
+
export function selectConfiguredRepositoryRemote(remotes, preference, remoteName) {
|
|
55
|
+
const selected = selectRepositoryRemote(remotes, remoteName !== undefined ? {remoteName}
|
|
56
|
+
: preference ? {remoteName: preference.name} : {});
|
|
57
|
+
if (remoteName === undefined && preference && selected.url !== preference.url) failProvider('invalid-request');
|
|
58
|
+
return selected;
|
|
59
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { boundedString, captureRecord, failProvider, immutableRedactedJson, providerTimestamp } from '../adapters/contract.js';
|
|
2
|
+
import { createGithubAdapter } from '../adapters/github.js';
|
|
3
|
+
import { createProviderHttpClient } from '../adapters/http.js';
|
|
4
|
+
import { parseRepositoryRemote } from './identity.js';
|
|
5
|
+
|
|
6
|
+
const ENDPOINTS = Object.freeze({ github: 'https://api.github.com', bitbucket: 'https://api.bitbucket.org/2.0', gitlab: 'https://gitlab.com/api/v4' });
|
|
7
|
+
const CAPABILITIES = Object.freeze({ read: Object.freeze(['repository', 'branch', 'review-request', 'checks', 'reviews']), write: Object.freeze([]) });
|
|
8
|
+
const SHA = /^[a-f0-9]{40}$/;
|
|
9
|
+
const BRANCH = /^[A-Za-z0-9_][A-Za-z0-9._/-]{0,254}$/;
|
|
10
|
+
function remote(condition, provider) { if (!condition) failProvider('remote', { provider, retryClassification: 'permanent' }); }
|
|
11
|
+
function record(value, provider) { remote(value !== null && typeof value === 'object' && !Array.isArray(value), provider); return value; }
|
|
12
|
+
function text(value, provider, maximum = 1024) { remote(typeof value === 'string' && value.length > 0 && value.length <= maximum, provider); return value; }
|
|
13
|
+
function sha(value, provider) { remote(typeof value === 'string' && SHA.test(value), provider); return value; }
|
|
14
|
+
function ref(value, provider) { remote(typeof value === 'string' && BRANCH.test(value) && !value.includes('..') && !value.includes('//'), provider); return value; }
|
|
15
|
+
function positive(value, provider) { remote(Number.isSafeInteger(value) && value > 0, provider); return value; }
|
|
16
|
+
function boundUrl(value, expected, provider) { remote(value === expected, provider); return value; }
|
|
17
|
+
function number(value) { if (!Number.isSafeInteger(value) || value < 1 || value > 1_000_000_000) failProvider('invalid-request'); return value; }
|
|
18
|
+
|
|
19
|
+
export function createRepositoryProvider(input) {
|
|
20
|
+
const config = captureRecord(input, new Set(['repository', 'transport', 'headers', 'timeoutMs', 'maxResponseBytes', 'maxPages', 'maxItems', 'clock']), ['repository', 'transport'], 'invalid-config');
|
|
21
|
+
const supplied = captureRecord(config.repository, new Set(['provider', 'host', 'namespace', 'name', 'fullName', 'url', 'remoteName']), ['provider', 'host', 'namespace', 'name', 'fullName', 'url'], 'invalid-config');
|
|
22
|
+
const repository = parseRepositoryRemote(supplied.url);
|
|
23
|
+
for (const key of Object.keys(repository)) if (repository[key] !== supplied[key]) failProvider('invalid-config');
|
|
24
|
+
const provider = repository.provider;
|
|
25
|
+
const clock = config.clock ?? (() => new Date().toISOString());
|
|
26
|
+
if (typeof clock !== 'function') failProvider('invalid-config', { provider });
|
|
27
|
+
const limits = Object.fromEntries(['headers', 'timeoutMs', 'maxResponseBytes', 'maxPages', 'maxItems'].filter(key => config[key] !== undefined).map(key => [key, config[key]]));
|
|
28
|
+
const http = createProviderHttpClient({ provider, transport: config.transport, baseUrl: ENDPOINTS[provider], ...limits, allowEncodedSlash: true });
|
|
29
|
+
const github = provider === 'github' ? createGithubAdapter({ transport: config.transport, baseUrl: ENDPOINTS.github, ...limits, clock }) : null;
|
|
30
|
+
const root = provider === 'github' ? `/repos/${repository.fullName}` : provider === 'bitbucket' ? `/repositories/${repository.fullName}` : `/projects/${encodeURIComponent(repository.fullName)}`;
|
|
31
|
+
const reviewRoot = provider === 'github' ? 'pulls' : provider === 'bitbucket' ? 'pullrequests' : 'merge_requests';
|
|
32
|
+
const maxPages = config.maxPages ?? 20, maxItems = config.maxItems ?? 10_000;
|
|
33
|
+
const request = (path, signal) => http.request({ method: 'GET', path, ...(signal === undefined ? {} : { signal }) });
|
|
34
|
+
|
|
35
|
+
async function bitbucketPages(path, signal) {
|
|
36
|
+
const initial = new URL(http.urlFor(`${path}?pagelen=100`));
|
|
37
|
+
const output = [], seen = new Set();
|
|
38
|
+
let next = initial.href, declaredSize;
|
|
39
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
40
|
+
let url;
|
|
41
|
+
try { url = new URL(next); } catch { failProvider('remote', { provider }); }
|
|
42
|
+
remote(url.origin === initial.origin && url.pathname === initial.pathname && !url.username && !url.password && !url.hash, provider);
|
|
43
|
+
remote([...url.searchParams.keys()].every(key => ['page', 'pagelen'].includes(key)) && url.searchParams.getAll('pagelen').length === 1
|
|
44
|
+
&& url.searchParams.get('pagelen') === '100' && url.searchParams.getAll('page').length <= 1
|
|
45
|
+
&& (!url.searchParams.has('page') || /^[1-9][0-9]{0,8}$/.test(url.searchParams.get('page'))), provider);
|
|
46
|
+
remote(Number(url.searchParams.get('page') ?? 1) === page + 1, provider);
|
|
47
|
+
remote(!seen.has(url.href), provider); seen.add(url.href);
|
|
48
|
+
const response = await request(`${url.pathname.slice('/2.0'.length)}${url.search}`, signal);
|
|
49
|
+
remote(Array.isArray(response.data?.values), provider);
|
|
50
|
+
if (response.data.size !== undefined) {
|
|
51
|
+
remote(Number.isSafeInteger(response.data.size) && response.data.size >= 0 && (declaredSize === undefined || declaredSize === response.data.size), provider);
|
|
52
|
+
declaredSize = response.data.size;
|
|
53
|
+
if (declaredSize > maxItems) failProvider('pagination-limit', { provider });
|
|
54
|
+
}
|
|
55
|
+
if (response.data.page !== undefined) remote(response.data.page === page + 1, provider);
|
|
56
|
+
output.push(...response.data.values);
|
|
57
|
+
if (declaredSize !== undefined) remote(output.length <= declaredSize, provider);
|
|
58
|
+
if (output.length > maxItems) failProvider('pagination-limit', { provider });
|
|
59
|
+
if (response.data.next === undefined || response.data.next === null) {
|
|
60
|
+
if (declaredSize !== undefined) remote(output.length === declaredSize, provider);
|
|
61
|
+
return output;
|
|
62
|
+
}
|
|
63
|
+
remote(typeof response.data.next === 'string' && response.data.next.length <= 4096 && response.data.values.length > 0, provider);
|
|
64
|
+
next = response.data.next;
|
|
65
|
+
}
|
|
66
|
+
failProvider('pagination-limit', { provider });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function repositoryInfo(signal) {
|
|
70
|
+
if (github) {
|
|
71
|
+
const value = (await github.read({ kind: 'repo', owner: repository.namespace, repo: repository.name, ...(signal === undefined ? {} : { signal }) })).normalized.repository;
|
|
72
|
+
return { ...repository, id: value.id, defaultBranch: value.defaultBranch, private: value.private };
|
|
73
|
+
}
|
|
74
|
+
const value = (await request(root, signal)).data;
|
|
75
|
+
if (provider === 'bitbucket') {
|
|
76
|
+
remote(value?.full_name === repository.fullName && typeof value.is_private === 'boolean', provider);
|
|
77
|
+
boundUrl(value.links?.html?.href, repository.url, provider);
|
|
78
|
+
return { ...repository, id: text(value.uuid, provider), defaultBranch: value.mainbranch === null ? null : ref(value.mainbranch?.name, provider), private: value.is_private };
|
|
79
|
+
}
|
|
80
|
+
remote(value?.path_with_namespace === repository.fullName && ['private', 'internal', 'public'].includes(value.visibility), provider);
|
|
81
|
+
boundUrl(value.web_url, repository.url, provider);
|
|
82
|
+
return { ...repository, id: positive(value.id, provider), defaultBranch: value.default_branch === null ? null : ref(value.default_branch, provider), private: value.visibility !== 'public' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function branchInfo(branch, signal) {
|
|
86
|
+
boundedString(branch, 255, BRANCH); if (branch.includes('..') || branch.includes('//')) failProvider('invalid-request');
|
|
87
|
+
if (github) {
|
|
88
|
+
const value = (await request(`${root}/branches/${encodeURIComponent(branch)}`, signal)).data;
|
|
89
|
+
remote(value?.name === branch && typeof value.protected === 'boolean', provider);
|
|
90
|
+
return { name: branch, commitSha: sha(value.commit?.sha, provider), protected: value.protected };
|
|
91
|
+
}
|
|
92
|
+
const value = (await request(`${root}/${provider === 'bitbucket' ? 'refs' : 'repository'}/branches/${encodeURIComponent(branch)}`, signal)).data;
|
|
93
|
+
remote(value?.name === branch, provider);
|
|
94
|
+
return { name: branch, commitSha: sha(provider === 'bitbucket' ? value.target?.hash : value.commit?.id, provider), protected: provider === 'bitbucket' ? null : typeof value.protected === 'boolean' ? value.protected : null };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function reviewInfo(reviewNumber, signal) {
|
|
98
|
+
const value = (await request(`${root}/${reviewRoot}/${number(reviewNumber)}`, signal)).data;
|
|
99
|
+
let result;
|
|
100
|
+
if (provider === 'github') {
|
|
101
|
+
remote(value?.number === reviewNumber && ['open', 'closed'].includes(value.state) && typeof value.merged === 'boolean', provider);
|
|
102
|
+
result = { number: reviewNumber, state: value.merged ? 'merged' : value.state, title: text(value.title, provider), headSha: sha(value.head?.sha, provider), headBranch: ref(value.head?.ref, provider), baseBranch: ref(value.base?.ref, provider), url: boundUrl(value.html_url, `${repository.url}/pull/${reviewNumber}`, provider) };
|
|
103
|
+
} else if (provider === 'bitbucket') {
|
|
104
|
+
remote(value?.id === reviewNumber && ['OPEN', 'MERGED', 'DECLINED', 'SUPERSEDED'].includes(value.state) && value.destination?.repository?.full_name === repository.fullName, provider);
|
|
105
|
+
result = { number: reviewNumber, state: value.state === 'OPEN' ? 'open' : value.state === 'MERGED' ? 'merged' : 'closed', title: text(value.title, provider), headSha: sha(value.source?.commit?.hash, provider), headBranch: ref(value.source?.branch?.name, provider), baseBranch: ref(value.destination?.branch?.name, provider), url: boundUrl(value.links?.html?.href, `${repository.url}/pull-requests/${reviewNumber}`, provider) };
|
|
106
|
+
} else {
|
|
107
|
+
remote(value?.iid === reviewNumber && ['opened', 'closed', 'locked', 'merged'].includes(value.state), provider);
|
|
108
|
+
result = { number: reviewNumber, state: value.state === 'opened' ? 'open' : value.state, title: text(value.title, provider), headSha: sha(value.sha, provider), headBranch: ref(value.source_branch, provider), baseBranch: ref(value.target_branch, provider), url: boundUrl(value.web_url, `${repository.url}/-/merge_requests/${reviewNumber}`, provider) };
|
|
109
|
+
}
|
|
110
|
+
return { result, raw: value };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function checksInfo(commitSha, signal) {
|
|
114
|
+
boundedString(commitSha, 40, SHA);
|
|
115
|
+
const options = signal === undefined ? {} : { signal };
|
|
116
|
+
let items;
|
|
117
|
+
if (provider === 'github') {
|
|
118
|
+
const checks = await http.paginate({ path: `${root}/commits/${commitSha}/check-runs?per_page=100`, itemsKey: 'check_runs', identityKey: 'id', totalKey: 'total_count', ...options });
|
|
119
|
+
const statuses = await http.paginate({ path: `${root}/commits/${commitSha}/statuses?per_page=100`, identityKey: 'id', ...options });
|
|
120
|
+
items = checks.map(value => {
|
|
121
|
+
record(value, provider);
|
|
122
|
+
positive(value.id, provider); sha(value.head_sha, provider);
|
|
123
|
+
if (value.head_sha !== commitSha) failProvider('state-conflict', { provider });
|
|
124
|
+
remote(['queued', 'in_progress', 'completed', 'pending', 'requested', 'waiting'].includes(value.status), provider);
|
|
125
|
+
const conclusions = { success: 'success', failure: 'failure', timed_out: 'failure', cancelled: 'cancelled', skipped: 'skipped', neutral: 'neutral', action_required: 'blocked', stale: 'stale' };
|
|
126
|
+
if (value.status === 'completed') remote(Object.hasOwn(conclusions, value.conclusion), provider);
|
|
127
|
+
return { id: `check:${value.id}`, name: text(value.name, provider), commitSha, state: value.status === 'completed' ? conclusions[value.conclusion] : 'pending' };
|
|
128
|
+
});
|
|
129
|
+
items.push(...statuses.map(value => {
|
|
130
|
+
record(value, provider);
|
|
131
|
+
positive(value.id, provider);
|
|
132
|
+
if (value.sha !== undefined && value.sha !== commitSha) failProvider('state-conflict', { provider });
|
|
133
|
+
remote(['success', 'failure', 'error', 'pending'].includes(value.state), provider);
|
|
134
|
+
return { id: `status:${value.id}`, name: text(value.context, provider), commitSha, state: value.state === 'error' ? 'failure' : value.state };
|
|
135
|
+
}));
|
|
136
|
+
} else if (provider === 'bitbucket') {
|
|
137
|
+
const values = await bitbucketPages(`${root}/commit/${commitSha}/statuses`, signal);
|
|
138
|
+
items = values.map(value => {
|
|
139
|
+
record(value, provider);
|
|
140
|
+
if (value.links?.commit?.href !== undefined && value.links.commit.href !== `${ENDPOINTS.bitbucket}${root}/commit/${commitSha}`) failProvider('state-conflict', { provider });
|
|
141
|
+
const states = { SUCCESSFUL: 'success', FAILED: 'failure', INPROGRESS: 'pending', STOPPED: 'cancelled' };
|
|
142
|
+
remote(Object.hasOwn(states, value.state), provider);
|
|
143
|
+
return { id: text(value.key, provider), name: text(value.name || value.key, provider), commitSha, state: states[value.state] };
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
const values = await http.paginate({ path: `${root}/repository/commits/${commitSha}/statuses?per_page=100`, identityKey: 'id', ...options });
|
|
147
|
+
items = values.map(value => {
|
|
148
|
+
record(value, provider);
|
|
149
|
+
positive(value.id, provider); sha(value.sha, provider);
|
|
150
|
+
if (value.sha !== commitSha) failProvider('state-conflict', { provider });
|
|
151
|
+
const states = { pending: 'pending', running: 'pending', success: 'success', failed: 'failure', canceled: 'cancelled', skipped: 'skipped', created: 'pending', manual: 'blocked', scheduled: 'pending', preparing: 'pending', waiting_for_resource: 'pending' };
|
|
152
|
+
remote(Object.hasOwn(states, value.status), provider);
|
|
153
|
+
return { id: String(value.id), name: text(value.name, provider), commitSha, state: states[value.status] };
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
remote(new Set(items.map(value => value.id)).size === items.length, provider);
|
|
157
|
+
return { commitSha, items, requiredPolicy: 'unknown' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function reviewsInfo(reviewNumber, signal) {
|
|
161
|
+
number(reviewNumber);
|
|
162
|
+
let items;
|
|
163
|
+
if (provider === 'github') {
|
|
164
|
+
const values = await http.paginate({ path: `${root}/pulls/${reviewNumber}/reviews?per_page=100`, identityKey: 'id', ...(signal === undefined ? {} : {signal}) });
|
|
165
|
+
items = values.map(value => {
|
|
166
|
+
record(value, provider);
|
|
167
|
+
positive(value.id, provider); remote(['PENDING', 'COMMENTED', 'APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(value.state), provider);
|
|
168
|
+
return { id: String(value.id), reviewer: text(value.user?.login, provider), state: value.state.toLowerCase(), commitSha: value.commit_id == null ? null : sha(value.commit_id, provider) };
|
|
169
|
+
});
|
|
170
|
+
} else if (provider === 'bitbucket') {
|
|
171
|
+
const { raw } = await reviewInfo(reviewNumber, signal);
|
|
172
|
+
remote(Array.isArray(raw.participants) && raw.participants.length <= maxItems, provider);
|
|
173
|
+
items = raw.participants.map(value => {
|
|
174
|
+
record(value, provider);
|
|
175
|
+
remote(typeof value.approved === 'boolean', provider);
|
|
176
|
+
return { reviewer: text(value.user?.uuid, provider), state: value.approved ? 'approved' : value.state === 'changes_requested' ? 'changes_requested' : 'unapproved', commitSha: null };
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
const value = (await request(`${root}/merge_requests/${reviewNumber}/approvals`, signal)).data;
|
|
180
|
+
remote(value?.iid === reviewNumber && Array.isArray(value.approved_by) && value.approved_by.length <= maxItems, provider);
|
|
181
|
+
items = value.approved_by.map(entry => ({ reviewer: text(record(entry, provider).user?.username, provider), state: 'approved', commitSha: null }));
|
|
182
|
+
}
|
|
183
|
+
return { items, policy: 'unknown' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function read(inputRead) {
|
|
187
|
+
const value = captureRecord(inputRead, new Set(['kind', 'branch', 'number', 'commitSha', 'signal']), ['kind']);
|
|
188
|
+
if (!CAPABILITIES.read.includes(value.kind)) failProvider('invalid-request', { provider });
|
|
189
|
+
let normalized;
|
|
190
|
+
if (value.kind === 'repository') normalized = { repository: await repositoryInfo(value.signal) };
|
|
191
|
+
else if (value.kind === 'branch') normalized = { branch: await branchInfo(value.branch, value.signal) };
|
|
192
|
+
else if (value.kind === 'review-request') normalized = { reviewRequest: (await reviewInfo(value.number, value.signal)).result };
|
|
193
|
+
else if (value.kind === 'checks') normalized = { checks: await checksInfo(value.commitSha, value.signal) };
|
|
194
|
+
else normalized = { reviews: await reviewsInfo(value.number, value.signal) };
|
|
195
|
+
return immutableRedactedJson({ repository, ...normalized, observedAt: providerTimestamp(clock, provider), capabilities: CAPABILITIES });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function inspect(inputInspect) {
|
|
199
|
+
const value = captureRecord(inputInspect, new Set(['number', 'signal']), ['number']);
|
|
200
|
+
number(value.number);
|
|
201
|
+
const info = await repositoryInfo(value.signal);
|
|
202
|
+
const review = (await reviewInfo(value.number, value.signal)).result;
|
|
203
|
+
const checks = await checksInfo(review.headSha, value.signal);
|
|
204
|
+
const reviews = await reviewsInfo(value.number, value.signal);
|
|
205
|
+
const latest = (await reviewInfo(value.number, value.signal)).result;
|
|
206
|
+
if (latest.headSha !== review.headSha || latest.state !== review.state || latest.baseBranch !== review.baseBranch) failProvider('state-conflict', { provider });
|
|
207
|
+
return immutableRedactedJson({ repository: info, reviewRequest: latest, checks, reviews, observedAt: providerTimestamp(clock, provider), consistency: 'head-rechecked', capabilities: CAPABILITIES });
|
|
208
|
+
}
|
|
209
|
+
return Object.freeze({ provider, repository, capabilities: CAPABILITIES, read, inspect, write() { failProvider('read-only', { provider }); } });
|
|
210
|
+
}
|