@mannyc1/ts-release 0.0.6 → 0.2.2

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 (639) hide show
  1. package/ARCHITECTURE.md +124 -87
  2. package/CHANGELOG.md +115 -0
  3. package/LICENSE +21 -0
  4. package/README.md +272 -540
  5. package/SPEC.md +206 -305
  6. package/apps/release-ts/README.md +32 -0
  7. package/apps/release-ts/src/cli/command.ts +164 -0
  8. package/apps/release-ts/src/cli/commands.ts +351 -0
  9. package/apps/release-ts/src/cli/main.ts +43 -0
  10. package/apps/release-ts/src/cli/node-main.ts +49 -0
  11. package/apps/ts-release-agents/LICENSE +21 -0
  12. package/apps/ts-release-agents/README.md +21 -0
  13. package/apps/ts-release-agents/evals/cases.json +11 -0
  14. package/apps/ts-release-agents/manifests/claude.json +5 -0
  15. package/apps/ts-release-agents/manifests/codex.json +5 -0
  16. package/apps/ts-release-agents/package.json +11 -0
  17. package/apps/ts-release-agents/skills/release/SKILL.md +48 -0
  18. package/apps/ts-release-agents/skills/release/references/configuration.md +18 -0
  19. package/apps/ts-release-agents/skills/release/references/recovery.md +10 -0
  20. package/apps/ts-release-agents/skills/release/references/staged-workflow.md +12 -0
  21. package/apps/ts-release-agents/skills/release/references/target-selection.md +13 -0
  22. package/apps/ts-release-agents/skills/release/references/verification.md +14 -0
  23. package/apps/ts-release-agents/src/build.ts +105 -0
  24. package/apps/ts-release-agents/src/check.ts +117 -0
  25. package/apps/ts-release-agents/src/install.ts +121 -0
  26. package/apps/ts-release-agents/test/contract.test.ts +56 -0
  27. package/apps/ts-release-agents/tsconfig.json +5 -0
  28. package/dist/api/api.d.ts +15 -0
  29. package/dist/api/api.d.ts.map +1 -0
  30. package/dist/api/api.js +244 -0
  31. package/dist/api/api.js.map +1 -0
  32. package/dist/api/errors.d.ts +39 -0
  33. package/dist/api/errors.d.ts.map +1 -0
  34. package/dist/api/errors.js +32 -0
  35. package/dist/api/errors.js.map +1 -0
  36. package/dist/api/input.d.ts +10 -0
  37. package/dist/api/input.d.ts.map +1 -0
  38. package/dist/api/input.js +73 -0
  39. package/dist/api/input.js.map +1 -0
  40. package/dist/api/runtime.d.ts +18 -0
  41. package/dist/api/runtime.d.ts.map +1 -0
  42. package/dist/api/runtime.js +7 -0
  43. package/dist/api/runtime.js.map +1 -0
  44. package/dist/api/types.d.ts +76 -0
  45. package/dist/api/types.d.ts.map +1 -0
  46. package/dist/api/types.js +17 -0
  47. package/dist/api/types.js.map +1 -0
  48. package/dist/bin/ts-release.js +39239 -0
  49. package/dist/capabilities/bun-targets.d.ts +3 -0
  50. package/dist/capabilities/bun-targets.d.ts.map +1 -0
  51. package/dist/capabilities/bun-targets.js +3 -0
  52. package/dist/capabilities/bun-targets.js.map +1 -0
  53. package/dist/capabilities/field-ownership.d.ts +17 -0
  54. package/dist/capabilities/field-ownership.d.ts.map +1 -0
  55. package/dist/capabilities/field-ownership.js +40 -0
  56. package/dist/capabilities/field-ownership.js.map +1 -0
  57. package/dist/capabilities/module.d.ts +91 -0
  58. package/dist/capabilities/module.d.ts.map +1 -0
  59. package/dist/capabilities/module.js +2 -0
  60. package/dist/capabilities/module.js.map +1 -0
  61. package/dist/capabilities/registry.d.ts +829 -0
  62. package/dist/capabilities/registry.d.ts.map +1 -0
  63. package/dist/capabilities/registry.js +291 -0
  64. package/dist/capabilities/registry.js.map +1 -0
  65. package/dist/config/config.d.ts +6 -0
  66. package/dist/config/config.d.ts.map +1 -0
  67. package/dist/config/config.js +42 -0
  68. package/dist/config/config.js.map +1 -0
  69. package/dist/correction/coordinator.d.ts +248 -0
  70. package/dist/correction/coordinator.d.ts.map +1 -0
  71. package/dist/correction/coordinator.js +274 -0
  72. package/dist/correction/coordinator.js.map +1 -0
  73. package/dist/correction/index.d.ts +3 -0
  74. package/dist/correction/index.d.ts.map +1 -0
  75. package/dist/correction/index.js +3 -0
  76. package/dist/correction/index.js.map +1 -0
  77. package/dist/correction/intent.d.ts +111 -0
  78. package/dist/correction/intent.d.ts.map +1 -0
  79. package/dist/correction/intent.js +146 -0
  80. package/dist/correction/intent.js.map +1 -0
  81. package/dist/drivers/archive.d.ts +8 -0
  82. package/dist/drivers/archive.d.ts.map +1 -0
  83. package/dist/drivers/archive.js +101 -0
  84. package/dist/drivers/archive.js.map +1 -0
  85. package/dist/drivers/contain.d.ts +6 -0
  86. package/dist/drivers/contain.d.ts.map +1 -0
  87. package/dist/drivers/contain.js +17 -0
  88. package/dist/drivers/contain.js.map +1 -0
  89. package/dist/drivers/environment.d.ts +4 -0
  90. package/dist/drivers/environment.d.ts.map +1 -0
  91. package/dist/drivers/environment.js +18 -0
  92. package/dist/drivers/environment.js.map +1 -0
  93. package/dist/drivers/errors.d.ts +9 -0
  94. package/dist/drivers/errors.d.ts.map +1 -0
  95. package/dist/drivers/errors.js +6 -0
  96. package/dist/drivers/errors.js.map +1 -0
  97. package/dist/drivers/glob.d.ts +2 -0
  98. package/dist/drivers/glob.d.ts.map +1 -0
  99. package/dist/drivers/glob.js +162 -0
  100. package/dist/drivers/glob.js.map +1 -0
  101. package/dist/drivers/process.d.ts +48 -0
  102. package/dist/drivers/process.d.ts.map +1 -0
  103. package/dist/drivers/process.js +257 -0
  104. package/dist/drivers/process.js.map +1 -0
  105. package/dist/drivers/redact.d.ts +4 -0
  106. package/dist/drivers/redact.d.ts.map +1 -0
  107. package/dist/drivers/redact.js +24 -0
  108. package/dist/drivers/redact.js.map +1 -0
  109. package/dist/drivers/seccomp-helper-source.d.ts +7 -0
  110. package/dist/drivers/seccomp-helper-source.d.ts.map +1 -0
  111. package/dist/drivers/seccomp-helper-source.js +121 -0
  112. package/dist/drivers/seccomp-helper-source.js.map +1 -0
  113. package/dist/drivers/utils.d.ts +4 -0
  114. package/dist/drivers/utils.d.ts.map +1 -0
  115. package/dist/drivers/utils.js +5 -0
  116. package/dist/drivers/utils.js.map +1 -0
  117. package/dist/drivers/workspace.d.ts +6 -0
  118. package/dist/drivers/workspace.d.ts.map +1 -0
  119. package/dist/drivers/workspace.js +49 -0
  120. package/dist/drivers/workspace.js.map +1 -0
  121. package/dist/extensions/provider-adapter.d.ts +48 -0
  122. package/dist/extensions/provider-adapter.d.ts.map +1 -0
  123. package/dist/extensions/provider-adapter.js +64 -0
  124. package/dist/extensions/provider-adapter.js.map +1 -0
  125. package/dist/host.d.ts +33 -0
  126. package/dist/host.d.ts.map +1 -0
  127. package/dist/host.js +20 -0
  128. package/dist/host.js.map +1 -0
  129. package/dist/index.d.ts +19 -1
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +12 -1
  132. package/dist/index.js.map +1 -1
  133. package/dist/model/artifact-collection.d.ts +84 -0
  134. package/dist/model/artifact-collection.d.ts.map +1 -0
  135. package/dist/model/artifact-collection.js +185 -0
  136. package/dist/model/artifact-collection.js.map +1 -0
  137. package/dist/model/authority.d.ts +89 -0
  138. package/dist/model/authority.d.ts.map +1 -0
  139. package/dist/model/authority.js +80 -0
  140. package/dist/model/authority.js.map +1 -0
  141. package/dist/model/bun-targets.d.ts +34 -0
  142. package/dist/model/bun-targets.d.ts.map +1 -0
  143. package/dist/model/bun-targets.js +17 -0
  144. package/dist/model/bun-targets.js.map +1 -0
  145. package/dist/model/canonical.d.ts +8 -0
  146. package/dist/model/canonical.d.ts.map +1 -0
  147. package/dist/model/canonical.js +205 -0
  148. package/dist/model/canonical.js.map +1 -0
  149. package/dist/model/catalog.d.ts +76 -0
  150. package/dist/model/catalog.d.ts.map +1 -0
  151. package/dist/model/catalog.js +174 -0
  152. package/dist/model/catalog.js.map +1 -0
  153. package/dist/model/digest.d.ts +53 -0
  154. package/dist/model/digest.d.ts.map +1 -0
  155. package/dist/model/digest.js +127 -0
  156. package/dist/model/digest.js.map +1 -0
  157. package/dist/model/errors.d.ts +14 -0
  158. package/dist/model/errors.d.ts.map +1 -0
  159. package/dist/model/errors.js +8 -0
  160. package/dist/model/errors.js.map +1 -0
  161. package/dist/model/primitives.d.ts +19 -0
  162. package/dist/model/primitives.d.ts.map +1 -0
  163. package/dist/model/primitives.js +18 -0
  164. package/dist/model/primitives.js.map +1 -0
  165. package/dist/model/pypi.d.ts +12 -0
  166. package/dist/model/pypi.d.ts.map +1 -0
  167. package/dist/model/pypi.js +20 -0
  168. package/dist/model/pypi.js.map +1 -0
  169. package/dist/model/python-distribution.d.ts +33 -0
  170. package/dist/model/python-distribution.d.ts.map +1 -0
  171. package/dist/model/python-distribution.js +367 -0
  172. package/dist/model/python-distribution.js.map +1 -0
  173. package/dist/model/secret-patterns.d.ts +2 -0
  174. package/dist/model/secret-patterns.d.ts.map +1 -0
  175. package/dist/model/secret-patterns.js +12 -0
  176. package/dist/model/secret-patterns.js.map +1 -0
  177. package/dist/platform/bun.d.ts +7 -0
  178. package/dist/platform/bun.d.ts.map +1 -0
  179. package/dist/platform/bun.js +15 -0
  180. package/dist/platform/bun.js.map +1 -0
  181. package/dist/platform/credentials.d.ts +32 -0
  182. package/dist/platform/credentials.d.ts.map +1 -0
  183. package/dist/platform/credentials.js +772 -0
  184. package/dist/platform/credentials.js.map +1 -0
  185. package/dist/platform/host-support.d.ts +2 -0
  186. package/dist/platform/host-support.d.ts.map +1 -0
  187. package/dist/platform/host-support.js +4 -0
  188. package/dist/platform/host-support.js.map +1 -0
  189. package/dist/platform/node.d.ts +7 -0
  190. package/dist/platform/node.d.ts.map +1 -0
  191. package/dist/platform/node.js +15 -0
  192. package/dist/platform/node.js.map +1 -0
  193. package/dist/platform/release-runtime.d.ts +11 -0
  194. package/dist/platform/release-runtime.d.ts.map +1 -0
  195. package/dist/platform/release-runtime.js +34 -0
  196. package/dist/platform/release-runtime.js.map +1 -0
  197. package/dist/platform/services.d.ts +15 -0
  198. package/dist/platform/services.d.ts.map +1 -0
  199. package/dist/platform/services.js +30 -0
  200. package/dist/platform/services.js.map +1 -0
  201. package/dist/platform/source-observer.d.ts +5 -0
  202. package/dist/platform/source-observer.d.ts.map +1 -0
  203. package/dist/platform/source-observer.js +159 -0
  204. package/dist/platform/source-observer.js.map +1 -0
  205. package/dist/provider-sdk.d.ts +14 -0
  206. package/dist/provider-sdk.d.ts.map +1 -0
  207. package/dist/provider-sdk.js +10 -0
  208. package/dist/provider-sdk.js.map +1 -0
  209. package/dist/publication/adapter.d.ts +212 -0
  210. package/dist/publication/adapter.d.ts.map +1 -0
  211. package/dist/publication/adapter.js +83 -0
  212. package/dist/publication/adapter.js.map +1 -0
  213. package/dist/publication/authority.d.ts +137 -0
  214. package/dist/publication/authority.d.ts.map +1 -0
  215. package/dist/publication/authority.js +233 -0
  216. package/dist/publication/authority.js.map +1 -0
  217. package/dist/publication/catalog-git.d.ts +37 -0
  218. package/dist/publication/catalog-git.d.ts.map +1 -0
  219. package/dist/publication/catalog-git.js +451 -0
  220. package/dist/publication/catalog-git.js.map +1 -0
  221. package/dist/publication/claim.d.ts +39 -0
  222. package/dist/publication/claim.d.ts.map +1 -0
  223. package/dist/publication/claim.js +31 -0
  224. package/dist/publication/claim.js.map +1 -0
  225. package/dist/publication/coordinator.d.ts +246 -0
  226. package/dist/publication/coordinator.d.ts.map +1 -0
  227. package/dist/publication/coordinator.js +370 -0
  228. package/dist/publication/coordinator.js.map +1 -0
  229. package/dist/publication/github.d.ts +36 -0
  230. package/dist/publication/github.d.ts.map +1 -0
  231. package/dist/publication/github.js +644 -0
  232. package/dist/publication/github.js.map +1 -0
  233. package/dist/publication/http.d.ts +69 -0
  234. package/dist/publication/http.d.ts.map +1 -0
  235. package/dist/publication/http.js +29 -0
  236. package/dist/publication/http.js.map +1 -0
  237. package/dist/publication/index.d.ts +5 -0
  238. package/dist/publication/index.d.ts.map +1 -0
  239. package/dist/publication/index.js +5 -0
  240. package/dist/publication/index.js.map +1 -0
  241. package/dist/publication/npm.d.ts +40 -0
  242. package/dist/publication/npm.d.ts.map +1 -0
  243. package/dist/publication/npm.js +359 -0
  244. package/dist/publication/npm.js.map +1 -0
  245. package/dist/publication/profiles.d.ts +136 -0
  246. package/dist/publication/profiles.d.ts.map +1 -0
  247. package/dist/publication/profiles.js +90 -0
  248. package/dist/publication/profiles.js.map +1 -0
  249. package/dist/publication/publisher.d.ts +82 -0
  250. package/dist/publication/publisher.d.ts.map +1 -0
  251. package/dist/publication/publisher.js +53 -0
  252. package/dist/publication/publisher.js.map +1 -0
  253. package/dist/publication/pypi.d.ts +45 -0
  254. package/dist/publication/pypi.d.ts.map +1 -0
  255. package/dist/publication/pypi.js +369 -0
  256. package/dist/publication/pypi.js.map +1 -0
  257. package/dist/publication/recovery.d.ts +663 -0
  258. package/dist/publication/recovery.d.ts.map +1 -0
  259. package/dist/publication/recovery.js +252 -0
  260. package/dist/publication/recovery.js.map +1 -0
  261. package/dist/publication/report.d.ts +1061 -0
  262. package/dist/publication/report.d.ts.map +1 -0
  263. package/dist/publication/report.js +441 -0
  264. package/dist/publication/report.js.map +1 -0
  265. package/dist/recipes/config.d.ts +296 -0
  266. package/dist/recipes/config.d.ts.map +1 -0
  267. package/dist/recipes/config.js +325 -0
  268. package/dist/recipes/config.js.map +1 -0
  269. package/dist/release/capabilities.d.ts +12 -0
  270. package/dist/release/capabilities.d.ts.map +1 -0
  271. package/dist/release/capabilities.js +395 -0
  272. package/dist/release/capabilities.js.map +1 -0
  273. package/dist/release/compiler.d.ts +5 -0
  274. package/dist/release/compiler.d.ts.map +1 -0
  275. package/dist/release/compiler.js +19 -0
  276. package/dist/release/compiler.js.map +1 -0
  277. package/dist/release/config.d.ts +5 -0
  278. package/dist/release/config.d.ts.map +1 -0
  279. package/dist/release/config.js +10 -0
  280. package/dist/release/config.js.map +1 -0
  281. package/dist/release/context.d.ts +96 -0
  282. package/dist/release/context.d.ts.map +1 -0
  283. package/dist/release/context.js +168 -0
  284. package/dist/release/context.js.map +1 -0
  285. package/dist/release/graph.d.ts +271 -0
  286. package/dist/release/graph.d.ts.map +1 -0
  287. package/dist/release/graph.js +766 -0
  288. package/dist/release/graph.js.map +1 -0
  289. package/dist/release/inspect.d.ts +88 -0
  290. package/dist/release/inspect.d.ts.map +1 -0
  291. package/dist/release/inspect.js +75 -0
  292. package/dist/release/inspect.js.map +1 -0
  293. package/dist/release/prepare.d.ts +24 -0
  294. package/dist/release/prepare.d.ts.map +1 -0
  295. package/dist/release/prepare.js +1116 -0
  296. package/dist/release/prepare.js.map +1 -0
  297. package/dist/release/prepared-ref.d.ts +60 -0
  298. package/dist/release/prepared-ref.d.ts.map +1 -0
  299. package/dist/release/prepared-ref.js +131 -0
  300. package/dist/release/prepared-ref.js.map +1 -0
  301. package/dist/release/prepared-store.d.ts +90 -0
  302. package/dist/release/prepared-store.d.ts.map +1 -0
  303. package/dist/release/prepared-store.js +337 -0
  304. package/dist/release/prepared-store.js.map +1 -0
  305. package/dist/release/prepared.d.ts +172 -0
  306. package/dist/release/prepared.d.ts.map +1 -0
  307. package/dist/release/prepared.js +268 -0
  308. package/dist/release/prepared.js.map +1 -0
  309. package/dist/release/staging.d.ts +31 -0
  310. package/dist/release/staging.d.ts.map +1 -0
  311. package/dist/release/staging.js +218 -0
  312. package/dist/release/staging.js.map +1 -0
  313. package/dist/resolve/authored.d.ts +53 -0
  314. package/dist/resolve/authored.d.ts.map +1 -0
  315. package/dist/resolve/authored.js +55 -0
  316. package/dist/resolve/authored.js.map +1 -0
  317. package/dist/resolve/encode.d.ts +3 -0
  318. package/dist/resolve/encode.d.ts.map +1 -0
  319. package/dist/resolve/encode.js +21 -0
  320. package/dist/resolve/encode.js.map +1 -0
  321. package/dist/resolve/errors.d.ts +7 -0
  322. package/dist/resolve/errors.d.ts.map +1 -0
  323. package/dist/resolve/errors.js +17 -0
  324. package/dist/resolve/errors.js.map +1 -0
  325. package/dist/resolve/facts.d.ts +12 -0
  326. package/dist/resolve/facts.d.ts.map +1 -0
  327. package/dist/resolve/facts.js +18 -0
  328. package/dist/resolve/facts.js.map +1 -0
  329. package/dist/resolve/resolve.d.ts +10 -0
  330. package/dist/resolve/resolve.d.ts.map +1 -0
  331. package/dist/resolve/resolve.js +242 -0
  332. package/dist/resolve/resolve.js.map +1 -0
  333. package/dist/store.d.ts +11 -0
  334. package/dist/store.d.ts.map +1 -0
  335. package/dist/store.js +9 -0
  336. package/dist/store.js.map +1 -0
  337. package/examples/README.md +20 -30
  338. package/examples/github-release/release.config.json +11 -18
  339. package/examples/homebrew-tap/README.md +11 -0
  340. package/examples/homebrew-tap/release.config.json +40 -28
  341. package/examples/multi-target/release.config.json +28 -53
  342. package/examples/npm-first-publish/release.config.json +23 -25
  343. package/examples/npm-only/release.config.json +22 -30
  344. package/examples/portable-cli/README.md +17 -0
  345. package/examples/portable-cli/artifacts/portable-example-0.1.0-darwin-arm64 +1 -0
  346. package/examples/portable-cli/artifacts/portable-example-0.1.0-darwin-x64 +1 -0
  347. package/examples/portable-cli/artifacts/portable-example-0.1.0-linux-arm64 +1 -0
  348. package/examples/portable-cli/artifacts/portable-example-0.1.0-linux-x64 +1 -0
  349. package/examples/portable-cli/artifacts/portable-example-0.1.0-windows-x64.exe +1 -0
  350. package/examples/portable-cli/artifacts/portable_example-0.1.0-py3-none-macosx_10_15_x86_64.whl +1 -0
  351. package/examples/portable-cli/artifacts/portable_example-0.1.0-py3-none-macosx_11_0_arm64.whl +1 -0
  352. package/examples/portable-cli/artifacts/portable_example-0.1.0-py3-none-manylinux2014_aarch64.whl +1 -0
  353. package/examples/portable-cli/artifacts/portable_example-0.1.0-py3-none-manylinux2014_x86_64.whl +1 -0
  354. package/examples/portable-cli/artifacts/portable_example-0.1.0-py3-none-win_amd64.whl +1 -0
  355. package/examples/portable-cli/package.json +9 -0
  356. package/examples/portable-cli/release.config.json +53 -0
  357. package/examples/portable-cli/src/cli.ts +3 -0
  358. package/examples/pypi-registry/README.md +8 -0
  359. package/examples/scoop-bucket/README.md +10 -0
  360. package/examples/scoop-bucket/release.config.json +40 -30
  361. package/package.json +61 -154
  362. package/src/api/api.ts +365 -0
  363. package/src/api/errors.ts +36 -0
  364. package/src/api/input.ts +92 -0
  365. package/src/api/runtime.ts +14 -0
  366. package/src/api/types.ts +103 -0
  367. package/src/capabilities/bun-targets.ts +2 -0
  368. package/src/capabilities/field-ownership.ts +60 -0
  369. package/src/capabilities/module.ts +158 -0
  370. package/src/capabilities/registry.ts +333 -0
  371. package/src/config/config.ts +48 -0
  372. package/src/correction/coordinator.ts +336 -0
  373. package/src/correction/index.ts +2 -0
  374. package/src/correction/intent.ts +172 -0
  375. package/src/drivers/archive.ts +102 -0
  376. package/src/drivers/contain.ts +20 -0
  377. package/src/drivers/environment.ts +28 -0
  378. package/src/drivers/errors.ts +6 -0
  379. package/src/drivers/glob.ts +137 -0
  380. package/src/drivers/process.ts +340 -0
  381. package/src/drivers/redact.ts +31 -0
  382. package/src/drivers/seccomp-helper-source.ts +121 -0
  383. package/src/drivers/utils.ts +10 -0
  384. package/src/drivers/workspace.ts +40 -0
  385. package/src/extensions/provider-adapter.ts +99 -0
  386. package/src/host.ts +140 -0
  387. package/src/index.ts +64 -0
  388. package/src/model/artifact-collection.ts +237 -0
  389. package/src/model/authority.ts +116 -0
  390. package/src/model/bun-targets.ts +34 -0
  391. package/src/model/canonical.ts +205 -0
  392. package/src/model/catalog.ts +220 -0
  393. package/src/model/digest.ts +172 -0
  394. package/src/model/errors.ts +6 -0
  395. package/src/model/primitives.ts +37 -0
  396. package/src/model/pypi.ts +30 -0
  397. package/src/model/python-distribution.ts +360 -0
  398. package/src/model/secret-patterns.ts +11 -0
  399. package/src/platform/bun.ts +38 -0
  400. package/src/platform/credentials.ts +1083 -0
  401. package/src/platform/host-support.ts +4 -0
  402. package/src/platform/node.ts +38 -0
  403. package/src/platform/release-runtime.ts +47 -0
  404. package/src/platform/services.ts +57 -0
  405. package/src/platform/source-observer.ts +193 -0
  406. package/src/provider-sdk.ts +48 -0
  407. package/src/publication/adapter.ts +132 -0
  408. package/src/publication/authority.ts +397 -0
  409. package/src/publication/catalog-git.ts +603 -0
  410. package/src/publication/claim.ts +51 -0
  411. package/src/publication/coordinator.ts +578 -0
  412. package/src/publication/github.ts +954 -0
  413. package/src/publication/http.ts +101 -0
  414. package/src/publication/index.ts +4 -0
  415. package/src/publication/npm.ts +525 -0
  416. package/src/publication/profiles.ts +94 -0
  417. package/src/publication/publisher.ts +130 -0
  418. package/src/publication/pypi.ts +463 -0
  419. package/src/publication/recovery.ts +349 -0
  420. package/src/publication/report.ts +628 -0
  421. package/src/recipes/config.ts +395 -0
  422. package/src/release/capabilities.ts +459 -0
  423. package/src/release/compiler.ts +28 -0
  424. package/src/release/config.ts +12 -0
  425. package/src/release/context.ts +223 -0
  426. package/src/release/graph.ts +866 -0
  427. package/src/release/inspect.ts +83 -0
  428. package/src/release/prepare.ts +1331 -0
  429. package/src/release/prepared-ref.ts +193 -0
  430. package/src/release/prepared-store.ts +377 -0
  431. package/src/release/prepared.ts +299 -0
  432. package/src/release/staging.ts +257 -0
  433. package/src/resolve/authored.ts +64 -0
  434. package/src/resolve/encode.ts +22 -0
  435. package/src/resolve/errors.ts +15 -0
  436. package/src/resolve/facts.ts +18 -0
  437. package/src/resolve/resolve.ts +328 -0
  438. package/src/store.ts +28 -0
  439. package/templates/README.md +55 -59
  440. package/templates/bun-cli-github/README.md +12 -0
  441. package/templates/bun-cli-github/release.config.json +51 -0
  442. package/templates/github-actions/release.yml +57 -0
  443. package/templates/github-actions/reviewed-release.yml +80 -0
  444. package/templates/multi-target-homebrew/README.md +10 -0
  445. package/templates/multi-target-homebrew/release.config.json +43 -60
  446. package/templates/multi-target-scoop/README.md +10 -0
  447. package/templates/multi-target-scoop/release.config.json +43 -62
  448. package/templates/npm-github/release.config.json +25 -38
  449. package/templates/npm-github/reviewed-release.config.json +37 -0
  450. package/templates/npm-only/README.md +10 -0
  451. package/templates/npm-only/release.config.json +15 -30
  452. package/templates/portable-cli/README.md +13 -0
  453. package/templates/portable-cli/release.config.json +53 -0
  454. package/dist/config/errors.d.ts +0 -24
  455. package/dist/config/errors.d.ts.map +0 -1
  456. package/dist/config/errors.js +0 -19
  457. package/dist/config/errors.js.map +0 -1
  458. package/dist/config/load.d.ts +0 -9
  459. package/dist/config/load.d.ts.map +0 -1
  460. package/dist/config/load.js +0 -29
  461. package/dist/config/load.js.map +0 -1
  462. package/dist/config/schema.d.ts +0 -12
  463. package/dist/config/schema.d.ts.map +0 -1
  464. package/dist/config/schema.js +0 -19
  465. package/dist/config/schema.js.map +0 -1
  466. package/dist/domain/artifact.d.ts +0 -36
  467. package/dist/domain/artifact.d.ts.map +0 -1
  468. package/dist/domain/artifact.js +0 -29
  469. package/dist/domain/artifact.js.map +0 -1
  470. package/dist/domain/evidence.d.ts +0 -108
  471. package/dist/domain/evidence.d.ts.map +0 -1
  472. package/dist/domain/evidence.js +0 -96
  473. package/dist/domain/evidence.js.map +0 -1
  474. package/dist/domain/operation.d.ts +0 -153
  475. package/dist/domain/operation.d.ts.map +0 -1
  476. package/dist/domain/operation.js +0 -320
  477. package/dist/domain/operation.js.map +0 -1
  478. package/dist/domain/release.d.ts +0 -141
  479. package/dist/domain/release.d.ts.map +0 -1
  480. package/dist/domain/release.js +0 -127
  481. package/dist/domain/release.js.map +0 -1
  482. package/dist/domain/remote-state.d.ts +0 -85
  483. package/dist/domain/remote-state.d.ts.map +0 -1
  484. package/dist/domain/remote-state.js +0 -77
  485. package/dist/domain/remote-state.js.map +0 -1
  486. package/dist/domain/status.d.ts +0 -43
  487. package/dist/domain/status.d.ts.map +0 -1
  488. package/dist/domain/status.js +0 -51
  489. package/dist/domain/status.js.map +0 -1
  490. package/dist/domain/target.d.ts +0 -135
  491. package/dist/domain/target.d.ts.map +0 -1
  492. package/dist/domain/target.js +0 -137
  493. package/dist/domain/target.js.map +0 -1
  494. package/dist/host/host.d.ts +0 -32
  495. package/dist/host/host.d.ts.map +0 -1
  496. package/dist/host/host.js +0 -24
  497. package/dist/host/host.js.map +0 -1
  498. package/dist/host/http-live.d.ts +0 -6
  499. package/dist/host/http-live.d.ts.map +0 -1
  500. package/dist/host/http-live.js +0 -86
  501. package/dist/host/http-live.js.map +0 -1
  502. package/dist/host/http.d.ts +0 -43
  503. package/dist/host/http.d.ts.map +0 -1
  504. package/dist/host/http.js +0 -59
  505. package/dist/host/http.js.map +0 -1
  506. package/dist/host/platform.d.ts +0 -10
  507. package/dist/host/platform.d.ts.map +0 -1
  508. package/dist/host/platform.js +0 -114
  509. package/dist/host/platform.js.map +0 -1
  510. package/dist/host/test.d.ts +0 -23
  511. package/dist/host/test.d.ts.map +0 -1
  512. package/dist/host/test.js +0 -222
  513. package/dist/host/test.js.map +0 -1
  514. package/dist/internal/workflow-phases.d.ts +0 -3
  515. package/dist/internal/workflow-phases.d.ts.map +0 -1
  516. package/dist/internal/workflow-phases.js +0 -2
  517. package/dist/internal/workflow-phases.js.map +0 -1
  518. package/dist/internal/workspace-path.d.ts +0 -15
  519. package/dist/internal/workspace-path.d.ts.map +0 -1
  520. package/dist/internal/workspace-path.js +0 -36
  521. package/dist/internal/workspace-path.js.map +0 -1
  522. package/dist/planner/artifact-inventory.d.ts +0 -9
  523. package/dist/planner/artifact-inventory.d.ts.map +0 -1
  524. package/dist/planner/artifact-inventory.js +0 -84
  525. package/dist/planner/artifact-inventory.js.map +0 -1
  526. package/dist/planner/create-release-plan.d.ts +0 -5
  527. package/dist/planner/create-release-plan.d.ts.map +0 -1
  528. package/dist/planner/create-release-plan.js +0 -25
  529. package/dist/planner/create-release-plan.js.map +0 -1
  530. package/dist/planner/errors.d.ts +0 -81
  531. package/dist/planner/errors.d.ts.map +0 -1
  532. package/dist/planner/errors.js +0 -70
  533. package/dist/planner/errors.js.map +0 -1
  534. package/dist/planner/evidence-recorder.d.ts +0 -26
  535. package/dist/planner/evidence-recorder.d.ts.map +0 -1
  536. package/dist/planner/evidence-recorder.js +0 -330
  537. package/dist/planner/evidence-recorder.js.map +0 -1
  538. package/dist/planner/executor.d.ts +0 -58
  539. package/dist/planner/executor.d.ts.map +0 -1
  540. package/dist/planner/executor.js +0 -159
  541. package/dist/planner/executor.js.map +0 -1
  542. package/dist/planner/normalize-release.d.ts +0 -16
  543. package/dist/planner/normalize-release.d.ts.map +0 -1
  544. package/dist/planner/normalize-release.js +0 -218
  545. package/dist/planner/normalize-release.js.map +0 -1
  546. package/dist/planner/reconcile.d.ts +0 -17
  547. package/dist/planner/reconcile.d.ts.map +0 -1
  548. package/dist/planner/reconcile.js +0 -261
  549. package/dist/planner/reconcile.js.map +0 -1
  550. package/dist/planner/release-eligibility.d.ts +0 -39
  551. package/dist/planner/release-eligibility.d.ts.map +0 -1
  552. package/dist/planner/release-eligibility.js +0 -578
  553. package/dist/planner/release-eligibility.js.map +0 -1
  554. package/dist/planner/render-plan.d.ts +0 -15
  555. package/dist/planner/render-plan.d.ts.map +0 -1
  556. package/dist/planner/render-plan.js +0 -238
  557. package/dist/planner/render-plan.js.map +0 -1
  558. package/dist/planner/status.d.ts +0 -40
  559. package/dist/planner/status.d.ts.map +0 -1
  560. package/dist/planner/status.js +0 -313
  561. package/dist/planner/status.js.map +0 -1
  562. package/dist/targets/adapter-helpers.d.ts +0 -64
  563. package/dist/targets/adapter-helpers.d.ts.map +0 -1
  564. package/dist/targets/adapter-helpers.js +0 -124
  565. package/dist/targets/adapter-helpers.js.map +0 -1
  566. package/dist/targets/adapter.d.ts +0 -17
  567. package/dist/targets/adapter.d.ts.map +0 -1
  568. package/dist/targets/adapter.js +0 -2
  569. package/dist/targets/adapter.js.map +0 -1
  570. package/dist/targets/github-release.d.ts +0 -25
  571. package/dist/targets/github-release.d.ts.map +0 -1
  572. package/dist/targets/github-release.js +0 -82
  573. package/dist/targets/github-release.js.map +0 -1
  574. package/dist/targets/github.d.ts +0 -11
  575. package/dist/targets/github.d.ts.map +0 -1
  576. package/dist/targets/github.js +0 -125
  577. package/dist/targets/github.js.map +0 -1
  578. package/dist/targets/homebrew.d.ts +0 -11
  579. package/dist/targets/homebrew.d.ts.map +0 -1
  580. package/dist/targets/homebrew.js +0 -97
  581. package/dist/targets/homebrew.js.map +0 -1
  582. package/dist/targets/live.d.ts +0 -5
  583. package/dist/targets/live.d.ts.map +0 -1
  584. package/dist/targets/live.js +0 -50
  585. package/dist/targets/live.js.map +0 -1
  586. package/dist/targets/npm.d.ts +0 -10
  587. package/dist/targets/npm.d.ts.map +0 -1
  588. package/dist/targets/npm.js +0 -118
  589. package/dist/targets/npm.js.map +0 -1
  590. package/dist/targets/pypi.d.ts +0 -11
  591. package/dist/targets/pypi.d.ts.map +0 -1
  592. package/dist/targets/pypi.js +0 -103
  593. package/dist/targets/pypi.js.map +0 -1
  594. package/dist/targets/registry.d.ts +0 -25
  595. package/dist/targets/registry.d.ts.map +0 -1
  596. package/dist/targets/registry.js +0 -34
  597. package/dist/targets/registry.js.map +0 -1
  598. package/dist/targets/scoop.d.ts +0 -11
  599. package/dist/targets/scoop.d.ts.map +0 -1
  600. package/dist/targets/scoop.js +0 -78
  601. package/dist/targets/scoop.js.map +0 -1
  602. package/dist/types/effect-internal.d.ts +0 -5
  603. package/dist/types/effect-internal.d.ts.map +0 -1
  604. package/dist/types/effect-internal.js +0 -2
  605. package/dist/types/effect-internal.js.map +0 -1
  606. package/dist/workflows/config.d.ts +0 -252
  607. package/dist/workflows/config.d.ts.map +0 -1
  608. package/dist/workflows/config.js +0 -465
  609. package/dist/workflows/config.js.map +0 -1
  610. package/dist/workflows/diagnostics.d.ts +0 -62
  611. package/dist/workflows/diagnostics.d.ts.map +0 -1
  612. package/dist/workflows/diagnostics.js +0 -527
  613. package/dist/workflows/diagnostics.js.map +0 -1
  614. package/dist/workflows/evidence.d.ts +0 -26
  615. package/dist/workflows/evidence.d.ts.map +0 -1
  616. package/dist/workflows/evidence.js +0 -44
  617. package/dist/workflows/evidence.js.map +0 -1
  618. package/dist/workflows/index.d.ts +0 -7
  619. package/dist/workflows/index.d.ts.map +0 -1
  620. package/dist/workflows/index.js +0 -6
  621. package/dist/workflows/index.js.map +0 -1
  622. package/dist/workflows/init.d.ts +0 -84
  623. package/dist/workflows/init.d.ts.map +0 -1
  624. package/dist/workflows/init.js +0 -441
  625. package/dist/workflows/init.js.map +0 -1
  626. package/dist/workflows/live.d.ts +0 -9
  627. package/dist/workflows/live.d.ts.map +0 -1
  628. package/dist/workflows/live.js +0 -7
  629. package/dist/workflows/live.js.map +0 -1
  630. package/dist/workflows/options.d.ts +0 -30
  631. package/dist/workflows/options.d.ts.map +0 -1
  632. package/dist/workflows/options.js +0 -16
  633. package/dist/workflows/options.js.map +0 -1
  634. package/examples/non-strict-skips/artifacts/release-example-skips-0.1.0.tgz +0 -1
  635. package/examples/non-strict-skips/release.config.json +0 -31
  636. package/examples/pypi-registry/release.config.json +0 -31
  637. package/templates/github-actions/plan-and-approved-execute.yml +0 -58
  638. package/templates/github-actions/plan-only.yml +0 -33
  639. package/templates/github-actions/trusted-publishing.yml +0 -61
