unaltraweb 0.3.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 (444) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/Makefile +412 -0
  4. data/README.md +226 -0
  5. data/_config.yml +842 -0
  6. data/_data/i18n/ca.yml +278 -0
  7. data/_data/i18n/en.yml +278 -0
  8. data/_data/i18n/es.yml +278 -0
  9. data/_includes/audio.liquid +30 -0
  10. data/_includes/bib_search.liquid +5 -0
  11. data/_includes/blog-list.liquid +66 -0
  12. data/_includes/book-citation.liquid +33 -0
  13. data/_includes/citation.liquid +26 -0
  14. data/_includes/custom_head.liquid +3 -0
  15. data/_includes/custom_scripts.liquid +5 -0
  16. data/_includes/cv/list.liquid +5 -0
  17. data/_includes/cv/list_groups.liquid +51 -0
  18. data/_includes/cv/map.liquid +29 -0
  19. data/_includes/cv/nested_list.liquid +16 -0
  20. data/_includes/cv/time_table.liquid +118 -0
  21. data/_includes/cv-download-card.liquid +26 -0
  22. data/_includes/developer-mode-switcher.liquid +24 -0
  23. data/_includes/disqus.liquid +13 -0
  24. data/_includes/distill_scripts.liquid +298 -0
  25. data/_includes/documentation-download-detail.liquid +76 -0
  26. data/_includes/documentation-downloads.liquid +71 -0
  27. data/_includes/documentation-profile-attributes.liquid +4 -0
  28. data/_includes/documentation-section-toc.liquid +9 -0
  29. data/_includes/documentation-sidebar.liquid +112 -0
  30. data/_includes/figure.liquid +87 -0
  31. data/_includes/footer.liquid +66 -0
  32. data/_includes/giscus.liquid +25 -0
  33. data/_includes/head.liquid +220 -0
  34. data/_includes/header.liquid +544 -0
  35. data/_includes/latest_posts.liquid +61 -0
  36. data/_includes/localized-date.liquid +5 -0
  37. data/_includes/manual-bibliography.liquid +2 -0
  38. data/_includes/manual-featured-readings.liquid +12 -0
  39. data/_includes/manual-other-readings.liquid +12 -0
  40. data/_includes/manual-page-toc.liquid +11 -0
  41. data/_includes/manual-sidebar.liquid +58 -0
  42. data/_includes/manual-teachers.liquid +74 -0
  43. data/_includes/metadata.liquid +278 -0
  44. data/_includes/news.liquid +48 -0
  45. data/_includes/newsletter.liquid +68 -0
  46. data/_includes/page-hero.liquid +96 -0
  47. data/_includes/page-link.liquid +8 -0
  48. data/_includes/pagination.liquid +22 -0
  49. data/_includes/post-reading-time.liquid +29 -0
  50. data/_includes/previous-next.liquid +37 -0
  51. data/_includes/profile-card.liquid +49 -0
  52. data/_includes/profile-feature-matrix.liquid +75 -0
  53. data/_includes/profile-highlights.liquid +122 -0
  54. data/_includes/project-grid.liquid +26 -0
  55. data/_includes/project-resource-link.liquid +45 -0
  56. data/_includes/project-resources.liquid +21 -0
  57. data/_includes/projects.liquid +38 -0
  58. data/_includes/projects_horizontal.liquid +37 -0
  59. data/_includes/publications-metrics-summary.liquid +69 -0
  60. data/_includes/publications-summary.liquid +127 -0
  61. data/_includes/reading-biblio-controls.liquid +65 -0
  62. data/_includes/reading-cover-card.liquid +93 -0
  63. data/_includes/reading-rating.liquid +11 -0
  64. data/_includes/recommended-readings.liquid +30 -0
  65. data/_includes/related_posts.liquid +39 -0
  66. data/_includes/repository/repo.liquid +48 -0
  67. data/_includes/repository/repo_trophies.liquid +44 -0
  68. data/_includes/repository/repo_user.liquid +35 -0
  69. data/_includes/resume/awards.liquid +19 -0
  70. data/_includes/resume/basics.liquid +28 -0
  71. data/_includes/resume/certificates.liquid +35 -0
  72. data/_includes/resume/education.liquid +54 -0
  73. data/_includes/resume/interests.liquid +34 -0
  74. data/_includes/resume/languages.liquid +31 -0
  75. data/_includes/resume/projects.liquid +32 -0
  76. data/_includes/resume/publications.liquid +28 -0
  77. data/_includes/resume/references.liquid +31 -0
  78. data/_includes/resume/skills.liquid +33 -0
  79. data/_includes/resume/volunteer.liquid +52 -0
  80. data/_includes/resume/work.liquid +52 -0
  81. data/_includes/scripts.liquid +364 -0
  82. data/_includes/search-data.js.liquid +341 -0
  83. data/_includes/selected_papers.liquid +3 -0
  84. data/_includes/t.liquid +31 -0
  85. data/_includes/team-grid.liquid +172 -0
  86. data/_includes/video.liquid +97 -0
  87. data/_layouts/about.liquid +93 -0
  88. data/_layouts/archive.liquid +45 -0
  89. data/_layouts/bib.liquid +677 -0
  90. data/_layouts/book-review.liquid +225 -0
  91. data/_layouts/book-shelf.liquid +66 -0
  92. data/_layouts/cv.liquid +127 -0
  93. data/_layouts/default.liquid +70 -0
  94. data/_layouts/distill.liquid +132 -0
  95. data/_layouts/documentation-download-detail.liquid +5 -0
  96. data/_layouts/documentation-home.liquid +100 -0
  97. data/_layouts/documentation-page.liquid +45 -0
  98. data/_layouts/manual-bib.liquid +73 -0
  99. data/_layouts/manual-chapter.liquid +99 -0
  100. data/_layouts/manual-featured-bib.liquid +100 -0
  101. data/_layouts/manual-home.liquid +120 -0
  102. data/_layouts/none.liquid +1 -0
  103. data/_layouts/outputs.liquid +29 -0
  104. data/_layouts/page.liquid +52 -0
  105. data/_layouts/post.liquid +95 -0
  106. data/_layouts/profile.liquid +36 -0
  107. data/_layouts/profiles.liquid +36 -0
  108. data/_layouts/project.liquid +7 -0
  109. data/_layouts/repositories.liquid +58 -0
  110. data/_layouts/theses.liquid +126 -0
  111. data/_plugins/bibliography_profiles.rb +164 -0
  112. data/_plugins/callouts.rb +104 -0
  113. data/_plugins/code_blocks.rb +277 -0
  114. data/_plugins/computation_figure_images.rb +266 -0
  115. data/_plugins/computation_sources.rb +42 -0
  116. data/_plugins/content_search_index.rb +210 -0
  117. data/_plugins/details.rb +23 -0
  118. data/_plugins/external-posts.rb +124 -0
  119. data/_plugins/figure_captions.rb +800 -0
  120. data/_plugins/file-exists.rb +22 -0
  121. data/_plugins/google-scholar-citations.rb +86 -0
  122. data/_plugins/hide-custom-bibtex.rb +18 -0
  123. data/_plugins/inspirehep-citations.rb +57 -0
  124. data/_plugins/localized_visual_sources.rb +145 -0
  125. data/_plugins/manual_release_metadata.rb +47 -0
  126. data/_plugins/mermaid_mmd_images.rb +233 -0
  127. data/_plugins/profile-pages.rb +28 -0
  128. data/_plugins/remove-accents.rb +32 -0
  129. data/_plugins/reproducible_build_time.rb +55 -0
  130. data/_plugins/search-data.rb +83 -0
  131. data/_plugins/theme-cache-bust.rb +56 -0
  132. data/_plugins/vega_visual_images.rb +290 -0
  133. data/_plugins/web_capture_images.rb +154 -0
  134. data/_sass/_base.scss +2133 -0
  135. data/_sass/_blog.scss +67 -0
  136. data/_sass/_callouts.scss +100 -0
  137. data/_sass/_custom.scss +437 -0
  138. data/_sass/_cv.scss +274 -0
  139. data/_sass/_developer.scss +97 -0
  140. data/_sass/_distill.scss +185 -0
  141. data/_sass/_documentation.scss +2220 -0
  142. data/_sass/_layout.scss +218 -0
  143. data/_sass/_manual.scss +2691 -0
  144. data/_sass/_profile.scss +323 -0
  145. data/_sass/_projects.scss +163 -0
  146. data/_sass/_reading.scss +465 -0
  147. data/_sass/_site-custom.scss +3 -0
  148. data/_sass/_tabs.scss +48 -0
  149. data/_sass/_team.scss +230 -0
  150. data/_sass/_themes.scss +254 -0
  151. data/_sass/_typograms.scss +132 -0
  152. data/_sass/_variables.scss +52 -0
  153. data/_sass/font-awesome/_animated.scss +152 -0
  154. data/_sass/font-awesome/_bordered-pulled.scss +20 -0
  155. data/_sass/font-awesome/_core.scss +49 -0
  156. data/_sass/font-awesome/_fixed-width.scss +7 -0
  157. data/_sass/font-awesome/_functions.scss +57 -0
  158. data/_sass/font-awesome/_icons.scss +13 -0
  159. data/_sass/font-awesome/_list.scss +18 -0
  160. data/_sass/font-awesome/_mixins.scss +65 -0
  161. data/_sass/font-awesome/_rotated-flipped.scss +31 -0
  162. data/_sass/font-awesome/_screen-reader.scss +14 -0
  163. data/_sass/font-awesome/_shims.scss +1578 -0
  164. data/_sass/font-awesome/_sizing.scss +16 -0
  165. data/_sass/font-awesome/_stacked.scss +32 -0
  166. data/_sass/font-awesome/_variables.scss +5044 -0
  167. data/_sass/font-awesome/brands.scss +30 -0
  168. data/_sass/font-awesome/fontawesome.scss +21 -0
  169. data/_sass/font-awesome/regular.scss +27 -0
  170. data/_sass/font-awesome/solid.scss +27 -0
  171. data/_sass/font-awesome/v4-shims.scss +11 -0
  172. data/_sass/tabler-icons/tabler-icons-filled.scss +1646 -0
  173. data/_sass/tabler-icons/tabler-icons-outline.scss +9859 -0
  174. data/_sass/tabler-icons/tabler-icons.scss +11463 -0
  175. data/_scripts/cronitor-analytics-setup.js +9 -0
  176. data/_scripts/giscus-setup.js +48 -0
  177. data/_scripts/google-analytics-setup.js +9 -0
  178. data/_scripts/open-panel-analytics-setup.js +14 -0
  179. data/_scripts/photoswipe-setup.js +11 -0
  180. data/assets/audio/epicaly-short-113909.mp3 +0 -0
  181. data/assets/bibliography/2018-12-22-distill.bib +7 -0
  182. data/assets/css/academicons.min.css +1 -0
  183. data/assets/css/bootstrap-toc.min.css +4 -0
  184. data/assets/css/bootstrap.min.css +7 -0
  185. data/assets/css/bootstrap.min.css.map +1 -0
  186. data/assets/css/jekyll-pygments-themes-github.css +209 -0
  187. data/assets/css/jekyll-pygments-themes-native.css +232 -0
  188. data/assets/css/jupyter-grade3.css +3758 -0
  189. data/assets/css/jupyter-monokai.css +3758 -0
  190. data/assets/css/jupyter.css +383 -0
  191. data/assets/css/main.scss +36 -0
  192. data/assets/css/mdb.min.css +48 -0
  193. data/assets/css/mdb.min.css.map +1 -0
  194. data/assets/css/scholar-icons.css +386 -0
  195. data/assets/fonts/academicons.ttf +0 -0
  196. data/assets/fonts/academicons.woff +0 -0
  197. data/assets/fonts/scholar-icons.ttf +0 -0
  198. data/assets/fonts/scholar-icons.woff +0 -0
  199. data/assets/fonts/tabler-icons-filled.ttf +0 -0
  200. data/assets/fonts/tabler-icons-filled.woff +0 -0
  201. data/assets/fonts/tabler-icons-filled.woff2 +0 -0
  202. data/assets/fonts/tabler-icons-outline.ttf +0 -0
  203. data/assets/fonts/tabler-icons-outline.woff +0 -0
  204. data/assets/fonts/tabler-icons-outline.woff2 +0 -0
  205. data/assets/fonts/tabler-icons.ttf +0 -0
  206. data/assets/fonts/tabler-icons.woff +0 -0
  207. data/assets/fonts/tabler-icons.woff2 +0 -0
  208. data/assets/html/relativity.html +15 -0
  209. data/assets/img/1.jpg +0 -0
  210. data/assets/img/10.jpg +0 -0
  211. data/assets/img/11.jpg +0 -0
  212. data/assets/img/12.jpg +0 -0
  213. data/assets/img/2.jpg +0 -0
  214. data/assets/img/3.jpg +0 -0
  215. data/assets/img/4.jpg +0 -0
  216. data/assets/img/5.jpg +0 -0
  217. data/assets/img/6.jpg +0 -0
  218. data/assets/img/7.jpg +0 -0
  219. data/assets/img/8.jpg +0 -0
  220. data/assets/img/9.jpg +0 -0
  221. data/assets/img/book_covers/the_godfather.jpg +0 -0
  222. data/assets/img/brand/dosquartsdedocs-logo-cafe.svg +31 -0
  223. data/assets/img/brand/dosquartsdedocs-logo-footer-cafe.svg +31 -0
  224. data/assets/img/brand/dosquartsdedocs-logo-white.svg +31 -0
  225. data/assets/img/brand/dosquartsdedocs-logo.svg +31 -0
  226. data/assets/img/prof_pic.jpg +0 -0
  227. data/assets/img/prof_pic_color.png +0 -0
  228. data/assets/img/profile-placeholder.svg +8 -0
  229. data/assets/img/publication_preview/brownian-motion.gif +0 -0
  230. data/assets/img/publication_preview/goodchild1992-gis-cover.jpg +0 -0
  231. data/assets/img/publication_preview/gutierrez2020-jtrg-preview.jpg +0 -0
  232. data/assets/img/publication_preview/harvey1989-postmodernity-cover.jpg +0 -0
  233. data/assets/img/publication_preview/tobler1970-economic-geography-cover.jpg +0 -0
  234. data/assets/img/publication_preview/tuan1977-space-place-cover.jpg +0 -0
  235. data/assets/img/publication_preview/wave-mechanics.gif +0 -0
  236. data/assets/img/publication_preview/zaragozi2025-rooftop-pv-preview.jpg +0 -0
  237. data/assets/img/rhino.png +0 -0
  238. data/assets/img/team/aaron-gutierrez.png +0 -0
  239. data/assets/img/team/alba-font.png +0 -0
  240. data/assets/img/team/alba-viana.png +0 -0
  241. data/assets/img/team/alejandro-gonzalez.png +0 -0
  242. data/assets/img/team/ana-pastor.png +0 -0
  243. data/assets/img/team/antoni-domenech.png +0 -0
  244. data/assets/img/team/benito-zaragozi.png +0 -0
  245. data/assets/img/team/cinta-sanz.png +0 -0
  246. data/assets/img/team/daniel-miravet.png +0 -0
  247. data/assets/img/team/eugenia-altamirano.png +0 -0
  248. data/assets/img/team/fiammetta-brandajs.png +0 -0
  249. data/assets/img/team/francesc-gonzalez.png +0 -0
  250. data/assets/img/team/isabel-paulino.png +0 -0
  251. data/assets/img/team/joan-borras.png +0 -0
  252. data/assets/img/team/julie-wilson.png +0 -0
  253. data/assets/img/team/liliana-sole.png +0 -0
  254. data/assets/img/team/maria-casado.png +0 -0
  255. data/assets/img/team/maria-medina.png +0 -0
  256. data/assets/img/team/marta-nello.png +0 -0
  257. data/assets/img/team/mogrify +1 -0
  258. data/assets/img/team/oscar-saladie.png +0 -0
  259. data/assets/img/team/paolo-russo.png +0 -0
  260. data/assets/img/team/raquel-santos.png +0 -0
  261. data/assets/img/team/salvador-anton.png +0 -0
  262. data/assets/img/team/sunsi-huertas.png +0 -0
  263. data/assets/img/team/user-placeholder.png +0 -0
  264. data/assets/img/team/wilbert-den-hoed.png +0 -0
  265. data/assets/img/team/xavier-delclos.png +0 -0
  266. data/assets/img/template_error.png +0 -0
  267. data/assets/js/bibsearch.js +70 -0
  268. data/assets/js/bootstrap-toc.min.js +5 -0
  269. data/assets/js/bootstrap.bundle.min.js +7 -0
  270. data/assets/js/bootstrap.bundle.min.js.map +1 -0
  271. data/assets/js/callouts.js +115 -0
  272. data/assets/js/chartjs-setup.js +14 -0
  273. data/assets/js/common.js +117 -0
  274. data/assets/js/content-search-match.js +159 -0
  275. data/assets/js/content-search.js +441 -0
  276. data/assets/js/copy_code.js +60 -0
  277. data/assets/js/custom.js +6 -0
  278. data/assets/js/developer-mode.js +103 -0
  279. data/assets/js/diff2html-setup.js +20 -0
  280. data/assets/js/distillpub/overrides.js +27 -0
  281. data/assets/js/distillpub/template.v2.js +9616 -0
  282. data/assets/js/distillpub/template.v2.js.map +1 -0
  283. data/assets/js/distillpub/transforms.v2.js +14557 -0
  284. data/assets/js/distillpub/transforms.v2.js.map +1 -0
  285. data/assets/js/documentation.js +435 -0
  286. data/assets/js/echarts-setup.js +29 -0
  287. data/assets/js/highlight-search-term.js +110 -0
  288. data/assets/js/jupyter_new_tab.js +18 -0
  289. data/assets/js/leaflet-setup.js +22 -0
  290. data/assets/js/manual.js +308 -0
  291. data/assets/js/masonry.js +12 -0
  292. data/assets/js/mathjax-setup.js +46 -0
  293. data/assets/js/mermaid-setup.js +37 -0
  294. data/assets/js/newsletter.js +105 -0
  295. data/assets/js/no_defer.js +24 -0
  296. data/assets/js/plotly-setup.js +52 -0
  297. data/assets/js/profile-nav.js +92 -0
  298. data/assets/js/progress-bar.js +73 -0
  299. data/assets/js/pseudocode-setup.js +18 -0
  300. data/assets/js/search/base-styles.min.js +286 -0
  301. data/assets/js/search/command-score.min.js +7 -0
  302. data/assets/js/search/hotkeys-js/hotkeys.esm.min.js +18 -0
  303. data/assets/js/search/lit/decorators.min.js +7 -0
  304. data/assets/js/search/lit/directives/class-map.min.js +7 -0
  305. data/assets/js/search/lit/directives/join.min.js +7 -0
  306. data/assets/js/search/lit/directives/live.min.js +7 -0
  307. data/assets/js/search/lit/directives/ref.min.js +7 -0
  308. data/assets/js/search/lit/directives/repeat.min.js +7 -0
  309. data/assets/js/search/lit/directives/unsafe-html.min.js +7 -0
  310. data/assets/js/search/lit/index.min.js +7 -0
  311. data/assets/js/search/lit-element/lit-element.min.js +12 -0
  312. data/assets/js/search/lit-html/directives/class-map.min.js +22 -0
  313. data/assets/js/search/lit-html/directives/join.min.js +12 -0
  314. data/assets/js/search/lit-html/directives/live.min.js +28 -0
  315. data/assets/js/search/lit-html/directives/ref.min.js +32 -0
  316. data/assets/js/search/lit-html/directives/repeat.min.js +27 -0
  317. data/assets/js/search/lit-html/directives/unsafe-html.min.js +22 -0
  318. data/assets/js/search/lit-html/is-server.min.js +12 -0
  319. data/assets/js/search/lit-html/lit-html.min.js +12 -0
  320. data/assets/js/search/ninja-action.min.js +101 -0
  321. data/assets/js/search/ninja-footer.min.js +62 -0
  322. data/assets/js/search/ninja-header.min.js +78 -0
  323. data/assets/js/search/ninja-keys.min.js +39 -0
  324. data/assets/js/search/reactive-element/decorators/custom-element.min.js +12 -0
  325. data/assets/js/search/reactive-element/decorators/event-options.min.js +12 -0
  326. data/assets/js/search/reactive-element/decorators/property.min.js +22 -0
  327. data/assets/js/search/reactive-element/decorators/query-all.min.js +17 -0
  328. data/assets/js/search/reactive-element/decorators/query-assigned-elements.min.js +17 -0
  329. data/assets/js/search/reactive-element/decorators/query-assigned-nodes.min.js +17 -0
  330. data/assets/js/search/reactive-element/decorators/query-async.min.js +17 -0
  331. data/assets/js/search/reactive-element/decorators/query.min.js +17 -0
  332. data/assets/js/search/reactive-element/decorators/state.min.js +27 -0
  333. data/assets/js/search/reactive-element/reactive-element.min.js +17 -0
  334. data/assets/js/search-setup.js +17 -0
  335. data/assets/js/shortcut-key.js +11 -0
  336. data/assets/js/tabs.min.js +5 -0
  337. data/assets/js/theme.js +314 -0
  338. data/assets/js/tooltips-setup.js +3 -0
  339. data/assets/js/typograms.js +1341 -0
  340. data/assets/js/vanilla-back-to-top.min.js +1 -0
  341. data/assets/js/vega-setup.js +24 -0
  342. data/assets/js/venobox-setup.js +5 -0
  343. data/assets/js/wechat.js +14 -0
  344. data/assets/js/zoom.js +15 -0
  345. data/assets/json/resume.json +98 -0
  346. data/assets/json/table_data.json +128 -0
  347. data/assets/jupyter/blog.ipynb +48 -0
  348. data/assets/pdf/example_pdf.pdf +0 -0
  349. data/assets/plotly/demo.html +72 -0
  350. data/assets/video/pexels-engin-akyurt-6069112-960x540-30fps.mp4 +0 -0
  351. data/assets/webfonts/fa-brands-400.ttf +0 -0
  352. data/assets/webfonts/fa-brands-400.woff2 +0 -0
  353. data/assets/webfonts/fa-regular-400.ttf +0 -0
  354. data/assets/webfonts/fa-regular-400.woff2 +0 -0
  355. data/assets/webfonts/fa-solid-900.ttf +0 -0
  356. data/assets/webfonts/fa-solid-900.woff2 +0 -0
  357. data/assets/webfonts/fa-v4compatibility.ttf +0 -0
  358. data/assets/webfonts/fa-v4compatibility.woff2 +0 -0
  359. data/docs/Gemfile +5 -0
  360. data/docs/_config.yml +83 -0
  361. data/docs/_documentation/en/01-quick-start.md +115 -0
  362. data/docs/_documentation/en/02-tools.md +146 -0
  363. data/docs/_documentation/en/03-usage.md +107 -0
  364. data/docs/_documentation/en/04-jekyll-basics.md +51 -0
  365. data/docs/_documentation/en/05-pages-collections.md +68 -0
  366. data/docs/_documentation/en/06-github-web-editing.md +92 -0
  367. data/docs/_documentation/en/10-profiles.md +125 -0
  368. data/docs/_documentation/en/11-unaltreselfie.md +39 -0
  369. data/docs/_documentation/en/12-unaltreprojecte.md +38 -0
  370. data/docs/_documentation/en/13-unaltremanual.md +237 -0
  371. data/docs/_documentation/en/14-unaltredocs.md +92 -0
  372. data/docs/_documentation/en/15-profile-checklist.md +39 -0
  373. data/docs/_documentation/en/20-syntax.md +353 -0
  374. data/docs/_documentation/en/21-themes.md +103 -0
  375. data/docs/_documentation/en/22-feature-reference.md +56 -0
  376. data/docs/_documentation/en/23-front-matter.md +51 -0
  377. data/docs/_documentation/en/24-data-files.md +28 -0
  378. data/docs/_documentation/en/30-customization.md +448 -0
  379. data/docs/_documentation/en/31-template.md +55 -0
  380. data/docs/_documentation/en/32-development.md +108 -0
  381. data/docs/_documentation/en/33-reusable-workflows.md +136 -0
  382. data/docs/_documentation/en/40-distribution.md +164 -0
  383. data/docs/_documentation/en/41-bibliometrics.md +137 -0
  384. data/docs/_documentation/en/42-docker-image.md +185 -0
  385. data/docs/_documentation/en/50-faq.md +33 -0
  386. data/docs/_pages/ca/index.md +35 -0
  387. data/docs/_pages/en/index.md +35 -0
  388. data/docs/_pages/es/index.md +35 -0
  389. data/docs/agents/action-prompts/00-start-site-session.txt +10 -0
  390. data/docs/agents/action-prompts/05-create-new-web.txt +5 -0
  391. data/docs/agents/action-prompts/10-content-update.txt +9 -0
  392. data/docs/agents/action-prompts/15-edit-default-content.txt +8 -0
  393. data/docs/agents/action-prompts/20-manual-teaching-materials.txt +18 -0
  394. data/docs/agents/action-prompts/22-manual-style-audit.txt +14 -0
  395. data/docs/agents/action-prompts/23-manual-structure-audit.txt +17 -0
  396. data/docs/agents/action-prompts/25-translation-prepublish.txt +8 -0
  397. data/docs/agents/action-prompts/30-project-site-update.txt +7 -0
  398. data/docs/agents/action-prompts/40-documentation-update.txt +7 -0
  399. data/docs/agents/action-prompts/50-bibliography-entry.txt +7 -0
  400. data/docs/agents/action-prompts/60-bibliometrics-refresh.txt +7 -0
  401. data/docs/agents/action-prompts/70-build-and-review.txt +12 -0
  402. data/docs/agents/manual-authoring-components.md +255 -0
  403. data/docs/agents/mcp-contract.md +162 -0
  404. data/docs/agents/quarto-computation-adoption.md +102 -0
  405. data/docs/assets/img/unaltraweb-docs-hero.svg +36 -0
  406. data/lib/unaltraweb/version.rb +8 -0
  407. data/lib/unaltraweb.rb +31 -0
  408. data/requirements.txt +5 -0
  409. data/scripts/biblio/fetch_scimago_csv.sh +126 -0
  410. data/scripts/biblio/metrics_common.py +154 -0
  411. data/scripts/biblio/metrics_merge_meta.py +73 -0
  412. data/scripts/biblio/metrics_update.py +826 -0
  413. data/scripts/computations/python/Dockerfile +37 -0
  414. data/scripts/computations/r/Dockerfile +31 -0
  415. data/scripts/computations/render.py +1267 -0
  416. data/scripts/cv/render_pdf_preview.sh +36 -0
  417. data/scripts/deploy/publish_branch.sh +80 -0
  418. data/scripts/generate_logo_variants.py +47 -0
  419. data/scripts/manual/Dockerfile +31 -0
  420. data/scripts/manual/build_pdf.py +1780 -0
  421. data/scripts/manual/filters/bibliography.lua +164 -0
  422. data/scripts/manual/filters/code-blocks.lua +102 -0
  423. data/scripts/manual/filters/figure-captions.lua +35 -0
  424. data/scripts/manual/publish_release.sh +281 -0
  425. data/scripts/manual/templates/manual.tex +494 -0
  426. data/scripts/manual/verify_release_assets.py +650 -0
  427. data/scripts/test_gem_build.py +128 -0
  428. data/scripts/test_reproducible_jekyll_build.py +191 -0
  429. data/scripts/test_wheel_install.py +282 -0
  430. data/scripts/unaltraweb-docker-mount.sh +36 -0
  431. data/scripts/unaltraweb-mcp-bootstrap.sh +105 -0
  432. data/scripts/unaltraweb-mcp-cleanup.sh +73 -0
  433. data/scripts/unaltraweb-mcp-project-id.sh +34 -0
  434. data/scripts/validate_distribution.py +563 -0
  435. data/scripts/validate_workflows.py +1285 -0
  436. data/scripts/web_captures/Dockerfile +31 -0
  437. data/scripts/web_captures/capture.mjs +157 -0
  438. data/scripts/web_captures/package-lock.json +56 -0
  439. data/scripts/web_captures/package.json +9 -0
  440. data/scripts/web_captures/render.py +867 -0
  441. data/src/unaltraweb_mcp/component-contract.json +153 -0
  442. data/src/unaltraweb_mcp/component-contract.schema.json +87 -0
  443. data/src/unaltraweb_mcp/docker_mount.py +19 -0
  444. metadata +861 -0
