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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/Makefile +412 -0
- data/README.md +226 -0
- data/_config.yml +842 -0
- data/_data/i18n/ca.yml +278 -0
- data/_data/i18n/en.yml +278 -0
- data/_data/i18n/es.yml +278 -0
- data/_includes/audio.liquid +30 -0
- data/_includes/bib_search.liquid +5 -0
- data/_includes/blog-list.liquid +66 -0
- data/_includes/book-citation.liquid +33 -0
- data/_includes/citation.liquid +26 -0
- data/_includes/custom_head.liquid +3 -0
- data/_includes/custom_scripts.liquid +5 -0
- data/_includes/cv/list.liquid +5 -0
- data/_includes/cv/list_groups.liquid +51 -0
- data/_includes/cv/map.liquid +29 -0
- data/_includes/cv/nested_list.liquid +16 -0
- data/_includes/cv/time_table.liquid +118 -0
- data/_includes/cv-download-card.liquid +26 -0
- data/_includes/developer-mode-switcher.liquid +24 -0
- data/_includes/disqus.liquid +13 -0
- data/_includes/distill_scripts.liquid +298 -0
- data/_includes/documentation-download-detail.liquid +76 -0
- data/_includes/documentation-downloads.liquid +71 -0
- data/_includes/documentation-profile-attributes.liquid +4 -0
- data/_includes/documentation-section-toc.liquid +9 -0
- data/_includes/documentation-sidebar.liquid +112 -0
- data/_includes/figure.liquid +87 -0
- data/_includes/footer.liquid +66 -0
- data/_includes/giscus.liquid +25 -0
- data/_includes/head.liquid +220 -0
- data/_includes/header.liquid +544 -0
- data/_includes/latest_posts.liquid +61 -0
- data/_includes/localized-date.liquid +5 -0
- data/_includes/manual-bibliography.liquid +2 -0
- data/_includes/manual-featured-readings.liquid +12 -0
- data/_includes/manual-other-readings.liquid +12 -0
- data/_includes/manual-page-toc.liquid +11 -0
- data/_includes/manual-sidebar.liquid +58 -0
- data/_includes/manual-teachers.liquid +74 -0
- data/_includes/metadata.liquid +278 -0
- data/_includes/news.liquid +48 -0
- data/_includes/newsletter.liquid +68 -0
- data/_includes/page-hero.liquid +96 -0
- data/_includes/page-link.liquid +8 -0
- data/_includes/pagination.liquid +22 -0
- data/_includes/post-reading-time.liquid +29 -0
- data/_includes/previous-next.liquid +37 -0
- data/_includes/profile-card.liquid +49 -0
- data/_includes/profile-feature-matrix.liquid +75 -0
- data/_includes/profile-highlights.liquid +122 -0
- data/_includes/project-grid.liquid +26 -0
- data/_includes/project-resource-link.liquid +45 -0
- data/_includes/project-resources.liquid +21 -0
- data/_includes/projects.liquid +38 -0
- data/_includes/projects_horizontal.liquid +37 -0
- data/_includes/publications-metrics-summary.liquid +69 -0
- data/_includes/publications-summary.liquid +127 -0
- data/_includes/reading-biblio-controls.liquid +65 -0
- data/_includes/reading-cover-card.liquid +93 -0
- data/_includes/reading-rating.liquid +11 -0
- data/_includes/recommended-readings.liquid +30 -0
- data/_includes/related_posts.liquid +39 -0
- data/_includes/repository/repo.liquid +48 -0
- data/_includes/repository/repo_trophies.liquid +44 -0
- data/_includes/repository/repo_user.liquid +35 -0
- data/_includes/resume/awards.liquid +19 -0
- data/_includes/resume/basics.liquid +28 -0
- data/_includes/resume/certificates.liquid +35 -0
- data/_includes/resume/education.liquid +54 -0
- data/_includes/resume/interests.liquid +34 -0
- data/_includes/resume/languages.liquid +31 -0
- data/_includes/resume/projects.liquid +32 -0
- data/_includes/resume/publications.liquid +28 -0
- data/_includes/resume/references.liquid +31 -0
- data/_includes/resume/skills.liquid +33 -0
- data/_includes/resume/volunteer.liquid +52 -0
- data/_includes/resume/work.liquid +52 -0
- data/_includes/scripts.liquid +364 -0
- data/_includes/search-data.js.liquid +341 -0
- data/_includes/selected_papers.liquid +3 -0
- data/_includes/t.liquid +31 -0
- data/_includes/team-grid.liquid +172 -0
- data/_includes/video.liquid +97 -0
- data/_layouts/about.liquid +93 -0
- data/_layouts/archive.liquid +45 -0
- data/_layouts/bib.liquid +677 -0
- data/_layouts/book-review.liquid +225 -0
- data/_layouts/book-shelf.liquid +66 -0
- data/_layouts/cv.liquid +127 -0
- data/_layouts/default.liquid +70 -0
- data/_layouts/distill.liquid +132 -0
- data/_layouts/documentation-download-detail.liquid +5 -0
- data/_layouts/documentation-home.liquid +100 -0
- data/_layouts/documentation-page.liquid +45 -0
- data/_layouts/manual-bib.liquid +73 -0
- data/_layouts/manual-chapter.liquid +99 -0
- data/_layouts/manual-featured-bib.liquid +100 -0
- data/_layouts/manual-home.liquid +120 -0
- data/_layouts/none.liquid +1 -0
- data/_layouts/outputs.liquid +29 -0
- data/_layouts/page.liquid +52 -0
- data/_layouts/post.liquid +95 -0
- data/_layouts/profile.liquid +36 -0
- data/_layouts/profiles.liquid +36 -0
- data/_layouts/project.liquid +7 -0
- data/_layouts/repositories.liquid +58 -0
- data/_layouts/theses.liquid +126 -0
- data/_plugins/bibliography_profiles.rb +164 -0
- data/_plugins/callouts.rb +104 -0
- data/_plugins/code_blocks.rb +277 -0
- data/_plugins/computation_figure_images.rb +266 -0
- data/_plugins/computation_sources.rb +42 -0
- data/_plugins/content_search_index.rb +210 -0
- data/_plugins/details.rb +23 -0
- data/_plugins/external-posts.rb +124 -0
- data/_plugins/figure_captions.rb +800 -0
- data/_plugins/file-exists.rb +22 -0
- data/_plugins/google-scholar-citations.rb +86 -0
- data/_plugins/hide-custom-bibtex.rb +18 -0
- data/_plugins/inspirehep-citations.rb +57 -0
- data/_plugins/localized_visual_sources.rb +145 -0
- data/_plugins/manual_release_metadata.rb +47 -0
- data/_plugins/mermaid_mmd_images.rb +233 -0
- data/_plugins/profile-pages.rb +28 -0
- data/_plugins/remove-accents.rb +32 -0
- data/_plugins/reproducible_build_time.rb +55 -0
- data/_plugins/search-data.rb +83 -0
- data/_plugins/theme-cache-bust.rb +56 -0
- data/_plugins/vega_visual_images.rb +290 -0
- data/_plugins/web_capture_images.rb +154 -0
- data/_sass/_base.scss +2133 -0
- data/_sass/_blog.scss +67 -0
- data/_sass/_callouts.scss +100 -0
- data/_sass/_custom.scss +437 -0
- data/_sass/_cv.scss +274 -0
- data/_sass/_developer.scss +97 -0
- data/_sass/_distill.scss +185 -0
- data/_sass/_documentation.scss +2220 -0
- data/_sass/_layout.scss +218 -0
- data/_sass/_manual.scss +2691 -0
- data/_sass/_profile.scss +323 -0
- data/_sass/_projects.scss +163 -0
- data/_sass/_reading.scss +465 -0
- data/_sass/_site-custom.scss +3 -0
- data/_sass/_tabs.scss +48 -0
- data/_sass/_team.scss +230 -0
- data/_sass/_themes.scss +254 -0
- data/_sass/_typograms.scss +132 -0
- data/_sass/_variables.scss +52 -0
- data/_sass/font-awesome/_animated.scss +152 -0
- data/_sass/font-awesome/_bordered-pulled.scss +20 -0
- data/_sass/font-awesome/_core.scss +49 -0
- data/_sass/font-awesome/_fixed-width.scss +7 -0
- data/_sass/font-awesome/_functions.scss +57 -0
- data/_sass/font-awesome/_icons.scss +13 -0
- data/_sass/font-awesome/_list.scss +18 -0
- data/_sass/font-awesome/_mixins.scss +65 -0
- data/_sass/font-awesome/_rotated-flipped.scss +31 -0
- data/_sass/font-awesome/_screen-reader.scss +14 -0
- data/_sass/font-awesome/_shims.scss +1578 -0
- data/_sass/font-awesome/_sizing.scss +16 -0
- data/_sass/font-awesome/_stacked.scss +32 -0
- data/_sass/font-awesome/_variables.scss +5044 -0
- data/_sass/font-awesome/brands.scss +30 -0
- data/_sass/font-awesome/fontawesome.scss +21 -0
- data/_sass/font-awesome/regular.scss +27 -0
- data/_sass/font-awesome/solid.scss +27 -0
- data/_sass/font-awesome/v4-shims.scss +11 -0
- data/_sass/tabler-icons/tabler-icons-filled.scss +1646 -0
- data/_sass/tabler-icons/tabler-icons-outline.scss +9859 -0
- data/_sass/tabler-icons/tabler-icons.scss +11463 -0
- data/_scripts/cronitor-analytics-setup.js +9 -0
- data/_scripts/giscus-setup.js +48 -0
- data/_scripts/google-analytics-setup.js +9 -0
- data/_scripts/open-panel-analytics-setup.js +14 -0
- data/_scripts/photoswipe-setup.js +11 -0
- data/assets/audio/epicaly-short-113909.mp3 +0 -0
- data/assets/bibliography/2018-12-22-distill.bib +7 -0
- data/assets/css/academicons.min.css +1 -0
- data/assets/css/bootstrap-toc.min.css +4 -0
- data/assets/css/bootstrap.min.css +7 -0
- data/assets/css/bootstrap.min.css.map +1 -0
- data/assets/css/jekyll-pygments-themes-github.css +209 -0
- data/assets/css/jekyll-pygments-themes-native.css +232 -0
- data/assets/css/jupyter-grade3.css +3758 -0
- data/assets/css/jupyter-monokai.css +3758 -0
- data/assets/css/jupyter.css +383 -0
- data/assets/css/main.scss +36 -0
- data/assets/css/mdb.min.css +48 -0
- data/assets/css/mdb.min.css.map +1 -0
- data/assets/css/scholar-icons.css +386 -0
- data/assets/fonts/academicons.ttf +0 -0
- data/assets/fonts/academicons.woff +0 -0
- data/assets/fonts/scholar-icons.ttf +0 -0
- data/assets/fonts/scholar-icons.woff +0 -0
- data/assets/fonts/tabler-icons-filled.ttf +0 -0
- data/assets/fonts/tabler-icons-filled.woff +0 -0
- data/assets/fonts/tabler-icons-filled.woff2 +0 -0
- data/assets/fonts/tabler-icons-outline.ttf +0 -0
- data/assets/fonts/tabler-icons-outline.woff +0 -0
- data/assets/fonts/tabler-icons-outline.woff2 +0 -0
- data/assets/fonts/tabler-icons.ttf +0 -0
- data/assets/fonts/tabler-icons.woff +0 -0
- data/assets/fonts/tabler-icons.woff2 +0 -0
- data/assets/html/relativity.html +15 -0
- data/assets/img/1.jpg +0 -0
- data/assets/img/10.jpg +0 -0
- data/assets/img/11.jpg +0 -0
- data/assets/img/12.jpg +0 -0
- data/assets/img/2.jpg +0 -0
- data/assets/img/3.jpg +0 -0
- data/assets/img/4.jpg +0 -0
- data/assets/img/5.jpg +0 -0
- data/assets/img/6.jpg +0 -0
- data/assets/img/7.jpg +0 -0
- data/assets/img/8.jpg +0 -0
- data/assets/img/9.jpg +0 -0
- data/assets/img/book_covers/the_godfather.jpg +0 -0
- data/assets/img/brand/dosquartsdedocs-logo-cafe.svg +31 -0
- data/assets/img/brand/dosquartsdedocs-logo-footer-cafe.svg +31 -0
- data/assets/img/brand/dosquartsdedocs-logo-white.svg +31 -0
- data/assets/img/brand/dosquartsdedocs-logo.svg +31 -0
- data/assets/img/prof_pic.jpg +0 -0
- data/assets/img/prof_pic_color.png +0 -0
- data/assets/img/profile-placeholder.svg +8 -0
- data/assets/img/publication_preview/brownian-motion.gif +0 -0
- data/assets/img/publication_preview/goodchild1992-gis-cover.jpg +0 -0
- data/assets/img/publication_preview/gutierrez2020-jtrg-preview.jpg +0 -0
- data/assets/img/publication_preview/harvey1989-postmodernity-cover.jpg +0 -0
- data/assets/img/publication_preview/tobler1970-economic-geography-cover.jpg +0 -0
- data/assets/img/publication_preview/tuan1977-space-place-cover.jpg +0 -0
- data/assets/img/publication_preview/wave-mechanics.gif +0 -0
- data/assets/img/publication_preview/zaragozi2025-rooftop-pv-preview.jpg +0 -0
- data/assets/img/rhino.png +0 -0
- data/assets/img/team/aaron-gutierrez.png +0 -0
- data/assets/img/team/alba-font.png +0 -0
- data/assets/img/team/alba-viana.png +0 -0
- data/assets/img/team/alejandro-gonzalez.png +0 -0
- data/assets/img/team/ana-pastor.png +0 -0
- data/assets/img/team/antoni-domenech.png +0 -0
- data/assets/img/team/benito-zaragozi.png +0 -0
- data/assets/img/team/cinta-sanz.png +0 -0
- data/assets/img/team/daniel-miravet.png +0 -0
- data/assets/img/team/eugenia-altamirano.png +0 -0
- data/assets/img/team/fiammetta-brandajs.png +0 -0
- data/assets/img/team/francesc-gonzalez.png +0 -0
- data/assets/img/team/isabel-paulino.png +0 -0
- data/assets/img/team/joan-borras.png +0 -0
- data/assets/img/team/julie-wilson.png +0 -0
- data/assets/img/team/liliana-sole.png +0 -0
- data/assets/img/team/maria-casado.png +0 -0
- data/assets/img/team/maria-medina.png +0 -0
- data/assets/img/team/marta-nello.png +0 -0
- data/assets/img/team/mogrify +1 -0
- data/assets/img/team/oscar-saladie.png +0 -0
- data/assets/img/team/paolo-russo.png +0 -0
- data/assets/img/team/raquel-santos.png +0 -0
- data/assets/img/team/salvador-anton.png +0 -0
- data/assets/img/team/sunsi-huertas.png +0 -0
- data/assets/img/team/user-placeholder.png +0 -0
- data/assets/img/team/wilbert-den-hoed.png +0 -0
- data/assets/img/team/xavier-delclos.png +0 -0
- data/assets/img/template_error.png +0 -0
- data/assets/js/bibsearch.js +70 -0
- data/assets/js/bootstrap-toc.min.js +5 -0
- data/assets/js/bootstrap.bundle.min.js +7 -0
- data/assets/js/bootstrap.bundle.min.js.map +1 -0
- data/assets/js/callouts.js +115 -0
- data/assets/js/chartjs-setup.js +14 -0
- data/assets/js/common.js +117 -0
- data/assets/js/content-search-match.js +159 -0
- data/assets/js/content-search.js +441 -0
- data/assets/js/copy_code.js +60 -0
- data/assets/js/custom.js +6 -0
- data/assets/js/developer-mode.js +103 -0
- data/assets/js/diff2html-setup.js +20 -0
- data/assets/js/distillpub/overrides.js +27 -0
- data/assets/js/distillpub/template.v2.js +9616 -0
- data/assets/js/distillpub/template.v2.js.map +1 -0
- data/assets/js/distillpub/transforms.v2.js +14557 -0
- data/assets/js/distillpub/transforms.v2.js.map +1 -0
- data/assets/js/documentation.js +435 -0
- data/assets/js/echarts-setup.js +29 -0
- data/assets/js/highlight-search-term.js +110 -0
- data/assets/js/jupyter_new_tab.js +18 -0
- data/assets/js/leaflet-setup.js +22 -0
- data/assets/js/manual.js +308 -0
- data/assets/js/masonry.js +12 -0
- data/assets/js/mathjax-setup.js +46 -0
- data/assets/js/mermaid-setup.js +37 -0
- data/assets/js/newsletter.js +105 -0
- data/assets/js/no_defer.js +24 -0
- data/assets/js/plotly-setup.js +52 -0
- data/assets/js/profile-nav.js +92 -0
- data/assets/js/progress-bar.js +73 -0
- data/assets/js/pseudocode-setup.js +18 -0
- data/assets/js/search/base-styles.min.js +286 -0
- data/assets/js/search/command-score.min.js +7 -0
- data/assets/js/search/hotkeys-js/hotkeys.esm.min.js +18 -0
- data/assets/js/search/lit/decorators.min.js +7 -0
- data/assets/js/search/lit/directives/class-map.min.js +7 -0
- data/assets/js/search/lit/directives/join.min.js +7 -0
- data/assets/js/search/lit/directives/live.min.js +7 -0
- data/assets/js/search/lit/directives/ref.min.js +7 -0
- data/assets/js/search/lit/directives/repeat.min.js +7 -0
- data/assets/js/search/lit/directives/unsafe-html.min.js +7 -0
- data/assets/js/search/lit/index.min.js +7 -0
- data/assets/js/search/lit-element/lit-element.min.js +12 -0
- data/assets/js/search/lit-html/directives/class-map.min.js +22 -0
- data/assets/js/search/lit-html/directives/join.min.js +12 -0
- data/assets/js/search/lit-html/directives/live.min.js +28 -0
- data/assets/js/search/lit-html/directives/ref.min.js +32 -0
- data/assets/js/search/lit-html/directives/repeat.min.js +27 -0
- data/assets/js/search/lit-html/directives/unsafe-html.min.js +22 -0
- data/assets/js/search/lit-html/is-server.min.js +12 -0
- data/assets/js/search/lit-html/lit-html.min.js +12 -0
- data/assets/js/search/ninja-action.min.js +101 -0
- data/assets/js/search/ninja-footer.min.js +62 -0
- data/assets/js/search/ninja-header.min.js +78 -0
- data/assets/js/search/ninja-keys.min.js +39 -0
- data/assets/js/search/reactive-element/decorators/custom-element.min.js +12 -0
- data/assets/js/search/reactive-element/decorators/event-options.min.js +12 -0
- data/assets/js/search/reactive-element/decorators/property.min.js +22 -0
- data/assets/js/search/reactive-element/decorators/query-all.min.js +17 -0
- data/assets/js/search/reactive-element/decorators/query-assigned-elements.min.js +17 -0
- data/assets/js/search/reactive-element/decorators/query-assigned-nodes.min.js +17 -0
- data/assets/js/search/reactive-element/decorators/query-async.min.js +17 -0
- data/assets/js/search/reactive-element/decorators/query.min.js +17 -0
- data/assets/js/search/reactive-element/decorators/state.min.js +27 -0
- data/assets/js/search/reactive-element/reactive-element.min.js +17 -0
- data/assets/js/search-setup.js +17 -0
- data/assets/js/shortcut-key.js +11 -0
- data/assets/js/tabs.min.js +5 -0
- data/assets/js/theme.js +314 -0
- data/assets/js/tooltips-setup.js +3 -0
- data/assets/js/typograms.js +1341 -0
- data/assets/js/vanilla-back-to-top.min.js +1 -0
- data/assets/js/vega-setup.js +24 -0
- data/assets/js/venobox-setup.js +5 -0
- data/assets/js/wechat.js +14 -0
- data/assets/js/zoom.js +15 -0
- data/assets/json/resume.json +98 -0
- data/assets/json/table_data.json +128 -0
- data/assets/jupyter/blog.ipynb +48 -0
- data/assets/pdf/example_pdf.pdf +0 -0
- data/assets/plotly/demo.html +72 -0
- data/assets/video/pexels-engin-akyurt-6069112-960x540-30fps.mp4 +0 -0
- data/assets/webfonts/fa-brands-400.ttf +0 -0
- data/assets/webfonts/fa-brands-400.woff2 +0 -0
- data/assets/webfonts/fa-regular-400.ttf +0 -0
- data/assets/webfonts/fa-regular-400.woff2 +0 -0
- data/assets/webfonts/fa-solid-900.ttf +0 -0
- data/assets/webfonts/fa-solid-900.woff2 +0 -0
- data/assets/webfonts/fa-v4compatibility.ttf +0 -0
- data/assets/webfonts/fa-v4compatibility.woff2 +0 -0
- data/docs/Gemfile +5 -0
- data/docs/_config.yml +83 -0
- data/docs/_documentation/en/01-quick-start.md +115 -0
- data/docs/_documentation/en/02-tools.md +146 -0
- data/docs/_documentation/en/03-usage.md +107 -0
- data/docs/_documentation/en/04-jekyll-basics.md +51 -0
- data/docs/_documentation/en/05-pages-collections.md +68 -0
- data/docs/_documentation/en/06-github-web-editing.md +92 -0
- data/docs/_documentation/en/10-profiles.md +125 -0
- data/docs/_documentation/en/11-unaltreselfie.md +39 -0
- data/docs/_documentation/en/12-unaltreprojecte.md +38 -0
- data/docs/_documentation/en/13-unaltremanual.md +237 -0
- data/docs/_documentation/en/14-unaltredocs.md +92 -0
- data/docs/_documentation/en/15-profile-checklist.md +39 -0
- data/docs/_documentation/en/20-syntax.md +353 -0
- data/docs/_documentation/en/21-themes.md +103 -0
- data/docs/_documentation/en/22-feature-reference.md +56 -0
- data/docs/_documentation/en/23-front-matter.md +51 -0
- data/docs/_documentation/en/24-data-files.md +28 -0
- data/docs/_documentation/en/30-customization.md +448 -0
- data/docs/_documentation/en/31-template.md +55 -0
- data/docs/_documentation/en/32-development.md +108 -0
- data/docs/_documentation/en/33-reusable-workflows.md +136 -0
- data/docs/_documentation/en/40-distribution.md +164 -0
- data/docs/_documentation/en/41-bibliometrics.md +137 -0
- data/docs/_documentation/en/42-docker-image.md +185 -0
- data/docs/_documentation/en/50-faq.md +33 -0
- data/docs/_pages/ca/index.md +35 -0
- data/docs/_pages/en/index.md +35 -0
- data/docs/_pages/es/index.md +35 -0
- data/docs/agents/action-prompts/00-start-site-session.txt +10 -0
- data/docs/agents/action-prompts/05-create-new-web.txt +5 -0
- data/docs/agents/action-prompts/10-content-update.txt +9 -0
- data/docs/agents/action-prompts/15-edit-default-content.txt +8 -0
- data/docs/agents/action-prompts/20-manual-teaching-materials.txt +18 -0
- data/docs/agents/action-prompts/22-manual-style-audit.txt +14 -0
- data/docs/agents/action-prompts/23-manual-structure-audit.txt +17 -0
- data/docs/agents/action-prompts/25-translation-prepublish.txt +8 -0
- data/docs/agents/action-prompts/30-project-site-update.txt +7 -0
- data/docs/agents/action-prompts/40-documentation-update.txt +7 -0
- data/docs/agents/action-prompts/50-bibliography-entry.txt +7 -0
- data/docs/agents/action-prompts/60-bibliometrics-refresh.txt +7 -0
- data/docs/agents/action-prompts/70-build-and-review.txt +12 -0
- data/docs/agents/manual-authoring-components.md +255 -0
- data/docs/agents/mcp-contract.md +162 -0
- data/docs/agents/quarto-computation-adoption.md +102 -0
- data/docs/assets/img/unaltraweb-docs-hero.svg +36 -0
- data/lib/unaltraweb/version.rb +8 -0
- data/lib/unaltraweb.rb +31 -0
- data/requirements.txt +5 -0
- data/scripts/biblio/fetch_scimago_csv.sh +126 -0
- data/scripts/biblio/metrics_common.py +154 -0
- data/scripts/biblio/metrics_merge_meta.py +73 -0
- data/scripts/biblio/metrics_update.py +826 -0
- data/scripts/computations/python/Dockerfile +37 -0
- data/scripts/computations/r/Dockerfile +31 -0
- data/scripts/computations/render.py +1267 -0
- data/scripts/cv/render_pdf_preview.sh +36 -0
- data/scripts/deploy/publish_branch.sh +80 -0
- data/scripts/generate_logo_variants.py +47 -0
- data/scripts/manual/Dockerfile +31 -0
- data/scripts/manual/build_pdf.py +1780 -0
- data/scripts/manual/filters/bibliography.lua +164 -0
- data/scripts/manual/filters/code-blocks.lua +102 -0
- data/scripts/manual/filters/figure-captions.lua +35 -0
- data/scripts/manual/publish_release.sh +281 -0
- data/scripts/manual/templates/manual.tex +494 -0
- data/scripts/manual/verify_release_assets.py +650 -0
- data/scripts/test_gem_build.py +128 -0
- data/scripts/test_reproducible_jekyll_build.py +191 -0
- data/scripts/test_wheel_install.py +282 -0
- data/scripts/unaltraweb-docker-mount.sh +36 -0
- data/scripts/unaltraweb-mcp-bootstrap.sh +105 -0
- data/scripts/unaltraweb-mcp-cleanup.sh +73 -0
- data/scripts/unaltraweb-mcp-project-id.sh +34 -0
- data/scripts/validate_distribution.py +563 -0
- data/scripts/validate_workflows.py +1285 -0
- data/scripts/web_captures/Dockerfile +31 -0
- data/scripts/web_captures/capture.mjs +157 -0
- data/scripts/web_captures/package-lock.json +56 -0
- data/scripts/web_captures/package.json +9 -0
- data/scripts/web_captures/render.py +867 -0
- data/src/unaltraweb_mcp/component-contract.json +153 -0
- data/src/unaltraweb_mcp/component-contract.schema.json +87 -0
- data/src/unaltraweb_mcp/docker_mount.py +19 -0
- 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
|