@@ -0,0 +1,954 @@
1
+ import * as Effect from "effect/Effect"
2
+ import type { CredentialRequest, ResolvedAuthStrategy } from "../model/authority.js"
3
+ import { CredentialRequest as CredentialRequestSchema } from "../model/authority.js"
4
+ import {
5
+ digestEquals,
6
+ formatGitHubSha256,
7
+ formatSha256Hex,
8
+ parseGitHubSha256,
9
+ sha256Digest,
10
+ type Sha256Digest
11
+ } from "../model/digest.js"
12
+ import { NonEmptyName } from "../model/primitives.js"
13
+ import type { PreparedGitHubPublication } from "../release/prepared.js"
14
+ import type { PreparedBundle } from "../release/prepared-store.js"
15
+ import type {
16
+ CredentialGrant,
17
+ MutationCredentialGrant,
18
+ PublisherOperation,
19
+ ScopedSecret
20
+ } from "./authority.js"
21
+ import {
22
+ ReleaseSubjectError,
23
+ type ReleaseObservationContext,
24
+ type ReleaseSubject
25
+ } from "./coordinator.js"
26
+ import type {
27
+ AuthorizedMutationHttpShape,
28
+ HttpAuthorizerShape,
29
+ HttpResponse,
30
+ MutationHttpRequest
31
+ } from "./http.js"
32
+ import {
33
+ AbsenceBasis,
34
+ Applied,
35
+ AuthoritativelyAbsent,
36
+ Conflict,
37
+ CreateAuthorizationProof,
38
+ Difference,
39
+ InconclusiveObservation,
40
+ MutationPrecondition,
41
+ NeedsMutation,
42
+ OutcomeUnknown,
43
+ PresentDifferent,
44
+ PresentEquivalent,
45
+ ProviderAlreadyEquivalent,
46
+ ProviderAuthorizedCreate,
47
+ ProviderBlocked,
48
+ ProviderMutationFact,
49
+ SafeReason,
50
+ VisibilityBasis,
51
+ VisibilityPending,
52
+ type MutationAttempt,
53
+ type MutationDecision,
54
+ type Observation,
55
+ type ProviderDecision
56
+ } from "./report.js"
57
+ import { makeRecoveryCapabilityProfile } from "./recovery.js"
58
+
59
+ const githubApiVersion = "2022-11-28"
60
+ const githubJsonHeaders = {
61
+ accept: "application/vnd.github+json",
62
+ "x-github-api-version": githubApiVersion
63
+ } as const
64
+ const gitObjectSha = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u
65
+ const maximumTagDepth = 32
66
+ const maximumAssetPages = 1_000
67
+
68
+ /**
69
+ * Timing values are policy bounds, not measured provider facts. They remain
70
+ * ASSUMED/UNVERIFIED until an explicitly authorized live run records evidence.
71
+ */
72
+ export const githubRecoveryCapabilityProfile = makeRecoveryCapabilityProfile({
73
+ observation: "exact",
74
+ authoritativeAbsence: "provider-specific",
75
+ createAuthorization: "authenticated-namespace-and-unique-coordinate",
76
+ replay: "coordinate-unique",
77
+ identifierReuse: "reusable",
78
+ correction: [],
79
+ exposure: "persistent-to-consumers",
80
+ historyRequirement: "optional-evidence",
81
+ readConvergence: {
82
+ contract: {
83
+ _tag: "assumed",
84
+ basis: "ASSUMED/UNVERIFIED: no authorized live mutation evidence establishes GitHub read-convergence timing."
85
+ },
86
+ observationRetry: {
87
+ maxAttempts: 5,
88
+ backoff: { baseMs: 1_000, factor: 2, capMs: 15_000 },
89
+ totalBudgetMs: 60_000
90
+ },
91
+ retryEligible: "VisibilityPending | Inconclusive",
92
+ exhaustion: "UncertainSubject with full trace"
93
+ }
94
+ })
95
+
96
+ interface RepositoryFacts {
97
+ readonly fullName: string
98
+ }
99
+
100
+ interface GitObjectFacts {
101
+ readonly type: "commit" | "tag"
102
+ readonly sha: string
103
+ }
104
+
105
+ interface ReleaseFacts {
106
+ readonly id: number
107
+ readonly tag: string
108
+ readonly title: string
109
+ readonly body: string
110
+ readonly draft: boolean
111
+ readonly prerelease: boolean
112
+ readonly uploadUrl: string
113
+ }
114
+
115
+ interface ReleaseAssetFacts {
116
+ readonly name: string
117
+ readonly size: number
118
+ readonly mediaType: string
119
+ readonly state: "uploaded"
120
+ readonly digest:
121
+ | { readonly _tag: "Present", readonly value: Sha256Digest }
122
+ | { readonly _tag: "Missing" }
123
+ | { readonly _tag: "Malformed" }
124
+ readonly apiUrl: string
125
+ }
126
+
127
+ type GithubMutationPlan =
128
+ | {
129
+ readonly _tag: "CreateRelease"
130
+ readonly tagAbsent: boolean
131
+ }
132
+ | {
133
+ readonly _tag: "UploadAssets"
134
+ readonly releaseId: number
135
+ readonly uploadUrl: string
136
+ readonly assetIndexes: ReadonlyArray<number>
137
+ }
138
+
139
+ type PageResult =
140
+ | { readonly _tag: "Complete", readonly assets: ReadonlyArray<ReleaseAssetFacts> }
141
+ | { readonly _tag: "Inconclusive", readonly reason: string }
142
+
143
+ const asObject = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
144
+ typeof value === "object" && value !== null && !Array.isArray(value)
145
+ ? value as Readonly<Record<string, unknown>>
146
+ : undefined
147
+
148
+ const asString = (value: unknown): string | undefined =>
149
+ typeof value === "string" ? value : undefined
150
+
151
+ const asNonEmptyString = (value: unknown): string | undefined =>
152
+ typeof value === "string" && value.length > 0 ? value : undefined
153
+
154
+ const asBoolean = (value: unknown): boolean | undefined =>
155
+ typeof value === "boolean" ? value : undefined
156
+
157
+ const asNonNegativeSize = (value: unknown): number | undefined =>
158
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined
159
+
160
+ const asPositiveId = (value: unknown): number | undefined =>
161
+ typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined
162
+
163
+ const parseJson = (body: Uint8Array | string): unknown | undefined => {
164
+ try {
165
+ const text = typeof body === "string"
166
+ ? body
167
+ : new TextDecoder("utf-8", { fatal: true }).decode(body)
168
+ return JSON.parse(text) as unknown
169
+ } catch {
170
+ return undefined
171
+ }
172
+ }
173
+
174
+ const responseHeader = (response: HttpResponse, name: string): string | undefined => {
175
+ const lower = name.toLowerCase()
176
+ return Object.entries(response.headers).find(([candidate]) => candidate.toLowerCase() === lower)?.[1]
177
+ }
178
+
179
+ const repositoryFacts = (value: unknown): RepositoryFacts | undefined => {
180
+ const object = asObject(value)
181
+ const fullName = asNonEmptyString(object?.full_name)
182
+ return fullName === undefined ? undefined : { fullName }
183
+ }
184
+
185
+ const gitObjectFacts = (value: unknown): GitObjectFacts | undefined => {
186
+ const object = asObject(value)
187
+ const type = object?.type
188
+ const sha = asString(object?.sha)
189
+ return (type !== "commit" && type !== "tag") || sha === undefined || !gitObjectSha.test(sha)
190
+ ? undefined
191
+ : { type, sha }
192
+ }
193
+
194
+ const parseRef = (
195
+ value: unknown,
196
+ expectedRef: string
197
+ ): GitObjectFacts | undefined => {
198
+ const object = asObject(value)
199
+ return object?.ref !== expectedRef ? undefined : gitObjectFacts(object.object)
200
+ }
201
+
202
+ const parseTagObject = (
203
+ value: unknown,
204
+ expectedSha: string
205
+ ): GitObjectFacts | undefined => {
206
+ const object = asObject(value)
207
+ return object?.sha !== expectedSha ? undefined : gitObjectFacts(object.object)
208
+ }
209
+
210
+ const releaseFacts = (value: unknown): ReleaseFacts | undefined => {
211
+ const object = asObject(value)
212
+ const id = asPositiveId(object?.id)
213
+ const tag = asNonEmptyString(object?.tag_name)
214
+ const title = asString(object?.name)
215
+ const body = object?.body === null ? "" : asString(object?.body)
216
+ const draft = asBoolean(object?.draft)
217
+ const prerelease = asBoolean(object?.prerelease)
218
+ const uploadUrl = asNonEmptyString(object?.upload_url)
219
+ return id === undefined || tag === undefined || title === undefined || body === undefined ||
220
+ draft === undefined || prerelease === undefined || uploadUrl === undefined
221
+ ? undefined
222
+ : { id, tag, title, body, draft, prerelease, uploadUrl }
223
+ }
224
+
225
+ const releaseAssetApiUrl = (
226
+ publication: PreparedGitHubPublication,
227
+ value: unknown
228
+ ): string | undefined => {
229
+ if (typeof value !== "string") return undefined
230
+ const prefix = `${publication.authority.audience}/releases/assets/`
231
+ const id = value.startsWith(prefix) ? value.slice(prefix.length) : ""
232
+ return /^[1-9][0-9]*$/u.test(id) ? value : undefined
233
+ }
234
+
235
+ const releaseAssetDigest = (
236
+ asset: Readonly<Record<string, unknown>>
237
+ ): ReleaseAssetFacts["digest"] => {
238
+ if (!Object.hasOwn(asset, "digest") || asset.digest === null) return { _tag: "Missing" }
239
+ try {
240
+ return { _tag: "Present", value: parseGitHubSha256(asset.digest) }
241
+ } catch {
242
+ return { _tag: "Malformed" }
243
+ }
244
+ }
245
+
246
+ const releaseAssetFacts = (
247
+ publication: PreparedGitHubPublication,
248
+ value: unknown
249
+ ): ReleaseAssetFacts | undefined => {
250
+ const object = asObject(value)
251
+ if (object === undefined) return undefined
252
+ const name = asNonEmptyString(object.name)
253
+ const size = asNonNegativeSize(object.size)
254
+ const mediaType = asNonEmptyString(object.content_type)
255
+ const apiUrl = releaseAssetApiUrl(publication, object.url)
256
+ if (name === undefined || size === undefined || mediaType === undefined ||
257
+ object.state !== "uploaded" || apiUrl === undefined) return undefined
258
+ return {
259
+ name,
260
+ size,
261
+ mediaType,
262
+ state: "uploaded",
263
+ digest: releaseAssetDigest(object),
264
+ apiUrl
265
+ }
266
+ }
267
+
268
+ const observationRequests = (
269
+ publication: PreparedGitHubPublication
270
+ ): readonly [CredentialRequest, ...Array<CredentialRequest>] => {
271
+ const make = (strategy: ResolvedAuthStrategy): CredentialRequest => CredentialRequestSchema.make({
272
+ subject: publication.authority.subject,
273
+ provider: publication.authority.provider,
274
+ audience: publication.authority.audience,
275
+ purpose: "observe",
276
+ strategy
277
+ })
278
+ const first = publication.authority.observationStrategies[0]!
279
+ return [make(first), ...publication.authority.observationStrategies.slice(1).map(make)]
280
+ }
281
+
282
+ const mutationRequest = (publication: PreparedGitHubPublication): CredentialRequest =>
283
+ CredentialRequestSchema.make({
284
+ subject: publication.authority.subject,
285
+ provider: publication.authority.provider,
286
+ audience: publication.authority.audience,
287
+ purpose: "publish",
288
+ strategy: publication.authority.publishStrategy
289
+ })
290
+
291
+ const valueFingerprint = (origin: "prepared" | "provider", value: string): SafeReason =>
292
+ SafeReason.make(
293
+ `${origin} value sha256-${formatSha256Hex(sha256Digest(new TextEncoder().encode(value)))}`
294
+ )
295
+
296
+ const textDifference = (
297
+ field: string,
298
+ expected: string,
299
+ observed: string
300
+ ): Difference => Difference.make({
301
+ field: NonEmptyName.make(field),
302
+ expected: valueFingerprint("prepared", expected),
303
+ observed: valueFingerprint("provider", observed)
304
+ })
305
+
306
+ const scalarDifference = (
307
+ field: string,
308
+ expected: boolean | number | string,
309
+ observed: boolean | number | string
310
+ ): Difference => Difference.make({
311
+ field: NonEmptyName.make(field),
312
+ expected: SafeReason.make(String(expected)),
313
+ observed: SafeReason.make(String(observed))
314
+ })
315
+
316
+ const inconclusive = (
317
+ publication: PreparedGitHubPublication,
318
+ reason: string
319
+ ): InconclusiveObservation => InconclusiveObservation.make({
320
+ subject: publication.authority.subject,
321
+ reason: SafeReason.make(reason)
322
+ })
323
+
324
+ const pending = (
325
+ publication: PreparedGitHubPublication,
326
+ detail: string
327
+ ): VisibilityPending => VisibilityPending.make({
328
+ subject: publication.authority.subject,
329
+ expectation: SafeReason.make("The accepted GitHub mutation is expected to become exactly observable."),
330
+ basis: VisibilityBasis.make({
331
+ kind: NonEmptyName.make("github-read-convergence"),
332
+ detail: SafeReason.make(detail)
333
+ })
334
+ })
335
+
336
+ const absent = (
337
+ publication: PreparedGitHubPublication,
338
+ kind: string,
339
+ detail: string
340
+ ): AuthoritativelyAbsent => AuthoritativelyAbsent.make({
341
+ subject: publication.authority.subject,
342
+ basis: AbsenceBasis.make({
343
+ kind: NonEmptyName.make(kind),
344
+ detail: SafeReason.make(detail)
345
+ })
346
+ })
347
+
348
+ const observationFailure = (
349
+ publication: PreparedGitHubPublication,
350
+ reason: string,
351
+ commitment: "before-dispatch" | "unknown" = "unknown"
352
+ ): ReleaseSubjectError => new ReleaseSubjectError({
353
+ subject: publication.authority.subject,
354
+ phase: "observe",
355
+ commitment,
356
+ reason: SafeReason.make(reason)
357
+ })
358
+
359
+ const mapObservationError = (
360
+ publication: PreparedGitHubPublication,
361
+ error: { readonly _tag: string, readonly commitment?: "before-dispatch" | "unknown" }
362
+ ): ReleaseSubjectError => observationFailure(
363
+ publication,
364
+ "GitHub observation could not be completed by the host HTTP boundary.",
365
+ error._tag === "CredentialPlatformError" ? error.commitment ?? "before-dispatch" : "before-dispatch"
366
+ )
367
+
368
+ const get = (
369
+ publication: PreparedGitHubPublication,
370
+ http: HttpAuthorizerShape,
371
+ grant: Exclude<CredentialGrant, { readonly _tag: "WorkloadIdentity" }>,
372
+ url: string,
373
+ accept = "application/vnd.github+json"
374
+ ): Effect.Effect<HttpResponse, ReleaseSubjectError> => http.execute({
375
+ subject: publication.authority.subject,
376
+ method: "GET",
377
+ url,
378
+ headers: { accept, "x-github-api-version": githubApiVersion }
379
+ }, grant).pipe(Effect.mapError((error) => mapObservationError(publication, error)))
380
+
381
+ const releaseUrl = (publication: PreparedGitHubPublication): string =>
382
+ `${publication.authority.audience}/releases/tags/${encodeURIComponent(publication.tag)}`
383
+
384
+ const refUrl = (publication: PreparedGitHubPublication): string =>
385
+ `${publication.authority.audience}/git/ref/tags/${encodeURIComponent(publication.tag)}`
386
+
387
+ const tagObjectUrl = (publication: PreparedGitHubPublication, sha: string): string =>
388
+ `${publication.authority.audience}/git/tags/${sha}`
389
+
390
+ const assetsPageUrl = (
391
+ publication: PreparedGitHubPublication,
392
+ releaseId: number,
393
+ page: number
394
+ ): string => `${publication.authority.audience}/releases/${releaseId}/assets?per_page=100&page=${page}`
395
+
396
+ const linkNextUrl = (value: string | undefined): { readonly malformed: boolean, readonly url?: string } => {
397
+ if (value === undefined) return { malformed: false }
398
+ const matches = [...value.matchAll(/<([^>]+)>\s*;\s*rel="next"/gu)]
399
+ if (matches.length > 1 || (/rel="next"/u.test(value) && matches.length !== 1)) return { malformed: true }
400
+ const url = matches[0]?.[1]
401
+ return url === undefined ? { malformed: false } : { malformed: false, url }
402
+ }
403
+
404
+ const listAllAssets = Effect.fn("GitHubReleaseSubject.listAllAssets")(function*(
405
+ publication: PreparedGitHubPublication,
406
+ http: HttpAuthorizerShape,
407
+ grant: Exclude<CredentialGrant, { readonly _tag: "WorkloadIdentity" }>,
408
+ releaseId: number
409
+ ) {
410
+ const assets: Array<ReleaseAssetFacts> = []
411
+ let page = 1
412
+ while (page <= maximumAssetPages) {
413
+ const expectedUrl = assetsPageUrl(publication, releaseId, page)
414
+ const response = yield* get(publication, http, grant, expectedUrl)
415
+ if (response.status !== 200) {
416
+ return { _tag: "Inconclusive", reason: `GitHub asset enumeration returned HTTP ${response.status}.` } satisfies PageResult
417
+ }
418
+ const raw = parseJson(response.body)
419
+ if (!Array.isArray(raw)) {
420
+ return { _tag: "Inconclusive", reason: "A GitHub asset page was malformed." } satisfies PageResult
421
+ }
422
+ const parsed = raw.map((value) => releaseAssetFacts(publication, value))
423
+ if (parsed.some((value) => value === undefined)) {
424
+ return {
425
+ _tag: "Inconclusive",
426
+ reason: "A GitHub asset page omitted or malformed an exact asset identity fact."
427
+ } satisfies PageResult
428
+ }
429
+ assets.push(...parsed as Array<ReleaseAssetFacts>)
430
+ const next = linkNextUrl(responseHeader(response, "link"))
431
+ if (next.malformed) {
432
+ return { _tag: "Inconclusive", reason: "GitHub asset pagination metadata was malformed." } satisfies PageResult
433
+ }
434
+ const computedNext = assetsPageUrl(publication, releaseId, page + 1)
435
+ if (next.url !== undefined && next.url !== computedNext) {
436
+ return {
437
+ _tag: "Inconclusive",
438
+ reason: "GitHub asset pagination left the exact prepared repository scope or skipped a page."
439
+ } satisfies PageResult
440
+ }
441
+ if (next.url === undefined && raw.length < 100) {
442
+ return { _tag: "Complete", assets } satisfies PageResult
443
+ }
444
+ page += 1
445
+ }
446
+ return {
447
+ _tag: "Inconclusive",
448
+ reason: "GitHub asset pagination exceeded the bounded full-enumeration limit."
449
+ } satisfies PageResult
450
+ })
451
+
452
+ const compareReleaseMetadata = (
453
+ publication: PreparedGitHubPublication,
454
+ facts: ReleaseFacts
455
+ ): ReadonlyArray<Difference> => {
456
+ const differences: Array<Difference> = []
457
+ if (facts.tag !== publication.tag.toString()) {
458
+ differences.push(textDifference("release.tag", publication.tag.toString(), facts.tag))
459
+ }
460
+ if (facts.title !== publication.title.toString()) {
461
+ differences.push(textDifference("release.title", publication.title.toString(), facts.title))
462
+ }
463
+ const expectedBody = publication.body ?? ""
464
+ if (facts.body !== expectedBody) differences.push(textDifference("release.body", expectedBody, facts.body))
465
+ if (facts.draft !== publication.draft) {
466
+ differences.push(scalarDifference("release.draft", publication.draft, facts.draft))
467
+ }
468
+ if (facts.prerelease !== publication.prerelease) {
469
+ differences.push(scalarDifference("release.prerelease", publication.prerelease, facts.prerelease))
470
+ }
471
+ return differences
472
+ }
473
+
474
+ const validateUploadUrl = (
475
+ publication: PreparedGitHubPublication,
476
+ releaseId: number,
477
+ value: string
478
+ ): boolean => value ===
479
+ `https://uploads.github.com/repos/${publication.repository}/releases/${releaseId}/assets{?name,label}`
480
+
481
+ const presentDifferent = (
482
+ publication: PreparedGitHubPublication,
483
+ differences: ReadonlyArray<Difference>
484
+ ): PresentDifferent => PresentDifferent.make({
485
+ subject: publication.authority.subject,
486
+ differences: differences as [Difference, ...Array<Difference>]
487
+ })
488
+
489
+ const decideFor = (
490
+ publication: PreparedGitHubPublication,
491
+ observation: Observation,
492
+ plan: GithubMutationPlan | undefined
493
+ ): ProviderDecision => {
494
+ switch (observation._tag) {
495
+ case "PresentEquivalent":
496
+ return ProviderAlreadyEquivalent.make({ subject: publication.authority.subject })
497
+ case "PresentDifferent":
498
+ return Conflict.make({ subject: publication.authority.subject, differences: observation.differences })
499
+ case "AuthoritativelyAbsent":
500
+ if (plan?._tag === "CreateRelease" && plan.tagAbsent) {
501
+ return ProviderAuthorizedCreate.make({
502
+ subject: publication.authority.subject,
503
+ proof: CreateAuthorizationProof.make({
504
+ kind: NonEmptyName.make("github-authenticated-repository-and-unique-tag")
505
+ })
506
+ })
507
+ }
508
+ if (plan?._tag === "CreateRelease") {
509
+ return NeedsMutation.make({
510
+ subject: publication.authority.subject,
511
+ precondition: MutationPrecondition.make({
512
+ kind: NonEmptyName.make("github-release-absent-at-exact-tag")
513
+ })
514
+ })
515
+ }
516
+ if (plan?._tag === "UploadAssets") {
517
+ return NeedsMutation.make({
518
+ subject: publication.authority.subject,
519
+ precondition: MutationPrecondition.make({
520
+ kind: NonEmptyName.make("github-intended-assets-authoritatively-absent")
521
+ })
522
+ })
523
+ }
524
+ return ProviderBlocked.make({
525
+ subject: publication.authority.subject,
526
+ reason: SafeReason.make("GitHub absence carried no exact mutation plan.")
527
+ })
528
+ case "VisibilityPending":
529
+ case "Inconclusive":
530
+ return ProviderBlocked.make({
531
+ subject: publication.authority.subject,
532
+ reason: SafeReason.make("GitHub publication truth is not yet conclusive enough to mutate.")
533
+ })
534
+ }
535
+ }
536
+
537
+ const mutationFailure = (
538
+ publication: PreparedGitHubPublication,
539
+ reason: string,
540
+ commitment: "before-dispatch" | "unknown" = "before-dispatch"
541
+ ): ReleaseSubjectError => new ReleaseSubjectError({
542
+ subject: publication.authority.subject,
543
+ phase: "mutate",
544
+ commitment,
545
+ reason: SafeReason.make(reason)
546
+ })
547
+
548
+ const unknownAttempt = (
549
+ publication: PreparedGitHubPublication,
550
+ reason: string
551
+ ): OutcomeUnknown => OutcomeUnknown.make({
552
+ subject: publication.authority.subject,
553
+ reason: SafeReason.make(reason)
554
+ })
555
+
556
+ const appliedAttempt = (publication: PreparedGitHubPublication): Applied => Applied.make({
557
+ subject: publication.authority.subject,
558
+ fact: ProviderMutationFact.make({
559
+ subject: publication.authority.subject,
560
+ detail: SafeReason.make("GitHub accepted the release mutation and every planned asset upload.")
561
+ })
562
+ })
563
+
564
+ type MutationExchange =
565
+ | { readonly _tag: "Response", readonly response: HttpResponse }
566
+ | {
567
+ readonly _tag: "Failure"
568
+ readonly error: { readonly _tag: string, readonly commitment?: "before-dispatch" | "unknown" }
569
+ }
570
+
571
+ const executeMutation = (
572
+ mutationHttp: AuthorizedMutationHttpShape,
573
+ operation: PublisherOperation,
574
+ request: MutationHttpRequest,
575
+ grant: ScopedSecret
576
+ ): Effect.Effect<MutationExchange> => mutationHttp.execute(operation, request, grant).pipe(
577
+ Effect.map((response) => ({ _tag: "Response", response } as const)),
578
+ Effect.catch((error) => Effect.succeed({ _tag: "Failure", error } as const))
579
+ )
580
+
581
+ const uploadRequest = (
582
+ publication: PreparedGitHubPublication,
583
+ uploadUrl: string,
584
+ index: number,
585
+ bytes: Uint8Array
586
+ ): MutationHttpRequest => {
587
+ const asset = publication.assets[index]!
588
+ const template = "{?name,label}"
589
+ const expanded = `${uploadUrl.slice(0, -template.length)}?name=${encodeURIComponent(asset.name)}`
590
+ return {
591
+ method: "POST",
592
+ url: expanded,
593
+ headers: {
594
+ ...githubJsonHeaders,
595
+ "content-type": asset.mediaType,
596
+ "content-length": String(bytes.length)
597
+ },
598
+ body: bytes
599
+ }
600
+ }
601
+
602
+ /**
603
+ * The exact tag ref and recursively peeled commit are established before the
604
+ * release endpoint can contribute any equality or absence fact. Release
605
+ * target_commitish is intentionally ignored: GitHub does not make it a stable
606
+ * equality fact for a release whose tag already exists.
607
+ */
608
+ export const makeGithubSubjects = (
609
+ bundle: PreparedBundle,
610
+ publication: PreparedGitHubPublication,
611
+ http: HttpAuthorizerShape,
612
+ mutationHttp: AuthorizedMutationHttpShape
613
+ ): readonly [ReleaseSubject] => {
614
+ let mutationPlan: GithubMutationPlan | undefined
615
+
616
+ const observe = Effect.fn("GitHubReleaseSubject.observe")(function*(
617
+ grant: CredentialGrant,
618
+ context: ReleaseObservationContext
619
+ ) {
620
+ mutationPlan = undefined
621
+ const mutationMayStillBecomeVisible = context.phase === "post-mutation" &&
622
+ context.attempt._tag !== "RejectedBeforeDispatch" &&
623
+ context.attempt._tag !== "RejectedByProvider"
624
+ if (grant._tag === "WorkloadIdentity") {
625
+ return yield* observationFailure(
626
+ publication,
627
+ "Workload identity cannot authorize GitHub release observation.",
628
+ "before-dispatch"
629
+ )
630
+ }
631
+ const targetCommit = publication.targetCommit.toString()
632
+ if (!gitObjectSha.test(targetCommit)) {
633
+ return inconclusive(publication, "The prepared GitHub target commit is not a canonical full Git object identifier.")
634
+ }
635
+ const intendedNames = publication.assets.map((asset) => asset.name)
636
+ if (new Set(intendedNames).size !== intendedNames.length) {
637
+ return inconclusive(publication, "The prepared GitHub release repeats an asset name and cannot define an exact set.")
638
+ }
639
+ if (publication.assets.some((asset) => bundle.blobs.get(asset.artifactId.toString()) === undefined)) {
640
+ return inconclusive(publication, "The prepared GitHub release is missing exact bytes for an intended asset.")
641
+ }
642
+
643
+ const repositoryResponse = yield* get(publication, http, grant, publication.authority.audience)
644
+ if (repositoryResponse.status !== 200) {
645
+ return inconclusive(publication, `GitHub repository visibility returned HTTP ${repositoryResponse.status}.`)
646
+ }
647
+ const repository = repositoryFacts(parseJson(repositoryResponse.body))
648
+ if (repository === undefined || repository.fullName !== publication.repository) {
649
+ return inconclusive(publication, "GitHub repository visibility did not establish the exact prepared repository coordinate.")
650
+ }
651
+
652
+ const referenceResponse = yield* get(publication, http, grant, refUrl(publication))
653
+ if (referenceResponse.status === 404) {
654
+ if (grant._tag === "AnonymousAccess") {
655
+ return inconclusive(publication, "Anonymous GitHub tag absence cannot authorize creation in the prepared namespace.")
656
+ }
657
+ const danglingRelease = yield* get(publication, http, grant, releaseUrl(publication))
658
+ if (danglingRelease.status === 200) {
659
+ return presentDifferent(publication, [scalarDifference("tag.exists", true, false)])
660
+ }
661
+ if (danglingRelease.status !== 404) {
662
+ return inconclusive(publication, `GitHub release observation returned HTTP ${danglingRelease.status}.`)
663
+ }
664
+ if (mutationMayStillBecomeVisible) {
665
+ return pending(publication, "The exact GitHub tag and release are not visible after the accepted mutation.")
666
+ }
667
+ mutationPlan = { _tag: "CreateRelease", tagAbsent: true }
668
+ return absent(
669
+ publication,
670
+ "github-authenticated-tag-and-release-absent",
671
+ "Authenticated exact-repository reads found neither the prepared tag nor its release coordinate."
672
+ )
673
+ }
674
+ if (referenceResponse.status !== 200) {
675
+ return inconclusive(publication, `GitHub tag-ref observation returned HTTP ${referenceResponse.status}.`)
676
+ }
677
+ let object = parseRef(parseJson(referenceResponse.body), `refs/tags/${publication.tag}`)
678
+ if (object === undefined) {
679
+ return inconclusive(publication, "The GitHub tag-ref response was malformed or named a different ref.")
680
+ }
681
+ const visited = new Set<string>()
682
+ for (let depth = 0; object.type === "tag"; depth += 1) {
683
+ if (depth >= maximumTagDepth || visited.has(object.sha)) {
684
+ return inconclusive(publication, "The GitHub annotated-tag chain was cyclic or exceeded its bounded peel depth.")
685
+ }
686
+ visited.add(object.sha)
687
+ const tagResponse = yield* get(publication, http, grant, tagObjectUrl(publication, object.sha))
688
+ if (tagResponse.status !== 200) {
689
+ return inconclusive(publication, `GitHub annotated-tag observation returned HTTP ${tagResponse.status}.`)
690
+ }
691
+ const peeled = parseTagObject(parseJson(tagResponse.body), object.sha)
692
+ if (peeled === undefined) {
693
+ return inconclusive(publication, "A GitHub annotated-tag object was malformed or changed identity while peeling.")
694
+ }
695
+ object = peeled
696
+ }
697
+ if (object.sha !== targetCommit) {
698
+ return presentDifferent(publication, [textDifference("tag.commit", targetCommit, object.sha)])
699
+ }
700
+
701
+ const releaseResponse = yield* get(publication, http, grant, releaseUrl(publication))
702
+ if (releaseResponse.status === 404) {
703
+ if (grant._tag === "AnonymousAccess") {
704
+ return inconclusive(publication, "Anonymous GitHub release absence may hide a draft release and is not authoritative.")
705
+ }
706
+ if (mutationMayStillBecomeVisible) {
707
+ return pending(publication, "The exact GitHub release is not yet visible after the accepted mutation.")
708
+ }
709
+ mutationPlan = { _tag: "CreateRelease", tagAbsent: false }
710
+ return absent(
711
+ publication,
712
+ "github-authenticated-release-absent",
713
+ "Authenticated exact-repository reads found the prepared tag but no release at that tag."
714
+ )
715
+ }
716
+ if (releaseResponse.status !== 200) {
717
+ return inconclusive(publication, `GitHub release observation returned HTTP ${releaseResponse.status}.`)
718
+ }
719
+ const release = releaseFacts(parseJson(releaseResponse.body))
720
+ if (release === undefined || !validateUploadUrl(publication, release.id, release.uploadUrl)) {
721
+ return inconclusive(publication, "The GitHub release response omitted identity facts or carried an invalid repository-scoped upload template.")
722
+ }
723
+ const releaseDifferences = compareReleaseMetadata(publication, release)
724
+ if (releaseDifferences.length > 0) return presentDifferent(publication, releaseDifferences)
725
+
726
+ const listed = yield* listAllAssets(publication, http, grant, release.id)
727
+ if (listed._tag === "Inconclusive") return inconclusive(publication, listed.reason)
728
+ const assetsByName = new Map<string, Array<ReleaseAssetFacts>>()
729
+ for (const asset of listed.assets) {
730
+ const candidates = assetsByName.get(asset.name) ?? []
731
+ candidates.push(asset)
732
+ assetsByName.set(asset.name, candidates)
733
+ }
734
+ const differences: Array<Difference> = []
735
+ for (const [name, candidates] of assetsByName) {
736
+ if (!intendedNames.includes(name)) {
737
+ differences.push(textDifference("assets.extra-name", "no undeclared asset", name))
738
+ }
739
+ if (candidates.length > 1) {
740
+ differences.push(scalarDifference(`asset.${name}.count`, 1, candidates.length))
741
+ }
742
+ }
743
+ const missingIndexes: Array<number> = []
744
+ const downloads: Array<{ readonly index: number, readonly facts: ReleaseAssetFacts, readonly expected: Sha256Digest }> = []
745
+ for (const [index, intended] of publication.assets.entries()) {
746
+ const candidates = assetsByName.get(intended.name) ?? []
747
+ if (candidates.length === 0) {
748
+ missingIndexes.push(index)
749
+ continue
750
+ }
751
+ if (candidates.length !== 1) continue
752
+ const observed = candidates[0]!
753
+ const bytes = bundle.blobs.get(intended.artifactId.toString())!
754
+ if (observed.size !== bytes.length) {
755
+ differences.push(scalarDifference(`asset.${intended.name}.size`, bytes.length, observed.size))
756
+ }
757
+ if (observed.mediaType !== intended.mediaType) {
758
+ differences.push(textDifference(`asset.${intended.name}.mediaType`, intended.mediaType, observed.mediaType))
759
+ }
760
+ const expected = sha256Digest(bytes)
761
+ if (observed.digest._tag === "Malformed") {
762
+ return inconclusive(publication, "A GitHub asset carried a malformed canonical sha256 digest.")
763
+ }
764
+ if (observed.digest._tag === "Missing") {
765
+ downloads.push({ index, facts: observed, expected })
766
+ } else if (!digestEquals(observed.digest.value, expected)) {
767
+ differences.push(textDifference(
768
+ `asset.${intended.name}.digest`,
769
+ formatGitHubSha256(expected),
770
+ formatGitHubSha256(observed.digest.value)
771
+ ))
772
+ }
773
+ }
774
+ if (differences.length > 0) return presentDifferent(publication, differences)
775
+ for (const download of downloads) {
776
+ const response = yield* get(
777
+ publication,
778
+ http,
779
+ grant,
780
+ download.facts.apiUrl,
781
+ "application/octet-stream"
782
+ ).pipe(
783
+ Effect.map((result) => ({ _tag: "Response", result } as const)),
784
+ Effect.catch(() => Effect.succeed({ _tag: "Unavailable" } as const))
785
+ )
786
+ if (response._tag === "Unavailable" || response.result.status !== 200) {
787
+ return inconclusive(publication, "GitHub asset bytes could not be downloaded for exact sha256 observation.")
788
+ }
789
+ const bytes = typeof response.result.body === "string"
790
+ ? new TextEncoder().encode(response.result.body)
791
+ : response.result.body
792
+ const digest = sha256Digest(bytes)
793
+ if (!digestEquals(digest, download.expected)) {
794
+ return presentDifferent(publication, [textDifference(
795
+ `asset.${publication.assets[download.index]!.name}.digest`,
796
+ formatGitHubSha256(download.expected),
797
+ formatGitHubSha256(digest)
798
+ )])
799
+ }
800
+ }
801
+ if (missingIndexes.length > 0) {
802
+ if (mutationMayStillBecomeVisible) {
803
+ return pending(publication, "One or more intended GitHub assets are not yet visible after the accepted mutation.")
804
+ }
805
+ mutationPlan = {
806
+ _tag: "UploadAssets",
807
+ releaseId: release.id,
808
+ uploadUrl: release.uploadUrl,
809
+ assetIndexes: missingIndexes
810
+ }
811
+ return absent(
812
+ publication,
813
+ "github-intended-assets-absent",
814
+ "A full paginated release-asset enumeration proved at least one intended asset name absent."
815
+ )
816
+ }
817
+ return PresentEquivalent.make({ subject: publication.authority.subject })
818
+ })
819
+
820
+ const mutate = Effect.fn("GitHubReleaseSubject.mutate")(function*(
821
+ decision: MutationDecision,
822
+ grant: MutationCredentialGrant
823
+ ): Effect.fn.Return<MutationAttempt, ReleaseSubjectError> {
824
+ if (grant._tag !== "ScopedSecret") {
825
+ return yield* mutationFailure(
826
+ publication,
827
+ "GitHub release mutation requires an opaque scoped-secret grant.",
828
+ "before-dispatch"
829
+ )
830
+ }
831
+ const plan = mutationPlan
832
+ if (decision.subject !== publication.authority.subject || plan === undefined) {
833
+ return yield* mutationFailure(publication, "The GitHub mutation decision has no matching exact observation plan.")
834
+ }
835
+ const expectedDecision = plan._tag === "CreateRelease" && plan.tagAbsent
836
+ ? decision._tag === "ProviderAuthorizedCreate" &&
837
+ decision.proof.kind === "github-authenticated-repository-and-unique-tag"
838
+ : plan._tag === "CreateRelease"
839
+ ? decision._tag === "NeedsMutation" && decision.precondition.kind === "github-release-absent-at-exact-tag"
840
+ : decision._tag === "NeedsMutation" &&
841
+ decision.precondition.kind === "github-intended-assets-authoritatively-absent"
842
+ if (!expectedDecision) {
843
+ return yield* mutationFailure(publication, "The GitHub mutation decision does not match its observed precondition.")
844
+ }
845
+ const indexes = plan._tag === "CreateRelease"
846
+ ? publication.assets.map((_, index) => index)
847
+ : [...plan.assetIndexes]
848
+ const bytesByIndex = new Map<number, Uint8Array>()
849
+ for (const index of indexes) {
850
+ const asset = publication.assets[index]
851
+ const bytes = asset === undefined ? undefined : bundle.blobs.get(asset.artifactId.toString())
852
+ if (asset === undefined || bytes === undefined) {
853
+ return yield* mutationFailure(publication, "An intended GitHub asset is unavailable before dispatch.")
854
+ }
855
+ bytesByIndex.set(index, bytes)
856
+ }
857
+ if (plan._tag === "UploadAssets" && !validateUploadUrl(publication, plan.releaseId, plan.uploadUrl)) {
858
+ return yield* mutationFailure(publication, "The observed GitHub upload template failed validation before dispatch.")
859
+ }
860
+ const operation: PublisherOperation = {
861
+ _tag: "PublishOperation",
862
+ subject: publication.authority.subject,
863
+ provider: publication.authority.provider,
864
+ audience: publication.authority.audience,
865
+ purpose: "publish",
866
+ decision
867
+ }
868
+ let acceptedWrites = 0
869
+ const dispatch = Effect.fn("GitHubReleaseSubject.dispatch")(function*(request: MutationHttpRequest) {
870
+ const exchange = yield* executeMutation(mutationHttp, operation, request, grant)
871
+ if (exchange._tag === "Failure") {
872
+ if (acceptedWrites > 0 || exchange.error.commitment === "unknown") {
873
+ return { _tag: "Attempt", attempt: unknownAttempt(
874
+ publication,
875
+ "GitHub transport outcome became unknown after mutation dispatch."
876
+ ) } as const
877
+ }
878
+ return yield* mutationFailure(
879
+ publication,
880
+ "The authorized GitHub HTTP boundary rejected mutation before dispatch.",
881
+ "before-dispatch"
882
+ )
883
+ }
884
+ if (exchange.response.status === 201) {
885
+ acceptedWrites += 1
886
+ return { _tag: "Accepted", response: exchange.response } as const
887
+ }
888
+ return { _tag: "Attempt", attempt: unknownAttempt(
889
+ publication,
890
+ `GitHub returned conservative mutation status ${exchange.response.status}; exact outcome requires reobservation.`
891
+ ) } as const
892
+ })
893
+
894
+ let uploadUrl: string
895
+ if (plan._tag === "CreateRelease") {
896
+ const body = JSON.stringify({
897
+ tag_name: publication.tag.toString(),
898
+ target_commitish: publication.targetCommit.toString(),
899
+ name: publication.title.toString(),
900
+ body: publication.body ?? "",
901
+ draft: publication.draft,
902
+ prerelease: publication.prerelease
903
+ })
904
+ const created = yield* dispatch({
905
+ method: "POST",
906
+ url: `${publication.authority.audience}/releases`,
907
+ headers: {
908
+ ...githubJsonHeaders,
909
+ "content-type": "application/json",
910
+ "content-length": String(new TextEncoder().encode(body).length)
911
+ },
912
+ body
913
+ })
914
+ if (created._tag === "Attempt") return created.attempt
915
+ const facts = releaseFacts(parseJson(created.response.body))
916
+ if (facts === undefined || !validateUploadUrl(publication, facts.id, facts.uploadUrl) ||
917
+ compareReleaseMetadata(publication, facts).length > 0) {
918
+ return unknownAttempt(
919
+ publication,
920
+ "GitHub accepted release creation but its response did not establish the exact release identity and repository-scoped upload template."
921
+ )
922
+ }
923
+ uploadUrl = facts.uploadUrl
924
+ } else {
925
+ uploadUrl = plan.uploadUrl
926
+ }
927
+ for (const index of indexes) {
928
+ const bytes = bytesByIndex.get(index)!
929
+ const uploaded = yield* dispatch(uploadRequest(publication, uploadUrl, index, bytes))
930
+ if (uploaded._tag === "Attempt") return uploaded.attempt
931
+ const intended = publication.assets[index]!
932
+ const facts = releaseAssetFacts(publication, parseJson(uploaded.response.body))
933
+ if (facts === undefined || facts.name !== intended.name || facts.size !== bytes.length ||
934
+ facts.mediaType !== intended.mediaType || facts.digest._tag === "Malformed" ||
935
+ (facts.digest._tag === "Present" && !digestEquals(facts.digest.value, sha256Digest(bytes)))) {
936
+ return unknownAttempt(
937
+ publication,
938
+ "GitHub accepted an asset upload but its response did not establish the exact uploaded asset identity."
939
+ )
940
+ }
941
+ }
942
+ return appliedAttempt(publication)
943
+ })
944
+
945
+ return [{
946
+ id: publication.authority.subject,
947
+ recovery: githubRecoveryCapabilityProfile,
948
+ observationRequests: observationRequests(publication),
949
+ mutationRequest: mutationRequest(publication),
950
+ observe,
951
+ decide: (observation) => decideFor(publication, observation, mutationPlan),
952
+ mutate
953
+ }]
954
+ }