@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.
Files changed (283) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +59 -0
  4. package/bin/cli.js +42 -0
  5. package/dist/agents/backend-django-agent/SKILL.md +35 -0
  6. package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
  7. package/dist/agents/boss-agent/SKILL.md +28 -0
  8. package/dist/agents/bounded-worker/SKILL.md +28 -0
  9. package/dist/agents/data-performance-agent/SKILL.md +32 -0
  10. package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
  11. package/dist/agents/devops-agent/SKILL.md +34 -0
  12. package/dist/agents/engineering-manager/SKILL.md +28 -0
  13. package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
  14. package/dist/agents/frontend-web-agent/SKILL.md +31 -0
  15. package/dist/agents/incident-response-agent/SKILL.md +42 -0
  16. package/dist/agents/jest-agent/SKILL.md +77 -0
  17. package/dist/agents/mobile-agent/SKILL.md +32 -0
  18. package/dist/agents/playwright-agent/SKILL.md +54 -0
  19. package/dist/agents/product-design-manager/SKILL.md +27 -0
  20. package/dist/agents/qa-agent/SKILL.md +29 -0
  21. package/dist/agents/quality-manager/SKILL.md +28 -0
  22. package/dist/agents/vitest-agent/SKILL.md +77 -0
  23. package/dist/governance/pre-push-rules.md +75 -0
  24. package/dist/governance/prompt-hygiene.md +22 -0
  25. package/dist/governance/review-checklist.md +24 -0
  26. package/dist/governance/safety-and-data.md +27 -0
  27. package/dist/governance/usage-rules.md +21 -0
  28. package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
  29. package/dist/mandatory/agentic-goal/SKILL.md +42 -0
  30. package/dist/mandatory/agentic-status/SKILL.md +74 -0
  31. package/dist/mandatory/apply-design/SKILL.md +228 -0
  32. package/dist/mandatory/check-ac/SKILL.md +124 -0
  33. package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
  34. package/dist/mandatory/design/SKILL.md +312 -0
  35. package/dist/mandatory/feature-workflow/SKILL.md +87 -0
  36. package/dist/mandatory/hotfix/SKILL.md +195 -0
  37. package/dist/mandatory/pre-push/SKILL.md +272 -0
  38. package/dist/mandatory/project-context/SKILL.md +180 -0
  39. package/dist/mandatory/release-docs/SKILL.md +191 -0
  40. package/dist/mandatory/review-pr/SKILL.md +323 -0
  41. package/dist/mandatory/security-review/SKILL.md +157 -0
  42. package/dist/skills/api-contract/SKILL.md +77 -0
  43. package/dist/skills/backend-django/SKILL.md +27 -0
  44. package/dist/skills/backend-nestjs/SKILL.md +65 -0
  45. package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
  46. package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
  47. package/dist/skills/database-migration/SKILL.md +97 -0
  48. package/dist/skills/debugging/SKILL.md +32 -0
  49. package/dist/skills/devops-infra/SKILL.md +26 -0
  50. package/dist/skills/documentation/SKILL.md +24 -0
  51. package/dist/skills/frontend-nextjs/SKILL.md +27 -0
  52. package/dist/skills/incident-postmortem/SKILL.md +38 -0
  53. package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
  54. package/dist/skills/mobile-react-native/SKILL.md +37 -0
  55. package/dist/skills/postgres-analytics/SKILL.md +29 -0
  56. package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
  57. package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
  58. package/dist/skills/refactoring/SKILL.md +32 -0
  59. package/dist/skills/sprint-planning/SKILL.md +32 -0
  60. package/dist/skills/testing-quality/SKILL.md +62 -0
  61. package/dist/v2/protocols/agent-orchestration.md +49 -0
  62. package/dist/v2/protocols/delivery.md +45 -0
  63. package/dist/v2/protocols/design-authority.md +29 -0
  64. package/dist/v2/protocols/goal-graph.md +44 -0
  65. package/dist/v2/protocols/pattern-first-development.md +29 -0
  66. package/dist/v2/protocols/qa-evidence.md +29 -0
  67. package/dist/v2/protocols/security.md +29 -0
  68. package/dist/v2/schemas/event.schema.json +86 -0
  69. package/dist/v2/schemas/evidence.schema.json +94 -0
  70. package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
  71. package/dist/v2/schemas/feature-plan.schema.json +95 -0
  72. package/dist/v2/schemas/goal-graph.schema.json +61 -0
  73. package/dist/v2/schemas/integration.schema.json +119 -0
  74. package/dist/v2/schemas/orchestration.schema.json +69 -0
  75. package/dist/v2/schemas/project.schema.json +209 -0
  76. package/dist/v2/schemas/providers.schema.json +142 -0
  77. package/dist/v2/schemas/quality.schema.json +50 -0
  78. package/dist/v2/schemas/work-action.schema.json +44 -0
  79. package/dist/v2/schemas/work-request.schema.json +253 -0
  80. package/dist/v2/templates/evidence/qa-bundle.json +194 -0
  81. package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
  82. package/dist/v2/templates/harness/SKILL.md +90 -0
  83. package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
  84. package/dist/v2/templates/project/.rivet/project.yaml +14 -0
  85. package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
  86. package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
  87. package/package.json +58 -0
  88. package/protocols/agent-orchestration.md +49 -0
  89. package/protocols/delivery.md +45 -0
  90. package/protocols/design-authority.md +29 -0
  91. package/protocols/goal-graph.md +44 -0
  92. package/protocols/pattern-first-development.md +29 -0
  93. package/protocols/qa-evidence.md +29 -0
  94. package/protocols/security.md +29 -0
  95. package/schemas/event.schema.json +86 -0
  96. package/schemas/evidence.schema.json +94 -0
  97. package/schemas/feature-decomposition.schema.json +48 -0
  98. package/schemas/feature-plan.schema.json +95 -0
  99. package/schemas/goal-graph.schema.json +61 -0
  100. package/schemas/integration.schema.json +119 -0
  101. package/schemas/orchestration.schema.json +69 -0
  102. package/schemas/project.schema.json +209 -0
  103. package/schemas/providers.schema.json +142 -0
  104. package/schemas/quality.schema.json +50 -0
  105. package/schemas/work-action.schema.json +44 -0
  106. package/schemas/work-request.schema.json +253 -0
  107. package/src/adapters/confluence.js +138 -0
  108. package/src/adapters/contract.js +657 -0
  109. package/src/adapters/factory.js +122 -0
  110. package/src/adapters/figma.js +178 -0
  111. package/src/adapters/fixtures.js +183 -0
  112. package/src/adapters/github.js +314 -0
  113. package/src/adapters/http.js +704 -0
  114. package/src/adapters/jira.js +195 -0
  115. package/src/adapters/linear.js +171 -0
  116. package/src/adapters/node-transport.js +59 -0
  117. package/src/cli/integration-setup-prompt.js +32 -0
  118. package/src/cli/interrupt.js +25 -0
  119. package/src/cli/main.js +596 -0
  120. package/src/cli/output.js +174 -0
  121. package/src/cli/parse-args.js +246 -0
  122. package/src/cli/project-discovery.js +70 -0
  123. package/src/cli/task-confirmation.js +38 -0
  124. package/src/cli/task-presentation.js +22 -0
  125. package/src/clients/claude.js +214 -0
  126. package/src/clients/codex.js +180 -0
  127. package/src/clients/compatibility.js +23 -0
  128. package/src/clients/contract.js +224 -0
  129. package/src/clients/fake.js +170 -0
  130. package/src/clients/process-runner.js +657 -0
  131. package/src/clients/result-contract.js +78 -0
  132. package/src/commands/delivery-publish.js +80 -0
  133. package/src/commands/delivery-remote.js +230 -0
  134. package/src/commands/delivery-review-update.js +39 -0
  135. package/src/commands/delivery-tracker.js +94 -0
  136. package/src/commands/delivery-transition.js +79 -0
  137. package/src/commands/delivery.js +223 -0
  138. package/src/commands/dependency-approval.js +21 -0
  139. package/src/commands/doctor.js +191 -0
  140. package/src/commands/evidence.js +44 -0
  141. package/src/commands/feature.js +212 -0
  142. package/src/commands/goals.js +231 -0
  143. package/src/commands/human-run.js +208 -0
  144. package/src/commands/human-task.js +278 -0
  145. package/src/commands/init.js +848 -0
  146. package/src/commands/install.js +814 -0
  147. package/src/commands/integration-setup.js +65 -0
  148. package/src/commands/integrations.js +54 -0
  149. package/src/commands/models.js +158 -0
  150. package/src/commands/orchestrate.js +61 -0
  151. package/src/commands/preflight.js +125 -0
  152. package/src/commands/protocols.js +228 -0
  153. package/src/commands/repositories.js +77 -0
  154. package/src/commands/setup-remote.js +41 -0
  155. package/src/commands/setup.js +214 -0
  156. package/src/commands/status.js +224 -0
  157. package/src/commands/support.js +44 -0
  158. package/src/commands/task-completion.js +104 -0
  159. package/src/commands/uninstall.js +234 -0
  160. package/src/commands/verify.js +171 -0
  161. package/src/commands/work.js +220 -0
  162. package/src/commands/worktrees.js +52 -0
  163. package/src/config/command-readiness.js +218 -0
  164. package/src/config/commands.js +206 -0
  165. package/src/config/defaults.js +55 -0
  166. package/src/config/load.js +230 -0
  167. package/src/config/validate.js +560 -0
  168. package/src/delivery/bitbucket-review.js +123 -0
  169. package/src/delivery/branch-publication.js +23 -0
  170. package/src/delivery/contract.js +379 -0
  171. package/src/delivery/github-deployment.js +132 -0
  172. package/src/delivery/github-review.js +5 -0
  173. package/src/delivery/github.js +382 -0
  174. package/src/delivery/gitlab-review.js +5 -0
  175. package/src/delivery/gitlab.js +395 -0
  176. package/src/delivery/prepare.js +90 -0
  177. package/src/delivery/publication-process.js +33 -0
  178. package/src/delivery/publication-transport.js +112 -0
  179. package/src/delivery/review-request.js +184 -0
  180. package/src/delivery/review-update.js +74 -0
  181. package/src/delivery/service.js +400 -0
  182. package/src/delivery/store.js +76 -0
  183. package/src/delivery/tracker-target.js +51 -0
  184. package/src/delivery/tracker-transition.js +158 -0
  185. package/src/delivery/tracker.js +164 -0
  186. package/src/discovery/git.js +85 -0
  187. package/src/discovery/portable.js +108 -0
  188. package/src/discovery/project.js +345 -0
  189. package/src/discovery/tools.js +145 -0
  190. package/src/evaluations/approval.js +20 -0
  191. package/src/evaluations/cost-policy.js +20 -0
  192. package/src/evaluations/harness-attempt.js +113 -0
  193. package/src/evaluations/live-runner.js +55 -0
  194. package/src/evaluations/profile.js +28 -0
  195. package/src/evaluations/report.js +11 -0
  196. package/src/evaluations/text-attempt.js +18 -0
  197. package/src/evidence/checksum.js +87 -0
  198. package/src/evidence/collect.js +727 -0
  199. package/src/evidence/validate.js +354 -0
  200. package/src/feature/accepted-integration.js +46 -0
  201. package/src/feature/actions.js +37 -0
  202. package/src/feature/client-profile.js +56 -0
  203. package/src/feature/decomposition-contract.js +58 -0
  204. package/src/feature/host-execution.js +633 -0
  205. package/src/feature/host-lock-recovery.js +96 -0
  206. package/src/feature/host-run-lock.js +32 -0
  207. package/src/feature/local-approval.js +183 -0
  208. package/src/feature/plan-contract.js +215 -0
  209. package/src/feature/planner.js +253 -0
  210. package/src/feature/run-store.js +241 -0
  211. package/src/feature/runtime-bridge.js +840 -0
  212. package/src/feature/verification-report.js +168 -0
  213. package/src/feature/workflow.js +375 -0
  214. package/src/git/client.js +509 -0
  215. package/src/git/integration-worktree.js +153 -0
  216. package/src/git/reconcile.js +222 -0
  217. package/src/git/reservations.js +450 -0
  218. package/src/git/worktrees.js +478 -0
  219. package/src/graph/completion.js +189 -0
  220. package/src/graph/fixtures.js +116 -0
  221. package/src/graph/reducer.js +124 -0
  222. package/src/graph/scheduler.js +252 -0
  223. package/src/graph/validate.js +106 -0
  224. package/src/install/managed.js +579 -0
  225. package/src/install/project-reference.js +33 -0
  226. package/src/install/project-runtime.js +142 -0
  227. package/src/install/runtime-integrity.cjs +141 -0
  228. package/src/integrations/capabilities.js +69 -0
  229. package/src/integrations/host-observation.js +26 -0
  230. package/src/integrations/registry.js +56 -0
  231. package/src/models/delegate.js +102 -0
  232. package/src/models/profiles.js +53 -0
  233. package/src/models/protocols.js +208 -0
  234. package/src/models/registry.js +83 -0
  235. package/src/models/transport.js +59 -0
  236. package/src/policy/approvals.js +251 -0
  237. package/src/policy/authority.js +282 -0
  238. package/src/policy/budget.js +149 -0
  239. package/src/policy/command-bootstrap.js +219 -0
  240. package/src/policy/commands.js +803 -0
  241. package/src/prompts/launch-contract.js +43 -0
  242. package/src/prompts/planning-contract.js +92 -0
  243. package/src/protocols/presentation.js +53 -0
  244. package/src/protocols/project.js +289 -0
  245. package/src/quality/runner.js +497 -0
  246. package/src/quality/traceability.js +215 -0
  247. package/src/repositories/identity.js +59 -0
  248. package/src/repositories/index.js +2 -0
  249. package/src/repositories/provider.js +210 -0
  250. package/src/runtime/application.js +353 -0
  251. package/src/runtime/dependency-directory.js +33 -0
  252. package/src/runtime/harness-discovery.js +121 -0
  253. package/src/runtime/heartbeat.js +47 -0
  254. package/src/runtime/instance-store.js +112 -0
  255. package/src/runtime/orchestrator.js +1133 -0
  256. package/src/runtime/portable-dependencies.js +94 -0
  257. package/src/runtime/recovery.js +190 -0
  258. package/src/runtime/retry.js +54 -0
  259. package/src/runtime/supervisor.js +166 -0
  260. package/src/runtime/worktree-bootstrap.js +169 -0
  261. package/src/state/event-store.js +333 -0
  262. package/src/state/lock.js +266 -0
  263. package/src/state/paths.js +274 -0
  264. package/src/state/redact.js +106 -0
  265. package/src/state/snapshot-store.js +310 -0
  266. package/src/status/public/app.js +96 -0
  267. package/src/status/public/index.html +37 -0
  268. package/src/status/public/styles.css +50 -0
  269. package/src/status/server.js +323 -0
  270. package/src/status/view-model.js +356 -0
  271. package/src/support/bundle.js +195 -0
  272. package/src/support/failure-report.js +96 -0
  273. package/src/work-request/contract.js +250 -0
  274. package/src/work-request/host.js +78 -0
  275. package/src/work-request/local.js +163 -0
  276. package/src/work-request/tracker.js +105 -0
  277. package/templates/evidence/qa-bundle.json +194 -0
  278. package/templates/github-actions/rivet-deploy.yml +98 -0
  279. package/templates/harness/SKILL.md +90 -0
  280. package/templates/project/.rivet/orchestration.yaml +42 -0
  281. package/templates/project/.rivet/project.yaml +14 -0
  282. package/templates/project/.rivet/providers.yaml +8 -0
  283. package/templates/project/.rivet/quality.yaml +16 -0
