@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,395 @@
|
|
|
1
|
+
import { createProviderHttpClient } from '../adapters/http.js';
|
|
2
|
+
import { captureRecord, createProviderWireBody } from '../adapters/contract.js';
|
|
3
|
+
import { parseRepositoryRemote } from '../repositories/identity.js';
|
|
4
|
+
import { createTrustedDeliveryExecutor } from './service.js';
|
|
5
|
+
import {
|
|
6
|
+
ensure,
|
|
7
|
+
exact,
|
|
8
|
+
factsDigest,
|
|
9
|
+
hash,
|
|
10
|
+
plain,
|
|
11
|
+
ready,
|
|
12
|
+
sha,
|
|
13
|
+
timestamp,
|
|
14
|
+
validateCandidate,
|
|
15
|
+
} from './contract.js';
|
|
16
|
+
|
|
17
|
+
// GitLab conditions this operation on the source SHA. Target and policy are
|
|
18
|
+
// rechecked, but concurrent administrator changes are not an atomic condition.
|
|
19
|
+
export function createGitlabDeliveryExecutor(inputConfig) {
|
|
20
|
+
const input = captureRecord(
|
|
21
|
+
inputConfig,
|
|
22
|
+
new Set(['repository', 'transport', 'headers', 'clock', 'timeoutMs']),
|
|
23
|
+
['repository', 'transport'],
|
|
24
|
+
'invalid-config',
|
|
25
|
+
);
|
|
26
|
+
const supplied = plain(input.repository),
|
|
27
|
+
repository = parseRepositoryRemote(supplied.url);
|
|
28
|
+
ensure(repository.provider === 'gitlab' && hash(repository) === hash(supplied));
|
|
29
|
+
const clock = input.clock ?? (() => new Date().toISOString());
|
|
30
|
+
ensure(typeof clock === 'function');
|
|
31
|
+
const http = createProviderHttpClient({
|
|
32
|
+
provider: 'gitlab',
|
|
33
|
+
baseUrl: 'https://gitlab.com/api/v4',
|
|
34
|
+
transport: input.transport,
|
|
35
|
+
...(input.headers === undefined ? {} : { headers: input.headers }),
|
|
36
|
+
...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
|
|
37
|
+
allowEncodedSlash: true,
|
|
38
|
+
});
|
|
39
|
+
const root = `/projects/${encodeURIComponent(repository.fullName)}`;
|
|
40
|
+
const get = async (path) => (await http.request({ method: 'GET', path: root + path })).data;
|
|
41
|
+
const pages = (path) => http.paginate({ path: root + path, identityKey: 'id' });
|
|
42
|
+
const number = (value) => {
|
|
43
|
+
ensure(Number.isSafeInteger(value) && value > 0 && value <= 1_000_000_000);
|
|
44
|
+
return value;
|
|
45
|
+
};
|
|
46
|
+
function target(value) {
|
|
47
|
+
validateCandidate(value);
|
|
48
|
+
ensure(hash(value.repository) === hash(repository), 'provider-mismatch');
|
|
49
|
+
}
|
|
50
|
+
function project(value) {
|
|
51
|
+
number(value?.id);
|
|
52
|
+
ensure(
|
|
53
|
+
value.path_with_namespace === repository.fullName && value.web_url === repository.url,
|
|
54
|
+
'changed-facts',
|
|
55
|
+
);
|
|
56
|
+
const fields = [
|
|
57
|
+
'id',
|
|
58
|
+
'archived',
|
|
59
|
+
'merge_method',
|
|
60
|
+
'squash_option',
|
|
61
|
+
'merge_pipelines_enabled',
|
|
62
|
+
'merge_trains_enabled',
|
|
63
|
+
'only_allow_merge_if_pipeline_succeeds',
|
|
64
|
+
'allow_merge_on_skipped_pipeline',
|
|
65
|
+
'only_allow_merge_if_all_discussions_are_resolved',
|
|
66
|
+
'only_allow_merge_if_all_status_checks_passed',
|
|
67
|
+
'prevent_merge_without_jira_issue',
|
|
68
|
+
'compliance_frameworks',
|
|
69
|
+
];
|
|
70
|
+
return Object.fromEntries(fields.map((key) => [key, value[key] ?? null]));
|
|
71
|
+
}
|
|
72
|
+
function mergeRequest(value, candidate, expected, projectId) {
|
|
73
|
+
ensure(
|
|
74
|
+
value?.iid === expected &&
|
|
75
|
+
value.project_id === projectId &&
|
|
76
|
+
value.source_project_id === projectId &&
|
|
77
|
+
value.target_project_id === projectId &&
|
|
78
|
+
value.sha === candidate.headSha &&
|
|
79
|
+
value.source_branch === candidate.sourceBranch &&
|
|
80
|
+
value.target_branch === candidate.targetBranch &&
|
|
81
|
+
value.web_url === `${repository.url}/-/merge_requests/${expected}`,
|
|
82
|
+
'changed-facts',
|
|
83
|
+
);
|
|
84
|
+
ensure(
|
|
85
|
+
['opened', 'closed', 'merged', 'locked'].includes(value.state) && typeof value.draft === 'boolean',
|
|
86
|
+
'invalid-provider-evidence',
|
|
87
|
+
);
|
|
88
|
+
// Merged results need only the recorded MR identity and merge commit; branches
|
|
89
|
+
// may already be deleted and mergeability/approval endpoints need not survive.
|
|
90
|
+
if (value.state === 'merged')
|
|
91
|
+
return {
|
|
92
|
+
number: expected,
|
|
93
|
+
state: 'merged',
|
|
94
|
+
url: value.web_url,
|
|
95
|
+
headSha: value.sha,
|
|
96
|
+
commitSha: sha(value.merge_commit_sha),
|
|
97
|
+
};
|
|
98
|
+
ensure(value.diff_refs?.head_sha === candidate.headSha, 'changed-facts');
|
|
99
|
+
return {
|
|
100
|
+
number: expected,
|
|
101
|
+
state: value.state === 'opened' ? 'open' : 'closed',
|
|
102
|
+
url: value.web_url,
|
|
103
|
+
headSha: value.sha,
|
|
104
|
+
commitSha: null,
|
|
105
|
+
draft: value.draft,
|
|
106
|
+
canMerge: value.user?.can_merge === true,
|
|
107
|
+
status: value.detailed_merge_status ?? null,
|
|
108
|
+
pipeline: value.head_pipeline ?? null,
|
|
109
|
+
autoMerge: value.merge_when_pipeline_succeeds ?? null,
|
|
110
|
+
mergeAfter: value.merge_after ?? null,
|
|
111
|
+
squash: value.squash ?? null,
|
|
112
|
+
forceRemoveSourceBranch: value.force_remove_source_branch ?? null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function locate(candidate, projectId, selected) {
|
|
116
|
+
let n = selected ?? candidate.reviewNumber;
|
|
117
|
+
if (n === null || n === undefined) {
|
|
118
|
+
const matches = await pages(
|
|
119
|
+
`/merge_requests?scope=all&state=opened&source_branch=${encodeURIComponent(candidate.sourceBranch)}&target_branch=${encodeURIComponent(candidate.targetBranch)}&per_page=100`,
|
|
120
|
+
);
|
|
121
|
+
ensure(matches.length === 1, 'ambiguous-review');
|
|
122
|
+
n = number(matches[0].iid);
|
|
123
|
+
}
|
|
124
|
+
number(n);
|
|
125
|
+
if (candidate.reviewNumber !== null) ensure(candidate.reviewNumber === n, 'changed-facts');
|
|
126
|
+
return mergeRequest(await get(`/merge_requests/${n}`), candidate, n, projectId);
|
|
127
|
+
}
|
|
128
|
+
async function policy(p, n) {
|
|
129
|
+
return {
|
|
130
|
+
project: p,
|
|
131
|
+
protections: await pages('/protected_branches?per_page=100'),
|
|
132
|
+
config: await get('/approvals'),
|
|
133
|
+
settings: await get('/merge_request_approval_setting'),
|
|
134
|
+
approvals: await get(`/merge_requests/${n}/approval_state`),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function supported(policies, candidate) {
|
|
138
|
+
const { project: p, protections, config: c, settings: s, approvals: a } = policies;
|
|
139
|
+
if (
|
|
140
|
+
p.archived !== false ||
|
|
141
|
+
p.merge_method !== 'merge' ||
|
|
142
|
+
!['never', 'default_off'].includes(p.squash_option) ||
|
|
143
|
+
p.only_allow_merge_if_pipeline_succeeds !== true ||
|
|
144
|
+
![
|
|
145
|
+
'merge_pipelines_enabled',
|
|
146
|
+
'merge_trains_enabled',
|
|
147
|
+
'allow_merge_on_skipped_pipeline',
|
|
148
|
+
'only_allow_merge_if_all_discussions_are_resolved',
|
|
149
|
+
'only_allow_merge_if_all_status_checks_passed',
|
|
150
|
+
'prevent_merge_without_jira_issue',
|
|
151
|
+
].every((key) => p[key] === false) ||
|
|
152
|
+
!Array.isArray(p.compliance_frameworks) ||
|
|
153
|
+
p.compliance_frameworks.length !== 0
|
|
154
|
+
)
|
|
155
|
+
return false;
|
|
156
|
+
// Wildcard and inherited overlapping protection resolution is a separate lane.
|
|
157
|
+
if (
|
|
158
|
+
!Array.isArray(protections) ||
|
|
159
|
+
protections.some((rule) => typeof rule.name !== 'string' || rule.name.includes('*'))
|
|
160
|
+
)
|
|
161
|
+
return false;
|
|
162
|
+
const matching = protections.filter((rule) => rule.name === candidate.targetBranch);
|
|
163
|
+
if (matching.length !== 1) return false;
|
|
164
|
+
const protection = matching[0];
|
|
165
|
+
if (
|
|
166
|
+
protection.allow_force_push !== false ||
|
|
167
|
+
protection.code_owner_approval_required !== false ||
|
|
168
|
+
!Array.isArray(protection.merge_access_levels) ||
|
|
169
|
+
!protection.merge_access_levels.length ||
|
|
170
|
+
!Array.isArray(protection.push_access_levels) ||
|
|
171
|
+
!protection.push_access_levels.length
|
|
172
|
+
)
|
|
173
|
+
return false;
|
|
174
|
+
// Direct pushes could bypass this review lane. Unknown/custom access is blocked.
|
|
175
|
+
if (
|
|
176
|
+
!protection.push_access_levels.every(
|
|
177
|
+
(v) => v.access_level === 0 && !v.user_id && !v.group_id && !v.deploy_key_id && !v.member_role_id,
|
|
178
|
+
) ||
|
|
179
|
+
!protection.merge_access_levels.every((v) => [30, 40].includes(v.access_level) && !v.member_role_id)
|
|
180
|
+
)
|
|
181
|
+
return false;
|
|
182
|
+
if (
|
|
183
|
+
c?.reset_approvals_on_push !== true ||
|
|
184
|
+
c.selective_code_owner_removals !== false ||
|
|
185
|
+
![
|
|
186
|
+
'retain_approvals_on_push',
|
|
187
|
+
'selective_code_owner_removals',
|
|
188
|
+
'allow_overrides_to_approver_list_per_merge_request',
|
|
189
|
+
].every((key) => s?.[key]?.value === false) ||
|
|
190
|
+
c.disable_overriding_approvers_per_merge_request !== true ||
|
|
191
|
+
a?.approval_rules_overwritten !== false ||
|
|
192
|
+
!Array.isArray(a.rules)
|
|
193
|
+
)
|
|
194
|
+
return false;
|
|
195
|
+
const ids = new Set();
|
|
196
|
+
return a.rules.every((r) => {
|
|
197
|
+
if (!Number.isSafeInteger(r.id) || r.id <= 0 || ids.has(r.id)) return false;
|
|
198
|
+
ids.add(r.id);
|
|
199
|
+
return (
|
|
200
|
+
r.rule_type === 'regular' &&
|
|
201
|
+
r.contains_hidden_groups === false &&
|
|
202
|
+
r.overridden === false &&
|
|
203
|
+
Number.isSafeInteger(r.approvals_required) &&
|
|
204
|
+
r.approvals_required >= 0 &&
|
|
205
|
+
typeof r.approved === 'boolean' &&
|
|
206
|
+
Array.isArray(r.eligible_approvers) &&
|
|
207
|
+
Array.isArray(r.approved_by) &&
|
|
208
|
+
[...r.eligible_approvers, ...r.approved_by].every((u) => Number.isSafeInteger(u.id) && u.id > 0)
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async function observe(candidate) {
|
|
213
|
+
target(candidate);
|
|
214
|
+
const p = project(await get('')),
|
|
215
|
+
first = await locate(candidate, p.id);
|
|
216
|
+
const source = await get(`/repository/branches/${encodeURIComponent(candidate.sourceBranch)}`),
|
|
217
|
+
base = await get(`/repository/branches/${encodeURIComponent(candidate.targetBranch)}`);
|
|
218
|
+
ensure(
|
|
219
|
+
source.name === candidate.sourceBranch &&
|
|
220
|
+
source.commit?.id === candidate.headSha &&
|
|
221
|
+
base.name === candidate.targetBranch,
|
|
222
|
+
'changed-facts',
|
|
223
|
+
);
|
|
224
|
+
const baseSha = sha(base.commit?.id);
|
|
225
|
+
let policies = null,
|
|
226
|
+
checks = { policy: 'unknown', satisfied: null, evidenceDigest: hash({ unavailable: true }) },
|
|
227
|
+
reviews = { ...checks };
|
|
228
|
+
try {
|
|
229
|
+
policies = await policy(p, first.number);
|
|
230
|
+
} catch {
|
|
231
|
+
policies = null;
|
|
232
|
+
}
|
|
233
|
+
if (policies && supported(policies, candidate)) {
|
|
234
|
+
const ancestor = await get(`/repository/merge_base?refs[]=${baseSha}&refs[]=${candidate.headSha}`);
|
|
235
|
+
const pipeline = first.pipeline;
|
|
236
|
+
const canMerge =
|
|
237
|
+
first.state === 'open' &&
|
|
238
|
+
!first.draft &&
|
|
239
|
+
first.canMerge &&
|
|
240
|
+
first.status === 'mergeable' &&
|
|
241
|
+
first.autoMerge === false &&
|
|
242
|
+
first.mergeAfter === null &&
|
|
243
|
+
first.squash === false &&
|
|
244
|
+
first.forceRemoveSourceBranch === false;
|
|
245
|
+
checks = {
|
|
246
|
+
policy: 'known',
|
|
247
|
+
satisfied: Boolean(
|
|
248
|
+
canMerge &&
|
|
249
|
+
base.protected === true &&
|
|
250
|
+
ancestor.id === baseSha &&
|
|
251
|
+
pipeline &&
|
|
252
|
+
Number.isSafeInteger(pipeline.id) &&
|
|
253
|
+
pipeline.id > 0 &&
|
|
254
|
+
pipeline.project_id === p.id &&
|
|
255
|
+
pipeline.sha === candidate.headSha &&
|
|
256
|
+
pipeline.status === 'success',
|
|
257
|
+
),
|
|
258
|
+
evidenceDigest: hash({ policies, first, ancestor, protected: base.protected ?? null }),
|
|
259
|
+
};
|
|
260
|
+
reviews = {
|
|
261
|
+
policy: 'known',
|
|
262
|
+
satisfied: policies.approvals.rules.every(
|
|
263
|
+
(r) =>
|
|
264
|
+
r.approved &&
|
|
265
|
+
new Set(
|
|
266
|
+
r.approved_by.filter((u) => r.eligible_approvers.some((e) => e.id === u.id)).map((u) => u.id),
|
|
267
|
+
).size >= r.approvals_required,
|
|
268
|
+
),
|
|
269
|
+
evidenceDigest: hash({ policies, first }),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const final = await locate(candidate, p.id, first.number),
|
|
273
|
+
finalSource = await get(`/repository/branches/${encodeURIComponent(candidate.sourceBranch)}`),
|
|
274
|
+
finalBase = await get(`/repository/branches/${encodeURIComponent(candidate.targetBranch)}`),
|
|
275
|
+
finalProject = project(await get(''));
|
|
276
|
+
ensure(
|
|
277
|
+
hash(first) === hash(final) &&
|
|
278
|
+
hash(p) === hash(finalProject) &&
|
|
279
|
+
finalSource.name === candidate.sourceBranch &&
|
|
280
|
+
finalSource.commit?.id === candidate.headSha &&
|
|
281
|
+
finalBase.name === candidate.targetBranch &&
|
|
282
|
+
finalBase.commit?.id === baseSha &&
|
|
283
|
+
finalBase.protected === base.protected,
|
|
284
|
+
'changed-facts',
|
|
285
|
+
);
|
|
286
|
+
if (policies) ensure(hash(await policy(finalProject, first.number)) === hash(policies), 'changed-facts');
|
|
287
|
+
return plain({
|
|
288
|
+
repositoryUrl: repository.url,
|
|
289
|
+
sourceBranch: candidate.sourceBranch,
|
|
290
|
+
targetBranch: candidate.targetBranch,
|
|
291
|
+
headSha: candidate.headSha,
|
|
292
|
+
baseSha,
|
|
293
|
+
review: {
|
|
294
|
+
number: first.number,
|
|
295
|
+
state: first.state,
|
|
296
|
+
url: first.url,
|
|
297
|
+
headSha: candidate.headSha,
|
|
298
|
+
},
|
|
299
|
+
checks: { headSha: candidate.headSha, ...checks },
|
|
300
|
+
reviews: { headSha: candidate.headSha, ...reviews },
|
|
301
|
+
observedAt: timestamp(clock()),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function operation(input) {
|
|
305
|
+
const op = plain(input);
|
|
306
|
+
target(op.candidate);
|
|
307
|
+
ensure(op.action === 'merge');
|
|
308
|
+
exact(op.payload, ['reviewNumber', 'mergeMethod']);
|
|
309
|
+
number(op.payload.reviewNumber);
|
|
310
|
+
ensure(op.payload.mergeMethod === 'merge');
|
|
311
|
+
return op;
|
|
312
|
+
}
|
|
313
|
+
function receipt(op, result) {
|
|
314
|
+
return plain({
|
|
315
|
+
status: 'succeeded',
|
|
316
|
+
operationDigest: op.digest,
|
|
317
|
+
headSha: op.candidate.headSha,
|
|
318
|
+
evidenceDigest: hash(result),
|
|
319
|
+
resourceUrl: result.url,
|
|
320
|
+
commitSha: result.commitSha,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
async function dispatch(input, context) {
|
|
324
|
+
const op = operation(input),
|
|
325
|
+
deadlineContext = plain(context);
|
|
326
|
+
exact(deadlineContext, ['deadline']);
|
|
327
|
+
const deadline = Date.parse(timestamp(deadlineContext.deadline));
|
|
328
|
+
const timely = () => ensure(Date.parse(timestamp(clock())) < deadline, 'dispatch-expired');
|
|
329
|
+
timely();
|
|
330
|
+
const facts = await observe(op.candidate);
|
|
331
|
+
ensure(
|
|
332
|
+
ready(facts) &&
|
|
333
|
+
facts.review.number === op.payload.reviewNumber &&
|
|
334
|
+
factsDigest(facts) === op.factsDigest,
|
|
335
|
+
'changed-facts',
|
|
336
|
+
);
|
|
337
|
+
const wireBody = createProviderWireBody({
|
|
338
|
+
provider: 'gitlab',
|
|
339
|
+
action: 'merge',
|
|
340
|
+
resourceId: `${repository.fullName}#${op.payload.reviewNumber}`,
|
|
341
|
+
expectedState: 'open',
|
|
342
|
+
expectedVersion: op.candidate.headSha,
|
|
343
|
+
idempotencyKey: op.digest,
|
|
344
|
+
payload: {
|
|
345
|
+
sha: op.candidate.headSha,
|
|
346
|
+
squash: false,
|
|
347
|
+
auto_merge: false,
|
|
348
|
+
should_remove_source_branch: false,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
timely();
|
|
352
|
+
const controller = new AbortController(),
|
|
353
|
+
remaining = deadline - Date.parse(timestamp(clock()));
|
|
354
|
+
ensure(remaining > 0, 'dispatch-expired');
|
|
355
|
+
const timer = setTimeout(() => controller.abort(), remaining);
|
|
356
|
+
let response;
|
|
357
|
+
try {
|
|
358
|
+
response = (
|
|
359
|
+
await http.request({
|
|
360
|
+
method: 'PUT',
|
|
361
|
+
path: `${root}/merge_requests/${op.payload.reviewNumber}/merge`,
|
|
362
|
+
wireBody,
|
|
363
|
+
signal: controller.signal,
|
|
364
|
+
})
|
|
365
|
+
).data;
|
|
366
|
+
} finally {
|
|
367
|
+
clearTimeout(timer);
|
|
368
|
+
}
|
|
369
|
+
const p = project(await get('')),
|
|
370
|
+
sent = mergeRequest(response, op.candidate, op.payload.reviewNumber, p.id);
|
|
371
|
+
ensure(sent.state === 'merged', 'unverified-effect');
|
|
372
|
+
const result = await locate(op.candidate, p.id, op.payload.reviewNumber);
|
|
373
|
+
ensure(result.state === 'merged' && result.commitSha === sent.commitSha, 'unverified-effect');
|
|
374
|
+
return receipt(op, result);
|
|
375
|
+
}
|
|
376
|
+
async function reconcile(input) {
|
|
377
|
+
try {
|
|
378
|
+
const op = operation(input),
|
|
379
|
+
p = project(await get('')),
|
|
380
|
+
result = await locate(op.candidate, p.id, op.payload.reviewNumber);
|
|
381
|
+
return result.state === 'merged'
|
|
382
|
+
? { status: 'succeeded', receipt: receipt(op, result) }
|
|
383
|
+
: { status: 'unknown' };
|
|
384
|
+
} catch {
|
|
385
|
+
return { status: 'unknown' };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return createTrustedDeliveryExecutor({
|
|
389
|
+
provider: 'gitlab',
|
|
390
|
+
capabilities: [{ action: 'merge', conditionalHead: true, reconcile: true }],
|
|
391
|
+
observe,
|
|
392
|
+
dispatch,
|
|
393
|
+
reconcile,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createHostExecution } from '../feature/host-execution.js';
|
|
3
|
+
import { acquireHostRunLock } from '../feature/host-run-lock.js';
|
|
4
|
+
import { resolveExistingFeatureRunPaths } from '../state/paths.js';
|
|
5
|
+
import { loadProjectConfig } from '../config/load.js';
|
|
6
|
+
import { discoverRepositoryRemotes, selectConfiguredRepositoryRemote } from '../repositories/index.js';
|
|
7
|
+
import { immutableJson } from '../clients/contract.js';
|
|
8
|
+
|
|
9
|
+
export class DeliveryPreparationError extends Error {
|
|
10
|
+
constructor(message = 'Delivery requires a verified run and unchanged integration checkout. Inspect work status and verification results.') {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'DeliveryPreparationError';
|
|
13
|
+
this.code = 'ERR_DELIVERY_PREPARATION';
|
|
14
|
+
this.safeMessage = this.message;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function fail() {
|
|
18
|
+
throw new DeliveryPreparationError();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Only the application reads local verification. CLI inputs never stand in for evidence.
|
|
22
|
+
export async function loadDeliveryCandidate({ project, runId, remoteName, gitClient, runner }) {
|
|
23
|
+
const paths = await resolveExistingFeatureRunPaths(project, runId, runner ? { runner } : {});
|
|
24
|
+
if (paths === null) fail();
|
|
25
|
+
const lock = await acquireHostRunLock(paths);
|
|
26
|
+
try {
|
|
27
|
+
const execution = createHostExecution({ gitClient });
|
|
28
|
+
const observed = await execution.status({ project, runId });
|
|
29
|
+
const config = await loadProjectConfig(project);
|
|
30
|
+
const { run, verification, checkout } = observed;
|
|
31
|
+
const source = await gitClient.inspectRepository(project);
|
|
32
|
+
if (
|
|
33
|
+
source.dirty ||
|
|
34
|
+
source.detached ||
|
|
35
|
+
source.headSha !== run.featurePlan.baselineCommit ||
|
|
36
|
+
source.branch !== config.project.repository.defaultBranch
|
|
37
|
+
)
|
|
38
|
+
fail();
|
|
39
|
+
if (
|
|
40
|
+
!observed.deliveryReady ||
|
|
41
|
+
verification.failure !== null ||
|
|
42
|
+
verification.checks.some(
|
|
43
|
+
(check) =>
|
|
44
|
+
check.required &&
|
|
45
|
+
(check.status !== 'passed' || check.exitCode !== 0 || check.executionStatus !== 'success')
|
|
46
|
+
) ||
|
|
47
|
+
!config.quality.commandGates
|
|
48
|
+
.filter((gate) => gate.required)
|
|
49
|
+
.every((gate) =>
|
|
50
|
+
verification.checks.some(
|
|
51
|
+
(check) => check.id === gate.id && check.required && check.status === 'passed'
|
|
52
|
+
)
|
|
53
|
+
) ||
|
|
54
|
+
!run.evidenceRefs.includes(`commit:${checkout.acceptedCommit}`)
|
|
55
|
+
)
|
|
56
|
+
fail();
|
|
57
|
+
const remotes = await discoverRepositoryRemotes(project, runner ? { runner } : {});
|
|
58
|
+
let selected;
|
|
59
|
+
try { selected = selectConfiguredRepositoryRemote(remotes, config.project.repository.remote, remoteName); }
|
|
60
|
+
catch { throw new DeliveryPreparationError('Repository remote is missing, ambiguous or changed. Review setup --remote=<name> or select --remote=<name> for this delivery operation.'); }
|
|
61
|
+
const { remoteName: _remote, ...repository } = selected;
|
|
62
|
+
const final = await gitClient.inspectRepository(checkout.path);
|
|
63
|
+
if (final.dirty || final.headSha !== checkout.acceptedCommit || final.branch !== checkout.branch) fail();
|
|
64
|
+
return immutableJson({
|
|
65
|
+
runId,
|
|
66
|
+
repository,
|
|
67
|
+
sourceBranch: checkout.branch,
|
|
68
|
+
targetBranch: config.project.repository.defaultBranch,
|
|
69
|
+
localVerification: {
|
|
70
|
+
runId,
|
|
71
|
+
status: 'passed',
|
|
72
|
+
headSha: checkout.acceptedCommit,
|
|
73
|
+
evidenceDigest: createHash('sha256')
|
|
74
|
+
.update(
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
proposalDigest: run.proposalDigest,
|
|
77
|
+
verification,
|
|
78
|
+
})
|
|
79
|
+
)
|
|
80
|
+
.digest('hex'),
|
|
81
|
+
verifiedAt: verification.checkedAt,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof DeliveryPreparationError) throw error;
|
|
86
|
+
fail();
|
|
87
|
+
} finally {
|
|
88
|
+
await lock.release();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { ensure } from './contract.js';
|
|
3
|
+
|
|
4
|
+
// Internal argv runner: publication supplies the executable, argv and environment.
|
|
5
|
+
// A killed process group does not prove escaped descendants stopped. Release pipes
|
|
6
|
+
// after a bounded grace and report uncertainty instead of waiting on close forever.
|
|
7
|
+
export async function runPublicationProcess(executable, args, {cwd, env, timeoutMs = 10000, signal, maxOutputBytes = 65536} = {}) {
|
|
8
|
+
ensure(['darwin','linux'].includes(process.platform), 'unsupported-platform');
|
|
9
|
+
ensure(Number.isSafeInteger(timeoutMs) && timeoutMs > 0 && timeoutMs <= 120000);
|
|
10
|
+
ensure(Number.isSafeInteger(maxOutputBytes) && maxOutputBytes > 0 && maxOutputBytes <= 1048576);
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
let child, timer, grace, settled = false, reason = null, stdout = '', size = 0;
|
|
13
|
+
const finish = code => {
|
|
14
|
+
if (settled) return; settled = true;
|
|
15
|
+
clearTimeout(timer); clearTimeout(grace); signal?.removeEventListener('abort', abort);
|
|
16
|
+
resolve({code: code ?? null, reason, cleanup: reason ? 'uncertain' : 'not-required', stdout: reason ? '' : stdout});
|
|
17
|
+
};
|
|
18
|
+
const stop = why => {
|
|
19
|
+
if (reason || settled) return; reason = why;
|
|
20
|
+
try { process.kill(-child.pid, 'SIGKILL'); } catch {}
|
|
21
|
+
grace = setTimeout(() => {child.stdout.destroy(); child.stderr.destroy(); child.unref(); finish(null);},100);
|
|
22
|
+
};
|
|
23
|
+
const abort = () => stop('cancelled');
|
|
24
|
+
if (signal?.aborted) {reason='cancelled';finish(null);return;}
|
|
25
|
+
try {child=spawn(executable,args,{cwd,env,shell:false,detached:true,stdio:['ignore','pipe','pipe']});}
|
|
26
|
+
catch {reason='launch-failed';finish(null);return;}
|
|
27
|
+
timer=setTimeout(()=>stop('timeout'),timeoutMs);signal?.addEventListener('abort',abort,{once:true});
|
|
28
|
+
for (const [stream,capture] of [[child.stdout,true],[child.stderr,false]]) stream.on('data',data=>{
|
|
29
|
+
if(reason) return;size+=data.length;if(size>maxOutputBytes) stop('output-limit');else if(capture) stdout+=data.toString('utf8');
|
|
30
|
+
});
|
|
31
|
+
child.on('error',()=>{reason='launch-failed';finish(null);});child.on('close',finish);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { lstat, realpath, open, readdir, mkdtemp, mkdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { join, dirname, relative, resolve, isAbsolute } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { ensure, hash, sha } from './contract.js';
|
|
5
|
+
import { parseRepositoryRemote } from '../repositories/identity.js';
|
|
6
|
+
import { runPublicationProcess } from './publication-process.js';
|
|
7
|
+
|
|
8
|
+
export function publicationAuthorization(provider, token) {
|
|
9
|
+
const username={github:'x-access-token',gitlab:'oauth2',bitbucket:'x-token-auth'}[provider];
|
|
10
|
+
ensure(username && typeof token==='string' && token.length>0 && token.length<=8192 && !/[\s\u0000-\u001f\u007f]/.test(token),'invalid-credentials');
|
|
11
|
+
return `Authorization: Basic ${Buffer.from(`${username}:${token}`).toString('base64')}`;
|
|
12
|
+
}
|
|
13
|
+
function outside(root,path) {const rel=relative(root,path);return rel==='..'||rel.startsWith('../')||isAbsolute(rel);}
|
|
14
|
+
function identity(stat) {return `${stat.dev}:${stat.ino}`;}
|
|
15
|
+
async function directory(path) {
|
|
16
|
+
ensure(typeof path==='string' && isAbsolute(path) && !/[\u0000-\u001f\u007f:]/.test(path),'unsafe-object-store');
|
|
17
|
+
ensure(await realpath(path)===path,'unsafe-object-store');
|
|
18
|
+
const stat=await lstat(path);ensure(stat.isDirectory()&&!stat.isSymbolicLink(),'unsafe-object-store');return identity(stat);
|
|
19
|
+
}
|
|
20
|
+
async function native(path,project) {
|
|
21
|
+
ensure(isAbsolute(path) && await realpath(path)===path && outside(project,path),'unsafe-git-executable');
|
|
22
|
+
const stat=await lstat(path);ensure(stat.isFile()&&!stat.isSymbolicLink()&&(stat.mode&0o111)!==0,'unsafe-git-executable');
|
|
23
|
+
const handle=await open(path,'r');let magic;
|
|
24
|
+
try {const opened=await handle.stat();ensure(identity(opened)===identity(stat),'unsafe-git-executable');const data=Buffer.alloc(4);ensure((await handle.read(data,0,4,0)).bytesRead===4,'unsafe-git-executable');magic=data.toString('hex');}
|
|
25
|
+
finally {await handle.close();}
|
|
26
|
+
ensure(['7f454c46','cffaedfe','cefaedfe','feedfacf','feedface','cafebabe','bebafeca','cafebabf','bfbafeca'].includes(magic),'unsafe-git-executable');
|
|
27
|
+
return `${identity(stat)}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
|
28
|
+
}
|
|
29
|
+
async function inspectObjects(root) {
|
|
30
|
+
const rootId=await directory(root);let count=0;
|
|
31
|
+
async function visit(path,depth) {
|
|
32
|
+
ensure(depth<=3,'unsafe-object-store');
|
|
33
|
+
for(const entry of await readdir(path,{withFileTypes:true})) {
|
|
34
|
+
ensure(++count<=200000 && !entry.isSymbolicLink(),'unsafe-object-store');
|
|
35
|
+
const child=join(path,entry.name);const stat=await lstat(child);
|
|
36
|
+
ensure(!stat.isSymbolicLink()&&(stat.isFile()||stat.isDirectory()),'unsafe-object-store');
|
|
37
|
+
if(path===join(root,'info')) ensure(!['alternates','http-alternates'].includes(entry.name),'unsafe-object-store');
|
|
38
|
+
if(stat.isDirectory()) await visit(child,depth+1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await visit(root,0);ensure(await directory(root)===rootId,'unsafe-object-store');return rootId;
|
|
42
|
+
}
|
|
43
|
+
export function publicationRef(branch) {
|
|
44
|
+
ensure(typeof branch==='string'&&/^[A-Za-z0-9_][A-Za-z0-9._/-]{0,254}$/.test(branch)&&!branch.includes('..')&&!branch.includes('//')&&!branch.includes('@{')&&!branch.endsWith('/')&&!branch.endsWith('.')&&!branch.split('/').some(p=>p.startsWith('.')||p.endsWith('.lock')),'invalid-branch');
|
|
45
|
+
return `refs/heads/${branch}`;
|
|
46
|
+
}
|
|
47
|
+
export async function createGitPublicationTransport({gitExecutable,sourceObjects,project,repository,token,runner=runPublicationProcess,signal}) {
|
|
48
|
+
ensure(hash(parseRepositoryRemote(repository.url))===hash(repository),'invalid-destination');
|
|
49
|
+
const destination=repository.url+'.git';const projectRoot=await realpath(project);
|
|
50
|
+
const executableId=await native(gitExecutable,projectRoot);
|
|
51
|
+
const objectsId=await inspectObjects(sourceObjects);
|
|
52
|
+
const authorization=publicationAuthorization(repository.provider,token);
|
|
53
|
+
const cleanBase={PATH:'/usr/bin:/bin',LANG:'C',LC_ALL:'C',GIT_CONFIG_NOSYSTEM:'1',GIT_TERMINAL_PROMPT:'0',GIT_ASKPASS:'/usr/bin/false',SSH_ASKPASS:'/usr/bin/false',GIT_ALLOW_PROTOCOL:'https',GIT_PROTOCOL_FROM_USER:'0'};
|
|
54
|
+
const probe=await runner(gitExecutable,['--exec-path'],{cwd:'/',env:{...cleanBase,HOME:'/nonexistent',GIT_CONFIG_GLOBAL:'/dev/null'},timeoutMs:3000,signal});
|
|
55
|
+
ensure(probe.code===0&&!probe.reason&&typeof probe.stdout==='string','unsafe-git-executable');
|
|
56
|
+
const execPath=probe.stdout.trim();const execPathId=await directory(execPath);
|
|
57
|
+
ensure(outside(projectRoot,execPath),'unsafe-git-executable');
|
|
58
|
+
const helpers=[];
|
|
59
|
+
for (const name of ['git','git-send-pack','git-pack-objects','git-remote-http','git-remote-https']) {
|
|
60
|
+
const path=await realpath(join(execPath,name));helpers.push({name,path,identity:await native(path,projectRoot)});
|
|
61
|
+
}
|
|
62
|
+
async function validateTools() {
|
|
63
|
+
ensure(await native(gitExecutable,projectRoot)===executableId && await directory(execPath)===execPathId,'changed-git-executable');
|
|
64
|
+
for(const helper of helpers) ensure(await realpath(join(execPath,helper.name))===helper.path && await native(helper.path,projectRoot)===helper.identity,'changed-git-executable');
|
|
65
|
+
}
|
|
66
|
+
async function withRepository(operation) {
|
|
67
|
+
await validateTools();
|
|
68
|
+
ensure(await inspectObjects(sourceObjects)===objectsId,'changed-object-store');
|
|
69
|
+
const scratch=await realpath(await mkdtemp(join(tmpdir(),'rivet-publish-')));
|
|
70
|
+
try {
|
|
71
|
+
const bare=join(scratch,'repository.git');const empty=join(scratch,'empty');await mkdir(empty);
|
|
72
|
+
const settings=[['credential.helper',''],['core.hooksPath',empty],['http.sslVerify','true'],['http.followRedirects','false'],['http.proxy',''],['http.cookieFile',''],['http.saveCookies','false'],['http.extraHeader',authorization],['protocol.allow','never'],['protocol.https.allow','always'],['push.followTags','false'],['push.gpgSign','false'],['push.recurseSubmodules','no'],['core.sshCommand','/usr/bin/false'],['core.useReplaceRefs','false'],['core.commitGraph','false'],['gc.auto','0'],['maintenance.auto','false']];
|
|
73
|
+
const env={...cleanBase,HOME:scratch,XDG_CONFIG_HOME:empty,GIT_CONFIG_GLOBAL:'/dev/null',GIT_CONFIG_SYSTEM:'/dev/null',GIT_EXEC_PATH:execPath,GIT_TEMPLATE_DIR:empty,GIT_NO_REPLACE_OBJECTS:'1',GIT_NO_LAZY_FETCH:'1',GIT_CONFIG_COUNT:String(settings.length),GIT_ALTERNATE_OBJECT_DIRECTORIES:sourceObjects};
|
|
74
|
+
settings.forEach(([key,value],i)=>{env[`GIT_CONFIG_KEY_${i}`]=key;env[`GIT_CONFIG_VALUE_${i}`]=value;});
|
|
75
|
+
const run=async(args,timeoutMs=10000,deadline)=>{
|
|
76
|
+
await validateTools();
|
|
77
|
+
const boundedTimeout=deadline===undefined?timeoutMs:Math.min(timeoutMs,Date.parse(deadline)-Date.now());
|
|
78
|
+
ensure(boundedTimeout>0,'expired-proposal');
|
|
79
|
+
const result=await runner(gitExecutable,args,{cwd:scratch,env,timeoutMs:boundedTimeout,signal,maxOutputBytes:65536});
|
|
80
|
+
ensure(result.code===0&&!result.reason,'publication-outcome-uncertain');return result.stdout;
|
|
81
|
+
};
|
|
82
|
+
await run(['init','--bare','--template=',bare]);
|
|
83
|
+
return await operation((args,timeout,deadline)=>run(['--git-dir='+bare,...args],timeout,deadline));
|
|
84
|
+
} finally {await rm(scratch,{recursive:true,force:true});}
|
|
85
|
+
}
|
|
86
|
+
async function readRefs(target) {
|
|
87
|
+
ensure(hash(target.repository)===hash(repository),'invalid-destination');
|
|
88
|
+
const source=publicationRef(target.sourceBranch),base=publicationRef(target.targetBranch);ensure(source!==base,'invalid-branch');
|
|
89
|
+
return withRepository(async run=>{
|
|
90
|
+
const output=await run(['ls-remote','--refs','--exit-code',destination,source,base]);
|
|
91
|
+
const refs=new Map();for(const line of output.trim().split('\n').filter(Boolean)) {
|
|
92
|
+
const match=/^([a-f0-9]{40})\t(refs\/heads\/[^\s]+)$/.exec(line);
|
|
93
|
+
ensure(match&&[source,base].includes(match[2])&&!refs.has(match[2]),'invalid-remote-observation');refs.set(match[2],match[1]);
|
|
94
|
+
}
|
|
95
|
+
ensure(refs.has(base),'missing-target-branch');return {sourceSha:refs.get(source)??null,baseSha:refs.get(base)};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function push(target,{deadline}) {
|
|
99
|
+
ensure(hash(target.repository)===hash(repository),'invalid-destination');sha(target.headSha);
|
|
100
|
+
const ref=publicationRef(target.sourceBranch);ensure(ref!==publicationRef(target.targetBranch),'invalid-branch');
|
|
101
|
+
const remaining=()=>Math.min(60000,Date.parse(deadline)-Date.now());ensure(remaining()>0,'expired-proposal');
|
|
102
|
+
return withRepository(async run=>{
|
|
103
|
+
ensure((await run(['cat-file','-t',target.headSha])).trim()==='commit','invalid-source-commit');
|
|
104
|
+
ensure(await directory(sourceObjects)===objectsId,'changed-object-store');
|
|
105
|
+
ensure(remaining()>0,'expired-proposal');
|
|
106
|
+
await run(['push','--porcelain','--no-verify','--no-follow-tags','--recurse-submodules=no','--signed=false',`--force-with-lease=${ref}:`,destination,`${target.headSha}:${ref}`],60000,deadline);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze({readRefs,push});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function validatePublicationExecutable(executable,project) {return native(executable,await realpath(project));}
|