@@ -0,0 +1,164 @@
1
+ local references = {}
2
+ local sort_keys = {}
3
+ local access = {}
4
+
5
+ local function has_class(div, expected)
6
+ for _, class in ipairs(div.classes) do
7
+ if class == expected then
8
+ return true
9
+ end
10
+ end
11
+ return false
12
+ end
13
+
14
+ local function fold(value)
15
+ local lower = pandoc.text and pandoc.text.lower or string.lower
16
+ value = lower(value)
17
+ local replacements = {
18
+ ["à"] = "a", ["á"] = "a", ["â"] = "a", ["ä"] = "a", ["ã"] = "a", ["å"] = "a",
19
+ ["ç"] = "c", ["è"] = "e", ["é"] = "e", ["ê"] = "e", ["ë"] = "e",
20
+ ["ì"] = "i", ["í"] = "i", ["î"] = "i", ["ï"] = "i", ["ñ"] = "n",
21
+ ["ò"] = "o", ["ó"] = "o", ["ô"] = "o", ["ö"] = "o", ["õ"] = "o",
22
+ ["ù"] = "u", ["ú"] = "u", ["û"] = "u", ["ü"] = "u", ["ý"] = "y", ["ÿ"] = "y"
23
+ }
24
+ for source, replacement in pairs(replacements) do
25
+ value = value:gsub(source, replacement)
26
+ end
27
+ return value:gsub("%s+", " ")
28
+ end
29
+
30
+ local function sort_references(items)
31
+ table.sort(items, function(left, right)
32
+ local left_text = pandoc.utils.stringify(left)
33
+ local right_text = pandoc.utils.stringify(right)
34
+ local left_id = left.identifier:match("^ref%-(.+)$")
35
+ local right_id = right.identifier:match("^ref%-(.+)$")
36
+ local left_key = sort_keys[left_id] or fold(left_text)
37
+ local right_key = sort_keys[right_id] or fold(right_text)
38
+ if left_key == right_key then
39
+ return left_text < right_text
40
+ end
41
+ return left_key < right_key
42
+ end)
43
+ end
44
+
45
+ local function append_link(entry, label, target)
46
+ local link = pandoc.Link(label, target)
47
+ local block = entry.content[#entry.content]
48
+ if block and (block.t == "Para" or block.t == "Plain") then
49
+ table.insert(block.content, pandoc.Space())
50
+ table.insert(block.content, link)
51
+ else
52
+ table.insert(entry.content, pandoc.Para({link}))
53
+ end
54
+ end
55
+
56
+ local function ensure_access(entry, key)
57
+ local values = access[key]
58
+ if not values then
59
+ return
60
+ end
61
+ local rendered = pandoc.utils.stringify(entry)
62
+ local doi = values.doi or ""
63
+ if doi ~= "" then
64
+ doi = doi:gsub("^[Dd][Oo][Ii]:%s*", "")
65
+ doi = doi:gsub("^https?://[Dd][Xx]%.[Dd][Oo][Ii]%.org/", "")
66
+ doi = doi:gsub("^https?://[Dd][Oo][Ii]%.org/", "")
67
+ if not rendered:find(doi, 1, true) then
68
+ local doi_url = "https://doi.org/" .. doi
69
+ append_link(entry, doi_url, doi_url)
70
+ rendered = rendered .. " " .. doi_url
71
+ end
72
+ end
73
+ for _, url in ipairs(values.urls or {}) do
74
+ if url ~= "" and not rendered:find(url, 1, true) then
75
+ append_link(entry, url, url)
76
+ rendered = rendered .. " " .. url
77
+ end
78
+ end
79
+ end
80
+
81
+ local function without_identifier(entry)
82
+ return pandoc.Div(entry.content, pandoc.Attr("", entry.classes, entry.attributes))
83
+ end
84
+
85
+ function Pandoc(document)
86
+ references = {}
87
+ sort_keys = {}
88
+ access = {}
89
+ for key, value in pairs(document.meta["bibliography-sort-keys"] or {}) do
90
+ sort_keys[key] = pandoc.utils.stringify(value)
91
+ end
92
+ for key, value in pairs(document.meta["bibliography-access"] or {}) do
93
+ access[key] = {
94
+ doi = pandoc.utils.stringify(value.doi or ""),
95
+ urls = {}
96
+ }
97
+ for _, url in ipairs(value.urls or {}) do
98
+ table.insert(access[key].urls, pandoc.utils.stringify(url))
99
+ end
100
+ end
101
+
102
+ document = document:walk({
103
+ Div = function(div)
104
+ if div.identifier ~= "refs" then
105
+ return nil
106
+ end
107
+
108
+ local sorted = {}
109
+ for _, block in ipairs(div.content) do
110
+ if block.t == "Div" and block.identifier:match("^ref%-") then
111
+ local key = block.identifier:sub(5)
112
+ ensure_access(block, key)
113
+ references[key] = block
114
+ table.insert(sorted, block)
115
+ end
116
+ end
117
+ sort_references(sorted)
118
+
119
+ local index = 1
120
+ for position, block in ipairs(div.content) do
121
+ if block.t == "Div" and block.identifier:match("^ref%-") then
122
+ div.content[position] = sorted[index]
123
+ index = index + 1
124
+ end
125
+ end
126
+ return div
127
+ end
128
+ })
129
+
130
+ local heading = "References"
131
+ if document.meta["chapter-references-title"] then
132
+ heading = pandoc.utils.stringify(document.meta["chapter-references-title"])
133
+ end
134
+
135
+ document = document:walk({
136
+ Div = function(div)
137
+ if not has_class(div, "manual-chapter-citations") then
138
+ return nil
139
+ end
140
+
141
+ local selected = {}
142
+ local keys = div.attributes["data-citations"] or ""
143
+ for key in keys:gmatch("[^,]+") do
144
+ if references[key] then
145
+ table.insert(selected, references[key])
146
+ end
147
+ end
148
+ if #selected == 0 then
149
+ return {}
150
+ end
151
+
152
+ sort_references(selected)
153
+ for index, entry in ipairs(selected) do
154
+ selected[index] = without_identifier(entry)
155
+ end
156
+ return {
157
+ pandoc.Header(2, heading, pandoc.Attr("", {"unnumbered", "unlisted"})),
158
+ pandoc.Div(selected, pandoc.Attr("", {"references", "manual-chapter-references"}))
159
+ }
160
+ end
161
+ })
162
+
163
+ return document
164
+ end
@@ -0,0 +1,102 @@
1
+ local listings_languages = {
2
+ bash = { listing = "bash", label = "Bash" },
3
+ python = { listing = "Python", label = "Python" },
4
+ r = { listing = "R", label = "R" },
5
+ shell = { listing = "bash", label = "Shell" },
6
+ sh = { listing = "bash", label = "Shell" },
7
+ sql = { listing = "SQL", label = "SQL" },
8
+ url = {
9
+ listing = "ManualURL",
10
+ labels = { ca = "URL", es = "URL", en = "URL" },
11
+ },
12
+ spreadsheet = {
13
+ listing = "ManualSpreadsheet",
14
+ labels = { ca = "Fórmula", es = "Fórmula", en = "Formula" },
15
+ },
16
+ filetree = {
17
+ listing = "ManualFileTree",
18
+ labels = { ca = "Fitxers", es = "Archivos", en = "Files" },
19
+ },
20
+ }
21
+
22
+ local function latex_escape(value)
23
+ local replacements = {
24
+ ["\\"] = "\\textbackslash{}",
25
+ ["{"] = "\\{",
26
+ ["}"] = "\\}",
27
+ ["$"] = "\\$",
28
+ ["&"] = "\\&",
29
+ ["#"] = "\\#",
30
+ ["_"] = "\\_",
31
+ ["%"] = "\\%",
32
+ ["~"] = "\\textasciitilde{}",
33
+ ["^"] = "\\textasciicircum{}",
34
+ }
35
+ local escaped = {}
36
+ for index = 1, #value do
37
+ local character = value:sub(index, index)
38
+ table.insert(escaped, replacements[character] or character)
39
+ end
40
+ return table.concat(escaped)
41
+ end
42
+
43
+ local function language_code(lang)
44
+ local value = lang and pandoc.utils.stringify(lang) or "en"
45
+ return value:lower():match("^[a-z]+") or "en"
46
+ end
47
+
48
+ local function display_label(definition, lang)
49
+ if definition.labels then
50
+ return definition.labels[language_code(lang)] or definition.labels.en
51
+ end
52
+ return definition.label
53
+ end
54
+
55
+ local function transform_code_block(block, lang)
56
+ if not FORMAT:match("latex") then
57
+ return nil
58
+ end
59
+
60
+ local language = nil
61
+ for _, class in ipairs(block.classes) do
62
+ if class ~= "numberLines" then
63
+ language = language or class
64
+ end
65
+ end
66
+
67
+ if not language then
68
+ language = "text"
69
+ end
70
+
71
+ language = language:lower()
72
+ local definition = listings_languages[language]
73
+ local rendered = nil
74
+ if not definition then
75
+ rendered = "\\begin{manualverbatim}\n" .. block.text .. "\n\\end{manualverbatim}"
76
+ else
77
+ local options = {}
78
+ table.insert(options, "language=" .. definition.listing)
79
+ local first_number = block.attributes.startFrom
80
+ if first_number and first_number:match("^%d+$") then
81
+ table.insert(options, "firstnumber=" .. first_number)
82
+ end
83
+ local optional = #options > 0 and "[" .. table.concat(options, ",") .. "]" or ""
84
+ rendered = "\\begin{manualcode}" .. optional .. "{" .. latex_escape(display_label(definition, lang)) .. "}\n" ..
85
+ block.text .. "\n\\end{manualcode}"
86
+ end
87
+
88
+ local caption = block.attributes["data-listing-caption"]
89
+ if caption and caption ~= "" then
90
+ rendered = "\\begin{manualcodelisting}{" .. latex_escape(caption) .. "}\n" .. rendered ..
91
+ "\n\\end{manualcodelisting}"
92
+ end
93
+ return pandoc.RawBlock("latex", rendered)
94
+ end
95
+
96
+ function Pandoc(document)
97
+ return document:walk({
98
+ CodeBlock = function(block)
99
+ return transform_code_block(block, document.meta.lang)
100
+ end,
101
+ })
102
+ end
@@ -0,0 +1,35 @@
1
+ local function append_all(destination, values)
2
+ for _, value in ipairs(values) do
3
+ table.insert(destination, value)
4
+ end
5
+ end
6
+
7
+ function Figure(figure)
8
+ if not FORMAT:match("latex") or #figure.caption.long == 0 then
9
+ return nil
10
+ end
11
+
12
+ local caption = {pandoc.RawInline("latex", "\\caption{")}
13
+ for index, block in ipairs(figure.caption.long) do
14
+ if index > 1 then
15
+ table.insert(caption, pandoc.Space())
16
+ end
17
+ if block.content then
18
+ append_all(caption, block.content)
19
+ else
20
+ table.insert(caption, pandoc.Str(pandoc.utils.stringify(block)))
21
+ end
22
+ end
23
+ table.insert(caption, pandoc.RawInline("latex", "}"))
24
+ if figure.identifier and figure.identifier ~= "" then
25
+ table.insert(caption, pandoc.RawInline("latex", "\\label{" .. figure.identifier .. "}"))
26
+ end
27
+
28
+ local blocks = {
29
+ pandoc.RawBlock("latex", "\\begin{figure}[H]\n\\centering"),
30
+ pandoc.Plain(caption),
31
+ }
32
+ append_all(blocks, figure.content)
33
+ table.insert(blocks, pandoc.RawBlock("latex", "\\end{figure}"))
34
+ return blocks
35
+ end
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if (($# != 0)); then
5
+ echo "publish_release.sh does not accept caller-controlled arguments." >&2
6
+ exit 2
7
+ fi
8
+
9
+ required_environment=(
10
+ CANDIDATE_MANIFEST_SHA256
11
+ CORE_SHA
12
+ GH_TOKEN
13
+ GITHUB_REPOSITORY
14
+ MANUAL_PDF_IMAGE
15
+ PYTHON_VERSION
16
+ REVIEWED_SHA
17
+ RUBY_VERSION
18
+ RUNNER_TEMP
19
+ SELECTOR
20
+ SITE_BUILD_IMAGE
21
+ WORKFLOW_FILE_PATH
22
+ WORKFLOW_REF
23
+ WORKFLOW_REPOSITORY
24
+ WORKFLOW_SHA
25
+ )
26
+ for variable in "${required_environment[@]}"; do
27
+ if [[ -z "${!variable:-}" ]]; then
28
+ echo "Required release environment is missing: $variable" >&2
29
+ exit 1
30
+ fi
31
+ done
32
+
33
+ if [[ "$WORKFLOW_REPOSITORY" != "dosquartsdedocs/unaltraweb" ]] || [[ "$WORKFLOW_FILE_PATH" != ".github/workflows/site-release.yml" ]]; then
34
+ echo "Stable publication must run from the canonical reusable workflow." >&2
35
+ exit 1
36
+ fi
37
+ if [[ ! "$WORKFLOW_SHA" =~ ^[0-9a-f]{40}$ ]] || [[ "$CORE_SHA" != "$WORKFLOW_SHA" ]]; then
38
+ echo "The release tooling revision does not match the defining workflow revision." >&2
39
+ exit 1
40
+ fi
41
+ if [[ "$WORKFLOW_REF" != "$WORKFLOW_REPOSITORY/$WORKFLOW_FILE_PATH@$CORE_SHA" ]]; then
42
+ echo "The reusable stable workflow was not invoked by its immutable commit SHA." >&2
43
+ exit 1
44
+ fi
45
+ if [[ ! "$GITHUB_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
46
+ echo "GITHUB_REPOSITORY is not a canonical owner/repository name." >&2
47
+ exit 1
48
+ fi
49
+ if [[ "$(git -C .unaltraweb-release-core rev-parse HEAD)" != "$WORKFLOW_SHA" ]] || \
50
+ [[ -n "$(git -C .unaltraweb-release-core status --porcelain=v1 --untracked-files=all)" ]]; then
51
+ echo "The privileged release tooling checkout is not the exact reviewed revision." >&2
52
+ exit 1
53
+ fi
54
+ actual_python="$(python3 -c 'import platform; print(platform.python_version())')"
55
+ if [[ "$actual_python" != "$PYTHON_VERSION" ]]; then
56
+ echo "The privileged verifier Python version does not match python_version." >&2
57
+ exit 1
58
+ fi
59
+
60
+ verify_assets=(
61
+ python3 .unaltraweb-release-core/scripts/manual/verify_release_assets.py
62
+ --selector "$SELECTOR"
63
+ --reviewed-sha "$REVIEWED_SHA"
64
+ --core-sha "$CORE_SHA"
65
+ --manual-pdf-image "$MANUAL_PDF_IMAGE"
66
+ --python-version "$PYTHON_VERSION"
67
+ --ruby-version "$RUBY_VERSION"
68
+ --site-build-image "$SITE_BUILD_IMAGE"
69
+ --vegavisuals-sha "${VEGAVISUALS_SHA:-}"
70
+ --repository "$GITHUB_REPOSITORY"
71
+ --candidate-manifest-sha256 "$CANDIDATE_MANIFEST_SHA256"
72
+ )
73
+ "${verify_assets[@]}" --assets release-assets
74
+
75
+ notes="$RUNNER_TEMP/release-notes.md"
76
+ printf 'Stable manual %s\n\nSource commit: `%s`\nCore commit: `%s`\n' \
77
+ "$SELECTOR" "$REVIEWED_SHA" "$CORE_SHA" > "$notes"
78
+
79
+ shopt -s nullglob
80
+ pdf_assets=(release-assets/*.pdf)
81
+ if ((${#pdf_assets[@]} == 0)); then
82
+ echo "The verified release has no standalone PDF assets." >&2
83
+ exit 1
84
+ fi
85
+ release_assets=(
86
+ release-assets/SHA256SUMS
87
+ release-assets/publication.json
88
+ release-assets/release-manifest.json
89
+ "release-assets/manual-site-$SELECTOR.tar.gz"
90
+ "${pdf_assets[@]}"
91
+ )
92
+ python3 - "$RUNNER_TEMP/expected-release-assets.json" "${release_assets[@]}" <<'PY'
93
+ import json
94
+ import os
95
+ import sys
96
+
97
+ names = []
98
+ for raw in sys.argv[2:]:
99
+ if not os.path.isfile(raw) or os.path.islink(raw):
100
+ raise SystemExit(f"Release asset is not a regular file: {raw}")
101
+ names.append(os.path.basename(raw))
102
+ if len(names) != len(set(names)):
103
+ raise SystemExit("Release asset names are not unique.")
104
+ with open(sys.argv[1], "w", encoding="utf-8", newline="\n") as handle:
105
+ json.dump(sorted(names), handle)
106
+ handle.write("\n")
107
+ PY
108
+
109
+ gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/releases?per_page=100" > "$RUNNER_TEMP/releases.json"
110
+ releases="$(jq -c --arg selector "$SELECTOR" '[.[][] | select(.tag_name == $selector)]' "$RUNNER_TEMP/releases.json")"
111
+ release_count="$(jq 'length' <<<"$releases")"
112
+ if [[ "$release_count" -gt 1 ]]; then
113
+ echo "More than one release uses tag $SELECTOR; refusing an ambiguous resume." >&2
114
+ exit 1
115
+ fi
116
+ release_id=""
117
+ if [[ "$release_count" == 1 ]]; then
118
+ release_draft="$(jq -r '.[0].draft' <<<"$releases")"
119
+ release_tag="$(jq -r '.[0].tag_name' <<<"$releases")"
120
+ release_id="$(jq -r '.[0].id' <<<"$releases")"
121
+ if [[ "$release_draft" != "true" ]] || [[ "$release_tag" != "$SELECTOR" ]] || [[ ! "$release_id" =~ ^[0-9]+$ ]]; then
122
+ echo "An existing release is published or does not match the reviewed tag." >&2
123
+ exit 1
124
+ fi
125
+ fi
126
+
127
+ gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/$SELECTOR?per_page=100" \
128
+ > "$RUNNER_TEMP/tags.json"
129
+ tags="$(jq -c --arg ref "refs/tags/$SELECTOR" '[.[][] | select(.ref == $ref)]' "$RUNNER_TEMP/tags.json")"
130
+ tag_count="$(jq 'length' <<<"$tags")"
131
+ if [[ "$tag_count" -gt 1 ]]; then
132
+ echo "More than one exact Git ref matches $SELECTOR." >&2
133
+ exit 1
134
+ fi
135
+ if [[ "$tag_count" == 1 ]]; then
136
+ tag_sha="$(jq -r '.[0].object.sha' <<<"$tags")"
137
+ tag_type="$(jq -r '.[0].object.type' <<<"$tags")"
138
+ if [[ "$tag_type" != "commit" ]] || [[ "$tag_sha" != "$REVIEWED_SHA" ]]; then
139
+ echo "Tag $SELECTOR is not a lightweight tag at the reviewed commit." >&2
140
+ exit 1
141
+ fi
142
+ else
143
+ gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
144
+ -f ref="refs/tags/$SELECTOR" -f sha="$REVIEWED_SHA" >/dev/null
145
+ fi
146
+
147
+ tag_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$SELECTOR")"
148
+ if [[ "$(jq -r .object.type <<<"$tag_json")" != "commit" ]] || \
149
+ [[ "$(jq -r .object.sha <<<"$tag_json")" != "$REVIEWED_SHA" ]]; then
150
+ echo "The release tag changed after it was bound to the reviewed commit." >&2
151
+ exit 1
152
+ fi
153
+
154
+ draft_payload() {
155
+ jq -n \
156
+ --arg tag "$SELECTOR" \
157
+ --arg title "$SELECTOR" \
158
+ --rawfile body "$notes" \
159
+ '{tag_name: $tag, name: $title, body: $body, draft: true, prerelease: false, make_latest: "false"}'
160
+ }
161
+
162
+ if [[ -z "$release_id" ]]; then
163
+ release_json="$(draft_payload | gh api --method POST "repos/$GITHUB_REPOSITORY/releases" --input -)"
164
+ release_id="$(jq -r .id <<<"$release_json")"
165
+ if [[ ! "$release_id" =~ ^[0-9]+$ ]]; then
166
+ echo "GitHub did not return a numeric release ID." >&2
167
+ exit 1
168
+ fi
169
+ fi
170
+
171
+ release_json="$(draft_payload | gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/$release_id" --input -)"
172
+ jq -e \
173
+ --arg tag "$SELECTOR" \
174
+ --arg title "$SELECTOR" \
175
+ --rawfile body "$notes" \
176
+ '.draft == true and .prerelease == false and .tag_name == $tag and .name == $title and .body == $body' \
177
+ <<<"$release_json" >/dev/null
178
+
179
+ gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/releases/$release_id/assets?per_page=100" \
180
+ > "$RUNNER_TEMP/existing-release-assets.json"
181
+ jq -e 'all(.[][]; (.id | type) == "number")' "$RUNNER_TEMP/existing-release-assets.json" >/dev/null
182
+ jq -r '.[][] | .id' "$RUNNER_TEMP/existing-release-assets.json" > "$RUNNER_TEMP/existing-release-asset-ids"
183
+ while IFS= read -r asset_id; do
184
+ [[ -z "$asset_id" ]] && continue
185
+ gh api --method DELETE "repos/$GITHUB_REPOSITORY/releases/assets/$asset_id"
186
+ done < "$RUNNER_TEMP/existing-release-asset-ids"
187
+
188
+ for asset in "${release_assets[@]}"; do
189
+ name="${asset##*/}"
190
+ encoded_name="$(jq -rn --arg value "$name" '$value | @uri')"
191
+ response="$(curl --fail-with-body --silent --show-error \
192
+ -H "Authorization: Bearer $GH_TOKEN" \
193
+ -H 'Accept: application/vnd.github+json' \
194
+ -H 'X-GitHub-Api-Version: 2022-11-28' \
195
+ -H 'Content-Type: application/octet-stream' \
196
+ --data-binary "@$asset" \
197
+ "https://uploads.github.com/repos/$GITHUB_REPOSITORY/releases/$release_id/assets?name=$encoded_name")"
198
+ asset_size="$(stat -c %s -- "$asset")"
199
+ jq -e --arg name "$name" --argjson size "$asset_size" \
200
+ '.name == $name and .state == "uploaded" and .size == $size and (.id | type) == "number"' \
201
+ <<<"$response" >/dev/null
202
+ done
203
+
204
+ downloaded_assets="$(mktemp -d "$RUNNER_TEMP/uploaded-release-assets.XXXXXX")"
205
+ cleanup() {
206
+ rm -rf -- "$downloaded_assets"
207
+ }
208
+ trap cleanup EXIT
209
+
210
+ release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/$release_id")"
211
+ jq -e \
212
+ --arg tag "$SELECTOR" \
213
+ --arg title "$SELECTOR" \
214
+ --rawfile body "$notes" \
215
+ '.draft == true and .prerelease == false and .tag_name == $tag and .name == $title and .body == $body' \
216
+ <<<"$release_json" >/dev/null
217
+ gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/releases/$release_id/assets?per_page=100" \
218
+ > "$RUNNER_TEMP/uploaded-release-assets-api.json"
219
+ jq -e --slurpfile expected "$RUNNER_TEMP/expected-release-assets.json" '
220
+ [.[][]] as $assets |
221
+ ($assets | length) == ($expected[0] | length) and
222
+ ([$assets[].name] | sort) == ($expected[0] | sort) and
223
+ all($assets[]; (.id | type) == "number" and (.name | type) == "string" and .state == "uploaded")
224
+ ' "$RUNNER_TEMP/uploaded-release-assets-api.json" >/dev/null
225
+ jq -S '[.[][] | {digest, id, name, size, state, updated_at}] | sort_by(.name)' \
226
+ "$RUNNER_TEMP/uploaded-release-assets-api.json" > "$RUNNER_TEMP/uploaded-release-assets-inventory.json"
227
+ jq -r '.[][] | [.id, .name] | @tsv' "$RUNNER_TEMP/uploaded-release-assets-api.json" \
228
+ > "$RUNNER_TEMP/uploaded-release-assets.tsv"
229
+ while IFS=$'\t' read -r asset_id asset_name; do
230
+ [[ "$asset_id" =~ ^[0-9]+$ ]]
231
+ gh api -H 'Accept: application/octet-stream' \
232
+ "repos/$GITHUB_REPOSITORY/releases/assets/$asset_id" > "$downloaded_assets/$asset_name"
233
+ done < "$RUNNER_TEMP/uploaded-release-assets.tsv"
234
+
235
+ "${verify_assets[@]}" --assets "$downloaded_assets" --reference-assets release-assets
236
+
237
+ tag_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$SELECTOR")"
238
+ if [[ "$(jq -r .object.type <<<"$tag_json")" != "commit" ]] || \
239
+ [[ "$(jq -r .object.sha <<<"$tag_json")" != "$REVIEWED_SHA" ]]; then
240
+ echo "The release tag changed before publication." >&2
241
+ exit 1
242
+ fi
243
+ release_json="$(gh api "repos/$GITHUB_REPOSITORY/releases/$release_id")"
244
+ jq -e \
245
+ --arg tag "$SELECTOR" \
246
+ --arg title "$SELECTOR" \
247
+ --rawfile body "$notes" \
248
+ '.draft == true and .prerelease == false and .tag_name == $tag and .name == $title and .body == $body' \
249
+ <<<"$release_json" >/dev/null
250
+ gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/releases/$release_id/assets?per_page=100" \
251
+ > "$RUNNER_TEMP/current-release-assets-api.json"
252
+ jq -S '[.[][] | {digest, id, name, size, state, updated_at}] | sort_by(.name)' \
253
+ "$RUNNER_TEMP/current-release-assets-api.json" > "$RUNNER_TEMP/current-release-assets-inventory.json"
254
+ cmp -- "$RUNNER_TEMP/uploaded-release-assets-inventory.json" "$RUNNER_TEMP/current-release-assets-inventory.json"
255
+
256
+ published_payload="$(jq -n \
257
+ --arg tag "$SELECTOR" \
258
+ --arg title "$SELECTOR" \
259
+ --rawfile body "$notes" \
260
+ '{tag_name: $tag, name: $title, body: $body, draft: false, prerelease: false, make_latest: "false"}')"
261
+ published="$(gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/$release_id" --input - <<<"$published_payload")"
262
+ jq -e \
263
+ --arg tag "$SELECTOR" \
264
+ --arg title "$SELECTOR" \
265
+ --rawfile body "$notes" \
266
+ '.draft == false and .prerelease == false and .tag_name == $tag and .name == $title and .body == $body' \
267
+ <<<"$published" >/dev/null
268
+
269
+ tag_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$SELECTOR")"
270
+ if [[ "$(jq -r .object.type <<<"$tag_json")" != "commit" ]] || \
271
+ [[ "$(jq -r .object.sha <<<"$tag_json")" != "$REVIEWED_SHA" ]]; then
272
+ echo "The release tag changed during publication." >&2
273
+ exit 1
274
+ fi
275
+ published="$(gh api "repos/$GITHUB_REPOSITORY/releases/$release_id")"
276
+ jq -e \
277
+ --arg tag "$SELECTOR" \
278
+ --arg title "$SELECTOR" \
279
+ --rawfile body "$notes" \
280
+ '.draft == false and .prerelease == false and .tag_name == $tag and .name == $title and .body == $body' \
281
+ <<<"$published" >/dev/null