@@ -0,0 +1,123 @@
1
+ import { captureRecord, createProviderWireBody } from '../adapters/contract.js';
2
+ import { createProviderHttpClient } from '../adapters/http.js';
3
+ import { parseRepositoryRemote } from '../repositories/identity.js';
4
+ import { createTrustedDeliveryExecutor } from './service.js';
5
+ import { reviewRequestContent } from './review-request.js';
6
+ import { digest, ensure, exact, factsDigest, hash, plain, sha, timestamp, validateCandidate } from './contract.js';
7
+
8
+ const API='https://api.bitbucket.org/2.0';
9
+ const STATES=['OPEN','MERGED','DECLINED','SUPERSEDED'];
10
+ const UUID=/^\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\}$/i;
11
+ // Bitbucket accepts refs rather than an expected commit condition. Creation is
12
+ // confirmed only after exact readback and stable repository/branch observations.
13
+ export function createBitbucketReviewExecutor(inputConfig) {
14
+ const input=captureRecord(inputConfig,new Set(['repository','transport','headers','clock','timeoutMs']),['repository','transport'],'invalid-config');
15
+ const supplied=plain(input.repository),repository=parseRepositoryRemote(supplied.url);
16
+ ensure(repository.provider==='bitbucket'&&hash(repository)===hash(supplied));
17
+ const clock=input.clock??(()=>new Date().toISOString());ensure(typeof clock==='function');
18
+ const http=createProviderHttpClient({provider:'bitbucket',baseUrl:API,transport:input.transport,allowEncodedSlash:true,
19
+ ...(input.headers===undefined?{}:{headers:input.headers}),...(input.timeoutMs===undefined?{}:{timeoutMs:input.timeoutMs})});
20
+ const root=`/repositories/${repository.fullName}`,listPath=root+'/pullrequests';
21
+ const get=async path=>(await http.request({method:'GET',path:root+path})).data;
22
+ function number(value){ensure(Number.isSafeInteger(value)&&value>0,'invalid-provider-evidence');return value;}
23
+ function target(value){validateCandidate(value);ensure(hash(value.repository)===hash(repository),'provider-mismatch');}
24
+ function identity(value,embedded=false){
25
+ ensure(typeof value?.uuid==='string'&&UUID.test(value.uuid)&&value.full_name===repository.fullName&&value.links?.html?.href===repository.url,'changed-facts');
26
+ let workspace=null;
27
+ if(!embedded||value.workspace!==undefined){ensure(typeof value.workspace?.uuid==='string'&&UUID.test(value.workspace.uuid)&&value.workspace.slug===repository.namespace,'changed-facts');workspace={uuid:value.workspace.uuid,slug:value.workspace.slug};}
28
+ return {uuid:value.uuid,fullName:repository.fullName,url:repository.url,workspace};
29
+ }
30
+ async function branches(candidate){
31
+ const project=identity(await get(''));
32
+ const branch=async name=>{
33
+ let result;try{result=await get(`/refs/branches/${encodeURIComponent(name)}`);}catch(error){
34
+ if(error?.status===404){const failure=new Error('Publish the verified source branch and ensure the target branch exists before requesting review.');failure.code='ERR_DELIVERY_BRANCH_NOT_PUBLISHED';failure.safeMessage=failure.message;throw failure;}throw error;
35
+ }
36
+ ensure(result?.name===name,'changed-facts');return sha(result.target?.hash);
37
+ };
38
+ const headSha=await branch(candidate.sourceBranch),baseSha=await branch(candidate.targetBranch);
39
+ ensure(headSha===candidate.headSha,'changed-facts');return {project,headSha,baseSha};
40
+ }
41
+ function rawBody(value){
42
+ const fields=[value.description,value.summary?.raw,value.rendered?.description?.raw].filter(field=>field!==undefined);
43
+ ensure(fields.length>0&&fields.every(field=>typeof field==='string'&&field===fields[0]),'invalid-provider-evidence');
44
+ return fields[0];
45
+ }
46
+ function review(value,candidate,before){
47
+ const id=number(value?.id);
48
+ ensure(STATES.includes(value.state)&&typeof value.draft==='boolean'&&value.links?.html?.href===`${repository.url}/pull-requests/${id}`,'changed-facts');
49
+ for(const [side,name,commit] of [['source',candidate.sourceBranch,candidate.headSha],['destination',candidate.targetBranch,before.baseSha]]){
50
+ const part=value[side],repo=identity(part?.repository,true);
51
+ ensure(repo.uuid===before.project.uuid&&(repo.workspace===null||hash(repo.workspace)===hash(before.project.workspace))
52
+ &&part.branch?.name===name&&part.commit?.hash===commit,'changed-facts');
53
+ }
54
+ ensure(typeof value.title==='string','changed-facts');const body=rawBody(value);
55
+ return plain({number:id,state:value.state==='OPEN'?'open':value.state==='MERGED'?'merged':'closed',draft:value.draft,url:value.links.html.href,
56
+ headSha:candidate.headSha,baseSha:before.baseSha,title:value.title,body});
57
+ }
58
+ // JSON next links are API input: pin all selectors and reject hidden partial
59
+ // responses. Bounds and duplicate IDs apply across all four state queries.
60
+ async function listed(candidate){
61
+ const result=[],ids=new Set(),visited=new Set();let pages=0;
62
+ const q=`source.branch.name=${JSON.stringify(candidate.sourceBranch)} AND destination.branch.name=${JSON.stringify(candidate.targetBranch)}`;
63
+ for(const state of STATES){
64
+ let declaredSize=null,received=0;
65
+ let next=new URL(API+listPath);next.search=new URLSearchParams({q,state,pagelen:'100'}).toString();
66
+ while(next){
67
+ ensure(++pages<=20&&next.origin==='https://api.bitbucket.org'&&next.pathname==='/2.0'+listPath&&!next.username&&!next.password&&!next.hash,'invalid-pagination');
68
+ const params=next.searchParams,keys=[...params.keys()];
69
+ ensure(new Set(keys).size===keys.length&&keys.every(key=>['q','state','pagelen','page'].includes(key))
70
+ &&params.get('q')===q&&params.get('state')===state&&params.get('pagelen')==='100'
71
+ &&(!params.has('page')||/^[1-9][0-9]{0,5}$/.test(params.get('page'))),'invalid-pagination');
72
+ const canonical=new URL(next);canonical.searchParams.sort();ensure(!visited.has(canonical.href),'invalid-pagination');visited.add(canonical.href);
73
+ const requestedPage=Number(params.get('page')??'1');
74
+ const page=(await http.request({method:'GET',path:next.pathname.slice('/2.0'.length)+next.search})).data;
75
+ ensure(Array.isArray(page?.values)&&page.values.length<=100,'invalid-pagination');
76
+ if(page.page!==undefined)ensure(page.page===requestedPage,'invalid-pagination');
77
+ if(page.pagelen!==undefined)ensure(page.pagelen===100,'invalid-pagination');
78
+ if(page.size!==undefined){ensure(Number.isSafeInteger(page.size)&&page.size>=0&&page.size<=1000&&(declaredSize===null||declaredSize===page.size),'invalid-pagination');declaredSize=page.size;}
79
+ received+=page.values.length;ensure(declaredSize===null||received<=declaredSize,'invalid-pagination');
80
+ for(const row of page.values){const id=number(row?.id);ensure(!ids.has(id)&&row.state===state,'invalid-pagination');ids.add(id);result.push(row);ensure(result.length<=1000,'invalid-pagination');}
81
+ if(page.next===undefined||page.next===null){ensure(declaredSize===null||received===declaredSize,'invalid-pagination');break;}
82
+ ensure(page.values.length>0&&typeof page.next==='string'&&page.next.length<=8192,'invalid-pagination');
83
+ try{next=new URL(page.next);}catch{ensure(false,'invalid-pagination');}
84
+ ensure(next.searchParams.get('page')===String(requestedPage+1),'invalid-pagination');
85
+ }
86
+ }
87
+ return result;
88
+ }
89
+ function facts(candidate,before,existing){
90
+ const unknown={headSha:candidate.headSha,policy:'unknown',satisfied:null,evidenceDigest:hash({before,existing})};
91
+ return plain({repositoryUrl:repository.url,sourceBranch:candidate.sourceBranch,targetBranch:candidate.targetBranch,headSha:candidate.headSha,baseSha:before.baseSha,
92
+ review:existing?{number:existing.number,state:existing.state,url:existing.url,headSha:candidate.headSha}:null,checks:unknown,reviews:unknown,observedAt:timestamp(clock())});
93
+ }
94
+ async function observe(candidate){
95
+ target(candidate);const before=await branches(candidate),rows=await listed(candidate);ensure(rows.length<=1,'ambiguous-review');let existing=null;
96
+ if(rows.length){const selected=number(rows[0].id);existing=review(await get(`/pullrequests/${selected}`),candidate,before);ensure(existing.number===selected&&(candidate.reviewNumber===null||candidate.reviewNumber===selected),'changed-facts');}
97
+ else ensure(candidate.reviewNumber===null,'changed-facts');
98
+ ensure(hash(await branches(candidate))===hash(before),'changed-facts');return facts(candidate,before,existing);
99
+ }
100
+ function operation(value){const op=plain(value);target(op.candidate);digest(op.digest);digest(op.factsDigest);ensure(op.action==='review-request'&&op.candidate.reviewNumber===null,'unsupported-action');reviewRequestContent(op);return op;}
101
+ function verified(value,op,before){const result=review(value,op.candidate,before),content=reviewRequestContent(op);ensure(!result.draft&&value.close_source_branch===false&&result.title===content.title&&result.body===content.body,'unverified-effect');return result;}
102
+ function receipt(op,result){return plain({status:'succeeded',operationDigest:op.digest,headSha:op.candidate.headSha,evidenceDigest:hash(result),resourceUrl:result.url,commitSha:null});}
103
+ async function marked(op,before){
104
+ const marker=`<!-- rivet-review-operation:${op.digest} -->`,rows=await listed(op.candidate);
105
+ const matches=rows.filter(row=>rawBody(row).includes(marker));ensure(matches.length===1,'ambiguous-review');
106
+ const selected=number(matches[0].id),result=verified(await get(`/pullrequests/${selected}`),op,before);ensure(result.number===selected,'changed-facts');return result;
107
+ }
108
+ async function dispatch(value,context){
109
+ const op=operation(value),limits=plain(context);exact(limits,['deadline']);const deadline=Date.parse(timestamp(limits.deadline));
110
+ const timely=()=>ensure(Date.parse(timestamp(clock()))<deadline,'dispatch-expired');timely();
111
+ const observed=await observe(op.candidate);ensure(observed.review===null&&factsDigest(observed)===op.factsDigest,'changed-facts');
112
+ const content=reviewRequestContent(op),wireBody=createProviderWireBody({provider:'bitbucket',action:'review-request',resourceId:repository.fullName,expectedState:'absent',expectedVersion:op.candidate.headSha,idempotencyKey:op.digest,
113
+ payload:{title:content.title,description:content.body,source:{branch:{name:op.candidate.sourceBranch}},destination:{branch:{name:op.candidate.targetBranch}},draft:false,close_source_branch:false}});
114
+ timely();const controller=new AbortController(),remaining=deadline-Date.parse(timestamp(clock()));ensure(remaining>0,'dispatch-expired');const timer=setTimeout(()=>controller.abort(),remaining);
115
+ let response;try{response=await http.request({method:'POST',path:listPath,wireBody,signal:controller.signal});}finally{clearTimeout(timer);}
116
+ ensure(response.status===201,'unverified-effect');const after=await branches(op.candidate);
117
+ ensure(hash({before:after,existing:null})===observed.checks.evidenceDigest,'changed-facts');
118
+ const sent=verified(response.data,op,after);ensure(sent.state==='open','unverified-effect');
119
+ const result=await marked(op,after);ensure(hash(sent)===hash(result)&&hash(await branches(op.candidate))===hash(after),'changed-facts');return receipt(op,result);
120
+ }
121
+ async function reconcile(value){try{const op=operation(value),before=await branches(op.candidate);ensure(factsDigest(facts(op.candidate,before,null))===op.factsDigest,'changed-facts');const result=await marked(op,before);ensure(hash(await branches(op.candidate))===hash(before),'changed-facts');return {status:'succeeded',receipt:receipt(op,result)};}catch{return {status:'unknown'};}}
122
+ return createTrustedDeliveryExecutor({provider:'bitbucket',capabilities:[{action:'review-request',conditionalHead:false,verifiesCreatedReview:true,reconcile:true}],observe,dispatch,reconcile});
123
+ }
@@ -0,0 +1,23 @@
1
+ import { createTrustedDeliveryExecutor } from './service.js';
2
+ import { ensure, exact, hash, sha, factsDigest } from './contract.js';
3
+ import { publicationRef } from './publication-transport.js';
4
+ export function publicationPayload(target) {return {destinationUrl:target.repository.url+'.git',ref:publicationRef(target.sourceBranch),headSha:target.headSha};}
5
+ export function createBranchPublicationExecutor({repository,transport}) {
6
+ ensure(transport&&typeof transport.readRefs==='function'&&typeof transport.push==='function');
7
+ const validate=op=>{ensure(op.action==='branch-publish'&&hash(op.candidate.repository)===hash(repository));ensure(hash(op.payload)===hash(publicationPayload(op.candidate)),'invalid-publication');};
8
+ const inspect=async target=>{
9
+ ensure(hash(target.repository)===hash(repository));const refs=await transport.readRefs(target);exact(refs,['sourceSha','baseSha']);sha(refs.baseSha);if(refs.sourceSha!==null)sha(refs.sourceSha);return refs;
10
+ };
11
+ const receipt=(op,refs)=>({status:'succeeded',operationDigest:op.digest,headSha:op.candidate.headSha,commitSha:op.candidate.headSha,resourceUrl:repository.url,evidenceDigest:hash({assurance:'publication-requirement-satisfied',publication:op.payload,remoteSha:refs.sourceSha})});
12
+ async function observe(target) {
13
+ const refs=await inspect(target);const policy={headSha:target.headSha,policy:'unknown',satisfied:null,evidenceDigest:hash({publication:refs})};
14
+ return {repositoryUrl:repository.url,sourceBranch:target.sourceBranch,targetBranch:target.targetBranch,headSha:target.headSha,baseSha:refs.baseSha,review:null,checks:policy,reviews:policy,observedAt:new Date().toISOString(),publication:{destinationUrl:repository.url+'.git',ref:publicationRef(target.sourceBranch),remoteSha:refs.sourceSha}};
15
+ }
16
+ return createTrustedDeliveryExecutor({provider:repository.provider,capabilities:[{action:'branch-publish',conditionalHead:true,reconcile:true}],observe,
17
+ async dispatch(op,context) {
18
+ validate(op);const current=await observe(op.candidate);ensure(current.publication.remoteSha===null,'branch-exists');ensure(factsDigest(current)===op.factsDigest,'changed-facts');
19
+ await transport.push(op.candidate,context);const refs=await inspect(op.candidate);ensure(refs.sourceSha===op.candidate.headSha,'publication-outcome-uncertain');return receipt(op,refs);
20
+ },
21
+ async reconcile(op) {validate(op);const refs=await inspect(op.candidate);return refs.sourceSha===op.candidate.headSha?{status:'succeeded',receipt:receipt(op,refs)}:{status:'unknown'};},
22
+ });
23
+ }
@@ -0,0 +1,379 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { containsSecretMaterial } from '../clients/contract.js';
3
+ import { parseRepositoryRemote } from '../repositories/identity.js';
4
+
5
+ export const ACTIONS = Object.freeze(['branch-publish', 'review-request', 'review-update', 'merge', 'deploy', 'tracker-update', 'tracker-transition']);
6
+ export const STAGES = Object.freeze([
7
+ 'locally-verified',
8
+ 'branch-published',
9
+ 'review-requested',
10
+ 'checks-passed',
11
+ 'merge-approved',
12
+ 'merged',
13
+ 'deployed',
14
+ 'tracker-updated',
15
+ 'tracker-status-confirmed',
16
+ ]);
17
+ export class DeliveryError extends Error {
18
+ constructor(reason = 'invalid-input') {
19
+ super(`Delivery operation failed: ${reason}.`);
20
+ this.name = 'DeliveryError';
21
+ this.code = `ERR_DELIVERY_${reason.replaceAll('-', '_').toUpperCase()}`;
22
+ this.safeMessage = this.message;
23
+ }
24
+ }
25
+ export function fail(reason) {
26
+ throw new DeliveryError(reason);
27
+ }
28
+ export function ensure(value, reason = 'invalid-input') {
29
+ if (!value) fail(reason);
30
+ }
31
+ export function plain(input) {
32
+ let nodes = 0;
33
+ function copy(value, depth) {
34
+ ensure(++nodes <= 10000 && depth <= 20);
35
+ if (value === null || typeof value === 'boolean') return value;
36
+ if (typeof value === 'string') {
37
+ ensure(value.length <= 65536 && !containsSecretMaterial(value));
38
+ return value;
39
+ }
40
+ if (typeof value === 'number') {
41
+ ensure(Number.isSafeInteger(value));
42
+ return value;
43
+ }
44
+ ensure(value && typeof value === 'object');
45
+ const array = Array.isArray(value);
46
+ ensure(
47
+ Object.getPrototypeOf(value) === (array ? Array.prototype : Object.prototype) ||
48
+ (!array && Object.getPrototypeOf(value) === null)
49
+ );
50
+ const out = array ? [] : {};
51
+ for (const key of Reflect.ownKeys(value)) {
52
+ if (array && key === 'length') continue;
53
+ ensure(typeof key === 'string' && key !== '__proto__');
54
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
55
+ ensure(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
56
+ if (array) ensure(/^(?:0|[1-9][0-9]*)$/.test(key) && Number(key) < value.length);
57
+ else ensure(!containsSecretMaterial(`${key}: sensitive-value`));
58
+ out[key] = copy(descriptor.value, depth + 1);
59
+ }
60
+ if (array) ensure(out.length === value.length && Object.keys(out).length === value.length);
61
+ return Object.freeze(out);
62
+ }
63
+ const result = copy(input, 0);
64
+ ensure(Buffer.byteLength(JSON.stringify(result)) <= 262144);
65
+ return result;
66
+ }
67
+ export function exact(value, keys, optional = []) {
68
+ ensure(value && typeof value === 'object' && !Array.isArray(value));
69
+ ensure(
70
+ Object.keys(value).every((key) => keys.includes(key) || optional.includes(key)) &&
71
+ keys.every((key) => Object.hasOwn(value, key))
72
+ );
73
+ return value;
74
+ }
75
+ export function id(value) {
76
+ ensure(typeof value === 'string' && /^[a-z][a-z0-9-]{0,63}$/.test(value));
77
+ return value;
78
+ }
79
+ export function sha(value) {
80
+ ensure(typeof value === 'string' && /^[a-f0-9]{40}$/.test(value));
81
+ return value;
82
+ }
83
+ export function digest(value) {
84
+ ensure(typeof value === 'string' && /^[a-f0-9]{64}$/.test(value));
85
+ return value;
86
+ }
87
+ export function timestamp(value) {
88
+ ensure(
89
+ typeof value === 'string' &&
90
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) &&
91
+ Number.isFinite(Date.parse(value)) &&
92
+ new Date(value).toISOString() === value
93
+ );
94
+ return value;
95
+ }
96
+ export function ref(value) {
97
+ ensure(
98
+ typeof value === 'string' &&
99
+ /^[A-Za-z0-9_][A-Za-z0-9._/-]{0,254}$/.test(value) &&
100
+ !value.includes('..') &&
101
+ !value.includes('//')
102
+ );
103
+ return value;
104
+ }
105
+ export function hash(value) {
106
+ const canonical = (item) =>
107
+ item === null || typeof item !== 'object'
108
+ ? JSON.stringify(item)
109
+ : Array.isArray(item)
110
+ ? `[${item.map(canonical).join(',')}]`
111
+ : `{${Object.keys(item)
112
+ .sort()
113
+ .map((key) => `${JSON.stringify(key)}:${canonical(item[key])}`)
114
+ .join(',')}}`;
115
+ return createHash('sha256').update(canonical(value)).digest('hex');
116
+ }
117
+ export function candidate(input) {
118
+ const value = plain(input);
119
+ exact(
120
+ value,
121
+ ['runId', 'repository', 'sourceBranch', 'targetBranch', 'localVerification'],
122
+ ['reviewNumber']
123
+ );
124
+ id(value.runId);
125
+ ref(value.sourceBranch);
126
+ ref(value.targetBranch);
127
+ const repository = parseRepositoryRemote(value.repository.url);
128
+ ensure(hash(repository) === hash(value.repository));
129
+ const proof = exact(value.localVerification, [
130
+ 'runId',
131
+ 'headSha',
132
+ 'evidenceDigest',
133
+ 'verifiedAt',
134
+ 'status',
135
+ ]);
136
+ ensure(proof.runId === value.runId && proof.status === 'passed');
137
+ sha(proof.headSha);
138
+ digest(proof.evidenceDigest);
139
+ timestamp(proof.verifiedAt);
140
+ ensure(
141
+ value.reviewNumber === undefined || (Number.isSafeInteger(value.reviewNumber) && value.reviewNumber > 0)
142
+ );
143
+ return plain({ ...value, headSha: proof.headSha, reviewNumber: value.reviewNumber ?? null });
144
+ }
145
+ export function validateCandidate(value) {
146
+ const { headSha, reviewNumber, ...input } = value;
147
+ const result = candidate({ ...input, ...(reviewNumber === null ? {} : { reviewNumber }) });
148
+ ensure(result.headSha === headSha);
149
+ return value;
150
+ }
151
+ export function observation(input, target) {
152
+ const value = plain(input);
153
+ exact(value, [
154
+ 'repositoryUrl',
155
+ 'sourceBranch',
156
+ 'targetBranch',
157
+ 'headSha',
158
+ 'baseSha',
159
+ 'review',
160
+ 'checks',
161
+ 'reviews',
162
+ 'observedAt',
163
+ ], ['publication']);
164
+ if (value.publication !== undefined) {
165
+ const p = exact(value.publication, ['destinationUrl', 'ref', 'remoteSha']);
166
+ ensure(p.destinationUrl === target.repository.url + '.git' && p.ref === `refs/heads/${target.sourceBranch}`, 'changed-facts');
167
+ ensure(p.remoteSha === null || sha(p.remoteSha), 'changed-facts');
168
+ }
169
+ timestamp(value.observedAt);
170
+ sha(value.baseSha);
171
+ ensure(
172
+ value.repositoryUrl === target.repository.url &&
173
+ value.sourceBranch === target.sourceBranch &&
174
+ value.targetBranch === target.targetBranch &&
175
+ value.headSha === target.headSha,
176
+ 'changed-facts'
177
+ );
178
+ if (value.review !== null) {
179
+ const review = exact(value.review, ['number', 'state', 'url', 'headSha']);
180
+ ensure(
181
+ Number.isSafeInteger(review.number) &&
182
+ review.number > 0 &&
183
+ (target.reviewNumber === null || review.number === target.reviewNumber)
184
+ );
185
+ ensure(
186
+ ['open', 'closed', 'merged'].includes(review.state) && review.headSha === target.headSha,
187
+ 'changed-facts'
188
+ );
189
+ const segment = { github: 'pull', bitbucket: 'pull-requests', gitlab: '-/merge_requests' }[
190
+ target.repository.provider
191
+ ];
192
+ ensure(review.url === `${target.repository.url}/${segment}/${review.number}`);
193
+ }
194
+ for (const key of ['checks', 'reviews']) {
195
+ const policy = exact(value[key], ['headSha', 'policy', 'satisfied', 'evidenceDigest']);
196
+ ensure(policy.headSha === target.headSha, 'changed-facts');
197
+ digest(policy.evidenceDigest);
198
+ ensure(
199
+ policy.policy === 'unknown'
200
+ ? policy.satisfied === null
201
+ : policy.policy === 'known' && typeof policy.satisfied === 'boolean'
202
+ );
203
+ }
204
+ return value;
205
+ }
206
+ export function ready(value) {
207
+ return (
208
+ value?.review?.state === 'open' &&
209
+ ['checks', 'reviews'].every((key) => value[key].policy === 'known' && value[key].satisfied === true)
210
+ );
211
+ }
212
+ export function factsDigest(value) {
213
+ const { observedAt, ...facts } = value;
214
+ return hash(facts);
215
+ }
216
+ export function validateReceipt(input, operation) {
217
+ const value = plain(input);
218
+ exact(value, ['status', 'operationDigest', 'headSha', 'evidenceDigest', 'resourceUrl', 'commitSha']);
219
+ ensure(
220
+ value.status === 'succeeded' &&
221
+ value.operationDigest === operation.digest &&
222
+ value.headSha === operation.candidate.headSha,
223
+ 'invalid-receipt'
224
+ );
225
+ digest(value.evidenceDigest);
226
+ let url;
227
+ try {
228
+ url = new URL(value.resourceUrl);
229
+ } catch {
230
+ fail('invalid-receipt');
231
+ }
232
+ ensure(
233
+ url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash,
234
+ 'invalid-receipt'
235
+ );
236
+ ensure(value.commitSha === null || sha(value.commitSha));
237
+ if (operation.action === 'merge') sha(value.commitSha);
238
+ if (operation.action === 'branch-publish') ensure(value.commitSha === operation.candidate.headSha && value.resourceUrl === operation.candidate.repository.url, 'invalid-receipt');
239
+ return value;
240
+ }
241
+ export function mergeReceiptFor(operations, action) {
242
+ if (!['deploy', 'tracker-update', 'tracker-transition'].includes(action)) return null;
243
+ const merge = operations.find(
244
+ (operation) => operation.action === 'merge' && operation.state === 'succeeded'
245
+ );
246
+ ensure(merge?.receipt?.commitSha, 'missing-merge-receipt');
247
+ return merge.receipt;
248
+ }
249
+
250
+ function publicationPayload(value, target) {
251
+ exact(value, ['destinationUrl', 'ref', 'headSha']);
252
+ ensure(value.destinationUrl === target.repository.url + '.git' && value.ref === `refs/heads/${target.sourceBranch}` && value.headSha === target.headSha, 'invalid-publication');
253
+ }
254
+
255
+ export function validateRecord(input) {
256
+ const value = plain(input);
257
+ exact(value, [
258
+ 'schemaVersion',
259
+ 'stage',
260
+ 'candidate',
261
+ 'observation',
262
+ 'proposal',
263
+ 'operations',
264
+ 'usedApprovalIds',
265
+ 'createdAt',
266
+ 'updatedAt',
267
+ ]);
268
+ ensure(value.schemaVersion === 1 && STAGES.includes(value.stage));
269
+ validateCandidate(value.candidate);
270
+ timestamp(value.createdAt);
271
+ timestamp(value.updatedAt);
272
+ ensure(value.updatedAt >= value.createdAt);
273
+ if (value.observation !== null) observation(value.observation, value.candidate);
274
+ ensure(
275
+ Array.isArray(value.operations) &&
276
+ value.operations.length <= 64 &&
277
+ Array.isArray(value.usedApprovalIds) &&
278
+ value.usedApprovalIds.length <= 64 &&
279
+ new Set(value.usedApprovalIds).size === value.usedApprovalIds.length
280
+ );
281
+ for (const operation of value.operations) {
282
+ exact(operation, [
283
+ 'digest',
284
+ 'action',
285
+ 'payload',
286
+ 'candidate',
287
+ 'factsDigest',
288
+ 'mergeReceipt',
289
+ 'approvalId',
290
+ 'dispatchedAt',
291
+ 'state',
292
+ 'receipt',
293
+ ]);
294
+ ensure(
295
+ hash(operation.mergeReceipt) ===
296
+ hash(
297
+ mergeReceiptFor(value.operations.slice(0, value.operations.indexOf(operation)), operation.action)
298
+ ),
299
+ 'merge-receipt-mismatch'
300
+ );
301
+ if (operation.action === 'branch-publish') publicationPayload(operation.payload, value.candidate);
302
+ digest(operation.digest);
303
+ digest(operation.factsDigest);
304
+ id(operation.approvalId);
305
+ timestamp(operation.dispatchedAt);
306
+ ensure(
307
+ ACTIONS.includes(operation.action) &&
308
+ ['dispatching', 'indeterminate', 'succeeded', 'not-applied'].includes(operation.state)
309
+ );
310
+ ensure(
311
+ hash(operation.candidate) === hash(value.candidate) &&
312
+ value.usedApprovalIds.includes(operation.approvalId)
313
+ );
314
+ if (operation.receipt !== null) {
315
+ ensure(operation.state === 'succeeded');
316
+ validateReceipt(operation.receipt, operation);
317
+ } else ensure(operation.state !== 'succeeded');
318
+ }
319
+ if (value.proposal !== null) {
320
+ const p = exact(value.proposal, [
321
+ 'digest',
322
+ 'action',
323
+ 'payload',
324
+ 'expiresAt',
325
+ 'factsDigest',
326
+ 'nonce',
327
+ 'resource',
328
+ 'approvalResource',
329
+ 'providerId',
330
+ 'mergeReceipt',
331
+ ]);
332
+ ensure(
333
+ hash(p.mergeReceipt) === hash(mergeReceiptFor(value.operations, p.action)),
334
+ 'merge-receipt-mismatch'
335
+ );
336
+ if (p.action === 'branch-publish') publicationPayload(p.payload, value.candidate);
337
+ digest(p.digest);
338
+ digest(p.factsDigest);
339
+ timestamp(p.expiresAt);
340
+ id(p.providerId);
341
+ ensure(ACTIONS.includes(p.action) && typeof p.nonce === 'string');
342
+ const body = {
343
+ candidate: value.candidate,
344
+ action: p.action,
345
+ payload: p.payload,
346
+ expiresAt: p.expiresAt,
347
+ factsDigest: p.factsDigest,
348
+ nonce: p.nonce,
349
+ providerId: p.providerId,
350
+ mergeReceipt: p.mergeReceipt,
351
+ };
352
+ ensure(
353
+ hash(body) === p.digest &&
354
+ p.resource === `delivery:${p.digest}` &&
355
+ p.approvalResource === `${p.providerId}:${p.action}:${p.resource}`
356
+ );
357
+ }
358
+ const success = (action) => value.operations.some((op) => op.action === action && op.state === 'succeeded');
359
+ if (['merged', 'deployed', 'tracker-updated', 'tracker-status-confirmed'].includes(value.stage)) ensure(success('merge'));
360
+ if (value.stage === 'branch-published') ensure(success('branch-publish'));
361
+ if (value.stage === 'deployed') ensure(success('deploy'));
362
+ if (value.stage === 'tracker-status-confirmed') ensure(success('tracker-transition'));
363
+ if (value.stage === 'tracker-updated') ensure(success('tracker-update'));
364
+ if (value.stage === 'merge-approved')
365
+ ensure(
366
+ value.operations.some(
367
+ (op) => op.action === 'merge' && ['dispatching', 'indeterminate', 'not-applied'].includes(op.state)
368
+ )
369
+ );
370
+ if (value.stage === 'review-requested')
371
+ ensure((value.observation?.review !== null && value.observation !== null) || success('review-request'));
372
+ ensure(
373
+ new Set(value.operations.map((op) => op.digest)).size === value.operations.length &&
374
+ new Set(value.operations.map((op) => op.approvalId)).size === value.operations.length &&
375
+ value.usedApprovalIds.length === value.operations.length
376
+ );
377
+ if (value.stage === 'checks-passed') ensure(ready(value.observation));
378
+ return value;
379
+ }
@@ -0,0 +1,132 @@
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 { digest, ensure, exact, factsDigest, hash, id, plain, sha, timestamp, validateCandidate, validateReceipt } from './contract.js';
6
+
7
+ export function deploymentPayload(config) {
8
+ const value = plain(config);
9
+ exact(value, ['providerId', 'workflow', 'environment', 'productionEnvironment']);
10
+ id(value.providerId);
11
+ ensure(typeof value.workflow === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,119}\.ya?ml$/.test(value.workflow));
12
+ ensure(typeof value.environment === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(value.environment));
13
+ ensure(typeof value.productionEnvironment === 'boolean');
14
+ return plain({ workflow: value.workflow, environment: value.environment, productionEnvironment: value.productionEnvironment });
15
+ }
16
+
17
+ // The configured project workflow is trusted to perform deployment and verify its
18
+ // result. GitHub run success proves that workflow's result, not generic app health.
19
+ export function createGithubDeploymentExecutor(inputConfig) {
20
+ const input = captureRecord(inputConfig,
21
+ new Set(['repository', 'deployment', 'mergeReceipt', 'transport', 'headers', 'clock', 'timeoutMs']),
22
+ ['repository', 'deployment', 'mergeReceipt', 'transport'], 'invalid-config');
23
+ const supplied = plain(input.repository), repository = parseRepositoryRemote(supplied.url);
24
+ ensure(repository.provider === 'github' && hash(repository) === hash(supplied));
25
+ const payload = deploymentPayload(input.deployment), merged = plain(input.mergeReceipt);
26
+ validateReceipt(merged, {action: 'merge', digest: digest(merged.operationDigest), candidate: {headSha: sha(merged.headSha)}});
27
+ const prPrefix = `${repository.url}/pull/`;
28
+ ensure(merged.resourceUrl.startsWith(prPrefix));
29
+ const pullNumber = Number(merged.resourceUrl.slice(prPrefix.length));
30
+ ensure(Number.isSafeInteger(pullNumber) && pullNumber > 0 && pullNumber <= 1_000_000_000
31
+ && merged.resourceUrl === `${prPrefix}${pullNumber}`);
32
+ const clock = input.clock ?? (() => new Date().toISOString());
33
+ ensure(typeof clock === 'function');
34
+ const http = createProviderHttpClient({provider: 'github', baseUrl: 'https://api.github.com', transport: input.transport,
35
+ ...(input.headers === undefined ? {} : {headers: input.headers}),
36
+ ...(input.timeoutMs === undefined ? {} : {timeoutMs: input.timeoutMs})});
37
+ const root = `/repos/${repository.fullName}`;
38
+ const get = async path => (await http.request({method: 'GET', path: root + path})).data;
39
+ const pages = path => http.paginate({path: root + path, identityKey: 'id'});
40
+ function target(candidate) {
41
+ validateCandidate(candidate);
42
+ ensure(hash(candidate.repository) === hash(repository) && candidate.headSha === merged.headSha, 'changed-facts');
43
+ ensure(candidate.reviewNumber === null || candidate.reviewNumber === pullNumber, 'changed-facts');
44
+ }
45
+ async function workflow() {
46
+ const value = await get(`/actions/workflows/${encodeURIComponent(payload.workflow)}`);
47
+ ensure(Number.isSafeInteger(value?.id) && value.id > 0 && value.state === 'active'
48
+ && value.path === `.github/workflows/${payload.workflow}`, 'changed-facts');
49
+ const file = await get(`/contents/.github/workflows/${encodeURIComponent(payload.workflow)}?ref=${merged.commitSha}`);
50
+ ensure(file?.type === 'file' && file.path === value.path, 'changed-facts');
51
+ return {id: value.id, path: value.path, blobSha: sha(file.sha), state: value.state};
52
+ }
53
+ async function observe(candidate) {
54
+ target(candidate);
55
+ const pull = await get(`/pulls/${pullNumber}`);
56
+ ensure(pull?.number === pullNumber && pull.state === 'closed' && pull.merged === true
57
+ && pull.merge_commit_sha === merged.commitSha && pull.html_url === merged.resourceUrl
58
+ && pull.head?.sha === candidate.headSha && pull.head?.ref === candidate.sourceBranch
59
+ && pull.head?.repo?.full_name === repository.fullName && pull.base?.ref === candidate.targetBranch
60
+ && pull.base?.repo?.full_name === repository.fullName, 'changed-facts');
61
+ const configured = await workflow();
62
+ const evidence = hash({workflow: configured, payload, mergeReceipt: merged});
63
+ return plain({repositoryUrl: repository.url, sourceBranch: candidate.sourceBranch, targetBranch: candidate.targetBranch,
64
+ headSha: candidate.headSha, baseSha: sha(pull.base.sha),
65
+ review: {number: pullNumber, state: 'merged', url: merged.resourceUrl, headSha: candidate.headSha},
66
+ checks: {headSha: candidate.headSha, policy: 'unknown', satisfied: null, evidenceDigest: evidence},
67
+ reviews: {headSha: candidate.headSha, policy: 'unknown', satisfied: null, evidenceDigest: evidence},
68
+ observedAt: timestamp(clock())});
69
+ }
70
+ function operation(input) {
71
+ const op = plain(input);
72
+ target(op.candidate);
73
+ digest(op.digest);
74
+ ensure(op.action === 'deploy' && hash(op.payload) === hash(payload) && hash(op.mergeReceipt) === hash(merged), 'changed-facts');
75
+ return op;
76
+ }
77
+ async function reconcile(input) {
78
+ try {
79
+ const op = operation(input), configured = await workflow();
80
+ const matches = (await pages(`/deployments?sha=${merged.commitSha}&task=rivet-deploy&environment=${encodeURIComponent(payload.environment)}&per_page=100`))
81
+ .filter(item => item?.payload?.rivetOperation === op.digest);
82
+ ensure(matches.length === 1, 'unverified-effect');
83
+ const deployment = matches[0];
84
+ ensure(Number.isSafeInteger(deployment.id) && deployment.id > 0 && deployment.sha === merged.commitSha
85
+ && deployment.ref === merged.commitSha && deployment.task === 'rivet-deploy'
86
+ && deployment.environment === payload.environment && deployment.production_environment === payload.productionEnvironment
87
+ && deployment.transient_environment === false && deployment.repository_url === `https://api.github.com${root}`
88
+ && deployment.payload.workflow === payload.workflow, 'unverified-effect');
89
+ exact(deployment.payload, ['rivetOperation', 'workflow']);
90
+ const statuses = await pages(`/deployments/${deployment.id}/statuses?per_page=100`);
91
+ ensure(statuses.length > 0 && statuses.every(item => Number.isSafeInteger(item.id) && item.id > 0), 'unverified-effect');
92
+ const latest = statuses.reduce((a,b) => a.id > b.id ? a : b);
93
+ ensure(latest.state === 'success' && latest.environment === payload.environment, 'unverified-effect');
94
+ const runPrefix = `${repository.url}/actions/runs/`, runId = Number(String(latest.log_url).slice(runPrefix.length));
95
+ ensure(Number.isSafeInteger(runId) && runId > 0 && latest.log_url === `${runPrefix}${runId}`, 'unverified-effect');
96
+ const run = await get(`/actions/runs/${runId}`);
97
+ ensure(run?.id === runId && run.status === 'completed' && run.conclusion === 'success'
98
+ && run.head_sha === merged.commitSha && run.workflow_id === configured.id && run.event === 'deployment'
99
+ && run.repository?.full_name === repository.fullName && run.display_title === `rivet-deploy:${op.digest}`
100
+ && run.html_url === latest.log_url, 'unverified-effect');
101
+ return {status: 'succeeded', receipt: plain({status: 'succeeded', operationDigest: op.digest,
102
+ headSha: op.candidate.headSha, commitSha: merged.commitSha, resourceUrl: run.html_url,
103
+ evidenceDigest: hash({deployment, latest, run, workflow: configured})})};
104
+ } catch {
105
+ return {status: 'unknown'};
106
+ }
107
+ }
108
+ async function dispatch(input, context) {
109
+ const op = operation(input), ctx = plain(context);
110
+ exact(ctx, ['deadline']);
111
+ const deadline = Date.parse(timestamp(ctx.deadline));
112
+ const timely = () => ensure(Date.parse(timestamp(clock())) < deadline, 'dispatch-expired');
113
+ timely();
114
+ ensure(factsDigest(await observe(op.candidate)) === op.factsDigest, 'changed-facts');
115
+ const wireBody = createProviderWireBody({provider: 'github', action: 'deploy', resourceId: repository.fullName,
116
+ expectedState: 'merged', expectedVersion: merged.commitSha, idempotencyKey: op.digest,
117
+ payload: {ref: merged.commitSha, auto_merge: false, task: 'rivet-deploy', environment: payload.environment,
118
+ production_environment: payload.productionEnvironment, transient_environment: false,
119
+ payload: {rivetOperation: op.digest, workflow: payload.workflow}}});
120
+ timely();
121
+ const controller = new AbortController(), remaining = deadline - Date.parse(timestamp(clock()));
122
+ ensure(remaining > 0, 'dispatch-expired');
123
+ const timer = setTimeout(() => controller.abort(), remaining);
124
+ try {
125
+ await http.request({method: 'POST', path: `${root}/deployments`, wireBody, signal: controller.signal});
126
+ } finally { clearTimeout(timer); }
127
+ const result = await reconcile(op);
128
+ ensure(result.status === 'succeeded', 'unverified-effect');
129
+ return result.receipt;
130
+ }
131
+ return createTrustedDeliveryExecutor({provider:'github',capabilities:[{action:'deploy',conditionalHead:true,reconcile:true}],observe,dispatch,reconcile});
132
+ }