chore(release): 0.7.6 — promote rc2, fold Unreleased, bump codegen (#… #33
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Tag-triggered release workflow. Pushing a tag matching the project's | |
| # bare-numeric scheme (e.g. `0.6.2`, no `v` prefix) runs `tools/make_release.sh` | |
| # on ubuntu-22.04 and uploads the resulting tarball + `.deb` + per-asset | |
| # `.sha256` files to a fresh GitHub Release. | |
| # | |
| # Why ubuntu-22.04 and not ubuntu-latest: | |
| # ubuntu-22.04 ships glibc 2.35. Binaries linked against an older glibc | |
| # load on every later glibc — Ubuntu 22.04 LTS, 24.04 LTS, 26.04, and | |
| # Debian 12 / 13. Building on ubuntu-latest (currently 24.04) would | |
| # exclude Ubuntu 22.04 LTS users, who are still on the most widespread | |
| # long-term release. | |
| # | |
| # Manual release procedure: see docs/release/RELEASING.md. | |
| name: Release | |
| on: | |
| push: | |
| tags: | |
| # Bare X.Y.Z — matches the repo's existing tag history (no v-prefix). | |
| # Auto-publishes a public release (see the publish step). | |
| - '[0-9]+.[0-9]+.[0-9]+' | |
| # Pre-release / RC tags, e.g. 0.7.4-rc1 (a valid semver pre-release, so it | |
| # matches the Cargo.toml version — make_release.sh's version check passes | |
| # and the artifacts are named `…-0.7.4-rc1-…`). Published as a DRAFT | |
| # prerelease for cross-OS testing, not a public release (publish step). | |
| - '[0-9]+.[0-9]+.[0-9]+-*' | |
| permissions: | |
| # softprops/action-gh-release needs to write the Release object. | |
| contents: write | |
| jobs: | |
| # Generate {plain,latex}.YYYY.dump.txt for the 5-year TL window in | |
| # pinned TL containers (see release-dumps.yml). The dumps are NOT in | |
| # git — the release binary embeds what this job produces. | |
| dumps: | |
| uses: ./.github/workflows/release-dumps.yml | |
| # Assemble THIRD-PARTY-NOTICES ONCE, here, and hand it to every packaging leg | |
| # as an artifact (F7 in docs/release/LICENSE_INVENTORY.md). Previously only the | |
| # `release` job ran gen_notices.sh, so the macOS/arm64-linux tarballs fell back | |
| # to the committed hand-authored sections 1-4 and shipped with NO Rust-crate | |
| # license texts at all. | |
| # | |
| # One file is valid for every target: about.toml sets no `targets`, and krates | |
| # treats an empty target filter as include_all_targets — so the generated | |
| # section 5 is the union over all platforms, not the host's subset. (Verified | |
| # in krates' builder.rs: `include_all_targets = target_filters.is_empty()`.) | |
| # Do NOT add a `targets` key to about.toml without re-checking this job. | |
| notices: | |
| name: Assemble THIRD-PARTY-NOTICES | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| # cargo-about only reads cargo metadata + crate files; it never compiles | |
| # the workspace, so this leg needs no libxml2/TeX/system deps. | |
| - name: install cargo-about | |
| run: cargo install cargo-about --features cli | |
| # Re-run the blind-spot audit here, not just in CI's `lint` job: CI.yml | |
| # triggers on `push: [main]` + `pull_request`, so a TAG push never runs it. | |
| # Without this, a release could ship a newly-vendored native library that | |
| # no gate ever looked at. Cheap (seconds) and every release passes through | |
| # this job. | |
| - name: audit vendored native code + embedded resources | |
| run: python3 tools/audit_vendored_natives.py --verbose | |
| - name: assemble notices | |
| run: bash tools/gen_notices.sh THIRD-PARTY-NOTICES.dist | |
| # Fail loudly rather than ship a truncated notice. Guards the three | |
| # hand-authored things that have silently gone missing before: the vendored | |
| # native libs (section 3.3/3.4), the generated Rust appendix (section 5), | |
| # and the copyleft texts the static LGPL links require (section 6). | |
| - name: verify assembled notices are complete | |
| run: | | |
| set -euo pipefail | |
| f=THIRD-PARTY-NOTICES.dist | |
| for needle in \ | |
| "2.2 SVG 1.1 RELAX NG schema" \ | |
| "World Wide Web Consortium" \ | |
| "3.2 libkpathsea" \ | |
| "3.3 libmarpa" \ | |
| "3.4 mimalloc" \ | |
| "5. RUST DEPENDENCY LICENSES" \ | |
| "6. COPYLEFT LICENSE TEXTS" \ | |
| "7. SOURCE PROVENANCE" \ | |
| "This License Agreement applies to any software library or other" \ | |
| "0. Additional Definitions." \ | |
| "16. Limitation of Liability."; do | |
| grep -qF "$needle" "$f" || { echo "::error::THIRD-PARTY-NOTICES is missing: $needle"; exit 1; } | |
| done | |
| # The last three needles are sentences from the BODIES of LGPL-2.1, LGPL-3.0 | |
| # and GPL-3.0 respectively, each unique to its own file (verified). They used | |
| # to be the section titles "GNU LESSER GENERAL PUBLIC LICENSE v2.1" etc -- | |
| # strings that appear in NO license text and match only the title gen_notices.sh | |
| # echoes above each one. Three 0-byte files in licenses/ therefore passed this | |
| # gate while shipping no license text at all. Grep the text, not the scaffolding. | |
| # Section 7's relink pointer must name real commits: an unresolved revision | |
| # means we shipped a promise (3.5) we cannot keep. `commit: unknown` is the | |
| # same failure wearing a different word -- it is what gen_notices.sh writes | |
| # when `git rev-parse` cannot answer. Scoped to the section-7 lines so a | |
| # future crate's license text containing "unresolved" cannot fail a release. | |
| if sed -n '/^7\. SOURCE PROVENANCE/,$p' "$f" | grep -qE "unresolved|commit: unknown"; then | |
| echo "::error::THIRD-PARTY-NOTICES section 7 has an unresolved source revision" | |
| exit 1 | |
| fi | |
| # Section 5 must actually ATTRIBUTE CRATES, not merely carry its heading. | |
| # This replaces a whole-file `wc -l < 1000` floor that could never fire: the | |
| # section 6 license texts alone are 1341 lines, so a notices file attributing | |
| # ZERO of the ~66 shipped crates still measured ~1706 lines and passed. The | |
| # heading needle above cannot cover it either -- about.hbs emits | |
| # "5. RUST DEPENDENCY LICENSES" as literal template text, outside {{#each}}, | |
| # so it proves the template ran, not that it rendered anything. Count the | |
| # per-crate "Applies to:" lines inside section 5's own range instead. | |
| # Floor 50 vs ~66 today: a legitimate drop below it should stop a release and | |
| # make a human look, which is the point. | |
| crates="$(sed -n '/^5\. RUST DEPENDENCY LICENSES/,/^6\. COPYLEFT LICENSE TEXTS/p' "$f" \ | |
| | grep -c '^Applies to:' || true)" | |
| if [ "${crates:-0}" -lt 50 ]; then | |
| echo "::error::THIRD-PARTY-NOTICES section 5 attributes only ${crates:-0} crates (expected ~66) — cargo-about emitted little or nothing" | |
| exit 1 | |
| fi | |
| echo "OK: notices complete ($(wc -l < "$f") lines, section 5 attributes ${crates} crates)" | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: third-party-notices | |
| path: THIRD-PARTY-NOTICES.dist | |
| if-no-files-found: error | |
| # macOS (Apple Silicon) release tarball. Native binaries are never | |
| # cross-OS — Linux emits ELF, macOS emits Mach-O — so the macOS asset is | |
| # built on its own runner, NOT cross-compiled from the Linux leg. We build | |
| # natively on the arm64 `macos-15` runner (the same arch the CI.yml macOS | |
| # job validates, #217). Intel macOS (x86_64-apple-darwin) is published from a | |
| # separate native `build-macos-intel` leg below — see docs/release/RELEASING.md | |
| # "Release asset strategy". The TL-window dumps are OS-agnostic gzipped text, so this leg | |
| # embeds the exact bytes the `dumps` job produced for the Linux leg. | |
| build-macos: | |
| name: Build macOS arm64 ${{ github.ref_name }} | |
| runs-on: macos-15 | |
| # maxperf is fat-LTO / codegen-units=1 — the slowest, most RAM-hungry | |
| # build. macos-15 free-tier runners are 3 vCPU / 7 GB; allow generous | |
| # wall-clock. If the LTO link OOMs on 7 GB, the documented mitigation is a | |
| # larger runner or relaxing the macOS leg's codegen-units (RELEASING.md). | |
| timeout-minutes: 90 | |
| needs: [dumps, notices] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| # libxml2 + libxslt are keg-only on Homebrew; install them and surface | |
| # their pkgconfig dirs so the libxml/libxslt build scripts find them. | |
| # libkpathsea is built from source and STATICALLY linked (see the | |
| # build-static-kpathsea step below): Homebrew ships no static `.a`, and a | |
| # static link makes the binary self-contained — in-process lookups with NO | |
| # runtime libkpathsea dependency, so it launches on MacTeX (which ships no | |
| # libkpathsea) and degrades to empty lookups where no TeX is present. | |
| - name: install dependencies | |
| run: | | |
| command -v pkg-config >/dev/null || brew install pkgconf | |
| brew install libxml2 libxslt | |
| - name: surface keg-only pkg-config paths | |
| run: | | |
| echo "PKG_CONFIG_PATH=$(brew --prefix libxml2)/lib/pkgconfig:$(brew --prefix libxslt)/lib/pkgconfig" >> "$GITHUB_ENV" | |
| - uses: dtolnay/rust-toolchain@nightly | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| key: release-maxperf-macos | |
| # Same TL-window dumps as the Linux leg (OS-agnostic gzipped text). | |
| - name: collect TL-window dumps | |
| uses: actions/download-artifact@v6 | |
| with: | |
| pattern: dumps-* | |
| path: resources/dumps/ | |
| merge-multiple: true | |
| - name: verify dump window completeness | |
| run: | | |
| ls -la resources/dumps/ | |
| for year in 2022 2023 2024 2025 2026; do | |
| for f in plain.$year.dump.txt latex.$year.dump.txt texlive.$year.version; do | |
| if [ ! -s "resources/dumps/$f" ]; then | |
| echo "::error::release dump window incomplete: missing resources/dumps/$f" | |
| exit 1 | |
| fi | |
| done | |
| done | |
| # Static, self-contained libkpathsea (in-process lookups, no runtime dep). | |
| # Sets KPATHSEA_LIB_DIR + KPATHSEA_STATIC for the build below. | |
| - name: build static libkpathsea from source | |
| run: bash tools/build_static_kpathsea.sh | |
| # Static, self-contained libxml2 + libxslt + libexslt (no runtime | |
| # libxml2/libxslt .dylib dependency, so the binary is independent of the | |
| # host's libxml2 SONAME — libxml2 2.14 bumped .so.2 -> .so.16). Sets | |
| # PKG_CONFIG_PATH + LIBXML2_STATIC + LIBXSLT_STATIC for the build below; | |
| # supersedes the brew libxml2/libxslt above (kept only as a fallback). | |
| - name: build static libxml2 + libxslt from source | |
| run: bash tools/build_static_libxml.sh | |
| # make_release.sh bundles THIRD-PARTY-NOTICES.dist when present, else it | |
| # silently falls back to the committed sections 1-4 (no Rust appendix). | |
| - name: fetch assembled THIRD-PARTY-NOTICES | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: third-party-notices | |
| - name: build macOS release tarball | |
| run: bash tools/make_release.sh | |
| env: | |
| GITHUB_REF_NAME: ${{ github.ref_name }} | |
| RELEASE_TARGET: aarch64-apple-darwin | |
| # Fail the release if the binary still dynamically links a C library we | |
| # bundle — the point is SONAME-independence + no host libxml2/libxslt. | |
| - name: verify self-contained binary | |
| run: | | |
| bin=$(find target/release-artifacts -name latexml_oxide -type f | head -1) | |
| echo "checking $bin"; otool -L "$bin" | |
| if otool -L "$bin" | grep -Eiq 'libxml2|libxslt|libexslt|libkpathsea'; then | |
| echo "::error::release binary dynamically links a library that must be static"; exit 1 | |
| fi | |
| echo "OK: no dynamic libxml2/libxslt/kpathsea" | |
| # Ad-hoc signature gate + launch smoke. make_release.sh re-signs the binary | |
| # after `strip` (dash = ad-hoc, no Apple Developer cert); an arm64 Mach-O | |
| # with a broken signature is killed at exec. `codesign --verify` asserts | |
| # the signature survived, and actually running `--version` proves the | |
| # binary launches on this arch end-to-end (the real Killed:9 gate — this | |
| # is what lets the release auto-publish, below, without a manual "download | |
| # and confirm it runs" step). Conversion paths (static libxml2/libxslt, | |
| # XSLT, RelaxNG) are covered by CI.yml's macOS test job on the same arch. | |
| # NOT notarization — ripgrep-style posture, see RELEASING.md "macOS | |
| # Gatekeeper & code signing". | |
| - name: verify code signature + launch smoke | |
| run: | | |
| bin=$(find target/release-artifacts -name latexml_oxide -type f | head -1) | |
| codesign --verify --verbose "$bin" \ | |
| || { echo "::error::macOS binary has no valid code signature — would be Killed:9 on arm64"; exit 1; } | |
| "$bin" --version \ | |
| || { echo "::error::macOS binary failed to launch (--version) — un-runnable artifact"; exit 1; } | |
| echo "OK: valid (ad-hoc) signature and the binary launches" | |
| - name: upload macOS artifact | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: macos-tarball | |
| path: | | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-aarch64-apple-darwin.tar.gz | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-aarch64-apple-darwin.tar.gz.sha256 | |
| if-no-files-found: error | |
| # macOS (Intel / x86_64) release tarball — for Intel Macs (e.g. the 2018 | |
| # MacBook Air). A separate NATIVE leg from build-macos (arm64): a native | |
| # binary is never cross-arch here (we do not cross-compile the static C | |
| # deps), so x86_64 gets its own runner. GitHub retired the `macos-13` Intel | |
| # image on 2025-12-04; `macos-15-intel` is the last free-tier x86_64 macOS | |
| # image, available until ~Fall 2027, after which GitHub Actions drops Intel | |
| # macOS entirely (revisit then — universal/lipo or a self-hosted Intel Mac). | |
| # MACOSX_DEPLOYMENT_TARGET=10.13 keeps the binary runnable on OLDER Intel | |
| # Macs even though the runner's SDK is macOS 15: the 2018 Air shipped with | |
| # 10.14 and tops out at Sonoma 14 (it cannot run Sequoia 15). | |
| build-macos-intel: | |
| name: Build macOS x86_64 ${{ github.ref_name }} | |
| runs-on: macos-15-intel | |
| # Intel runners are slower than Apple Silicon; give fat-LTO generous | |
| # wall-clock (build-macos uses 90 on arm64). | |
| timeout-minutes: 120 | |
| needs: [dumps, notices] | |
| env: | |
| MACOSX_DEPLOYMENT_TARGET: "10.13" | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: install dependencies | |
| run: | | |
| command -v pkg-config >/dev/null || brew install pkgconf | |
| brew install libxml2 libxslt | |
| - name: surface keg-only pkg-config paths | |
| run: | | |
| echo "PKG_CONFIG_PATH=$(brew --prefix libxml2)/lib/pkgconfig:$(brew --prefix libxslt)/lib/pkgconfig" >> "$GITHUB_ENV" | |
| - uses: dtolnay/rust-toolchain@nightly | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| key: release-maxperf-macos-intel | |
| # Same TL-window dumps as the other legs (OS/arch-agnostic gzipped text). | |
| - name: collect TL-window dumps | |
| uses: actions/download-artifact@v6 | |
| with: | |
| pattern: dumps-* | |
| path: resources/dumps/ | |
| merge-multiple: true | |
| - name: verify dump window completeness | |
| run: | | |
| ls -la resources/dumps/ | |
| for year in 2022 2023 2024 2025 2026; do | |
| for f in plain.$year.dump.txt latex.$year.dump.txt texlive.$year.version; do | |
| if [ ! -s "resources/dumps/$f" ]; then | |
| echo "::error::release dump window incomplete: missing resources/dumps/$f" | |
| exit 1 | |
| fi | |
| done | |
| done | |
| - name: build static libkpathsea from source | |
| run: bash tools/build_static_kpathsea.sh | |
| - name: build static libxml2 + libxslt from source | |
| run: bash tools/build_static_libxml.sh | |
| # make_release.sh bundles THIRD-PARTY-NOTICES.dist when present, else it | |
| # silently falls back to the committed sections 1-4 (no Rust appendix). | |
| - name: fetch assembled THIRD-PARTY-NOTICES | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: third-party-notices | |
| - name: build macOS release tarball | |
| run: bash tools/make_release.sh | |
| env: | |
| GITHUB_REF_NAME: ${{ github.ref_name }} | |
| RELEASE_TARGET: x86_64-apple-darwin | |
| - name: verify self-contained binary | |
| run: | | |
| bin=$(find target/release-artifacts -name latexml_oxide -type f | head -1) | |
| echo "checking $bin"; otool -L "$bin" | |
| if otool -L "$bin" | grep -Eiq 'libxml2|libxslt|libexslt|libkpathsea'; then | |
| echo "::error::release binary dynamically links a library that must be static"; exit 1 | |
| fi | |
| echo "OK: no dynamic libxml2/libxslt/kpathsea" | |
| # Ad-hoc signature gate + launch smoke (see build-macos leg). x86_64 does | |
| # not require a signature to run the way arm64 does, but make_release.sh | |
| # re-signs both legs uniformly; assert it here too, and run `--version` so | |
| # this Intel artifact — the one NOT covered by CI.yml's arm64 macOS test | |
| # job — is at least proven to launch before it auto-publishes. | |
| - name: verify code signature + launch smoke | |
| run: | | |
| bin=$(find target/release-artifacts -name latexml_oxide -type f | head -1) | |
| codesign --verify --verbose "$bin" \ | |
| || { echo "::error::macOS binary has no valid code signature"; exit 1; } | |
| "$bin" --version \ | |
| || { echo "::error::macOS Intel binary failed to launch (--version) — un-runnable artifact"; exit 1; } | |
| echo "OK: valid (ad-hoc) signature and the binary launches" | |
| - name: upload macOS Intel artifact | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: macos-intel-tarball | |
| path: | | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-x86_64-apple-darwin.tar.gz | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-x86_64-apple-darwin.tar.gz.sha256 | |
| if-no-files-found: error | |
| # aarch64 (arm64) Linux release tarball + .deb — for ARM Linux (AWS Graviton, | |
| # Ampere, Raspberry Pi OS 64-bit, Apple Silicon Linux VMs). A separate NATIVE | |
| # leg: an arm64 ELF is never cross-compiled here (the static C deps build | |
| # natively), so it gets its own arm64 runner. `ubuntu-22.04-arm` ships glibc | |
| # 2.35 for the same broad-compatibility reason as the x86_64 leg (see header). | |
| # A full build+gate peer of the publishing `release` job below — same static | |
| # linkage, self-contained ldd check, conversion + embedded-resource smokes, | |
| # and size budget — minus the publish (its assets are collected there). | |
| build-linux-arm64: | |
| name: Build Linux arm64 ${{ github.ref_name }} | |
| runs-on: ubuntu-22.04-arm | |
| timeout-minutes: 90 | |
| needs: [dumps, notices] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| # Same build-time + smoke-test apt set as the x86_64 `release` leg (TeX | |
| # Live is needed for the conversion smoke below). | |
| - name: install dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| libxml2-dev libxslt1-dev libkpathsea-dev libkpathsea6 \ | |
| mold \ | |
| dpkg-dev \ | |
| texlive texlive-latex-extra texlive-science \ | |
| texlive-bibtex-extra texlive-publishers \ | |
| poppler-utils | |
| - uses: dtolnay/rust-toolchain@nightly | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| key: release-maxperf-linux-arm64 | |
| # Built from source (not a prebuilt binary) for the same glibc reason as | |
| # the x86_64 leg — the arm64 runner is ubuntu-22.04 (glibc 2.35). | |
| - name: install cargo-deb | |
| run: cargo install cargo-deb --locked | |
| - name: collect TL-window dumps | |
| uses: actions/download-artifact@v6 | |
| with: | |
| pattern: dumps-* | |
| path: resources/dumps/ | |
| merge-multiple: true | |
| - name: verify dump window completeness | |
| run: | | |
| ls -la resources/dumps/ | |
| for year in 2022 2023 2024 2025 2026; do | |
| for f in plain.$year.dump.txt latex.$year.dump.txt texlive.$year.version; do | |
| if [ ! -s "resources/dumps/$f" ]; then | |
| echo "::error::release dump window incomplete: missing resources/dumps/$f" | |
| exit 1 | |
| fi | |
| done | |
| done | |
| - name: build static libkpathsea from source | |
| run: bash tools/build_static_kpathsea.sh | |
| - name: build static libxml2 + libxslt from source | |
| run: bash tools/build_static_libxml.sh | |
| # make_release.sh bundles THIRD-PARTY-NOTICES.dist when present, else it | |
| # silently falls back to the committed sections 1-4 (no Rust appendix). | |
| - name: fetch assembled THIRD-PARTY-NOTICES | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: third-party-notices | |
| # cargo-deb derives `Architecture: arm64` from the native host; make_release | |
| # labels the FILENAME arm64 from RELEASE_TARGET. Produces the tarball + .deb. | |
| - name: build release artifacts | |
| run: bash tools/make_release.sh | |
| env: | |
| GITHUB_REF_NAME: ${{ github.ref_name }} | |
| RELEASE_TARGET: aarch64-unknown-linux-gnu | |
| - name: binary size budget | |
| run: | | |
| bin=$(find target/release-artifacts -name latexml_oxide -type f | head -1) | |
| size=$(stat -c%s "$bin"); mb=$(( size / 1024 / 1024 )) | |
| echo "latexml_oxide (stripped, arm64): ${mb} MB (${size} bytes)" | |
| budget=64 | |
| if [ "$mb" -gt "$budget" ]; then | |
| echo "::error::binary ${mb} MB exceeds ${budget} MB budget (RELEASE_CRITERIA §2)"; exit 1 | |
| fi | |
| echo "OK: within ${budget} MB size budget" | |
| # Structural (no bundled C lib stays dynamic) + functional (a real | |
| # conversion exercises the RelaxNG/XPath/XSLT paths through our static | |
| # libxml2/libxslt on arm64 — catches a mis-trimmed static config that | |
| # links but fails at runtime). | |
| - name: verify self-contained binary | |
| run: | | |
| bin=$(find target/release-artifacts -name latexml_oxide -type f | head -1) | |
| echo "checking $bin"; ldd "$bin" || true | |
| if ldd "$bin" | grep -Eiq 'libxml2|libxslt|libexslt|libkpathsea'; then | |
| echo "::error::release binary dynamically links a library that must be static"; exit 1 | |
| fi | |
| echo "OK: glibc-only (no dynamic libxml2/libxslt/kpathsea)" | |
| out="$(mktemp -d)" | |
| "$bin" --destination="$out/hello.html" latexml_oxide/tests/hello/hello.tex 2>"$out/log" \ | |
| || { echo "::error::release binary failed to run a conversion"; cat "$out/log"; exit 1; } | |
| errs=$(sed 's/\x1b\[[0-9;]*m//g' "$out/log" | grep -acE '^(Error|Fatal):' || true) | |
| if [ "$errs" != "0" ] || [ ! -s "$out/hello.html" ]; then | |
| echo "::error::conversion smoke: $errs Error/Fatal, $(wc -c <"$out/hello.html" 2>/dev/null || echo 0) bytes out" | |
| sed 's/\x1b\[[0-9;]*m//g' "$out/log" | grep -E '^(Error|Fatal):' | head; exit 1 | |
| fi | |
| echo "OK: hello.tex converts clean on arm64 ($(wc -c <"$out/hello.html") bytes, 0 errors)" | |
| - name: embedded-resource smoke (no own-resource disk read) | |
| run: | | |
| bin=$(find target/release-artifacts -name latexml_oxide -type f | head -1) | |
| mv resources resources.hidden | |
| out="$(mktemp -d)"; set +e | |
| "$bin" --destination="$out/hello.html" latexml_oxide/tests/hello/hello.tex 2>"$out/log" | |
| rc=$?; set -e | |
| mv resources.hidden resources | |
| if [ "$rc" -ne 0 ] || [ ! -s "$out/hello.html" ]; then | |
| echo "::error::conversion failed with resources/ removed — self-containment regression" | |
| cat "$out/log"; exit 1 | |
| fi | |
| if ! grep -q 'ltx_' "$out/hello.html"; then | |
| echo "::error::output missing ltx_ classes — embedded CSS/XSLT not applied"; exit 1 | |
| fi | |
| echo "OK: converts with resources/ removed (fully self-contained)" | |
| - name: upload Linux arm64 artifacts | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: linux-arm64-artifacts | |
| path: | | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-aarch64-unknown-linux-gnu.tar.gz | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-aarch64-unknown-linux-gnu.tar.gz.sha256 | |
| target/release-artifacts/latexml-oxide_${{ github.ref_name }}-1_arm64.deb | |
| target/release-artifacts/latexml-oxide_${{ github.ref_name }}-1_arm64.deb.sha256 | |
| if-no-files-found: error | |
| # Windows (x86_64-pc-windows-msvc) — a single self-contained `latexml_oxide.exe`, | |
| # shipped in a `.zip` (+ `.sha256`) so the binary travels with its license | |
| # notices; there is no .deb equivalent. Uses the proven | |
| # windows-ci-manual-trigger.yml recipe (vcpkg static libxml2/libxslt) but builds | |
| # the publish-grade `maxperf` binary. (It was a bare `.exe` through 0.7.4-rc3, | |
| # which made it the one download carrying no THIRD-PARTY-NOTICES — see | |
| # LICENSE_INVENTORY F7.) | |
| # `kpathsea` is linked IN-PROCESS via a STATIC libkpathsea built from source | |
| # (`kpathsea-build-from-source`; see the build step) — no runtime | |
| # `kpathsealibw64.dll`. At runtime `select_kpaths` keeps the fast in-process | |
| # backend on TeX Live and falls back to subprocess `kpsewhich` on MiKTeX (whose | |
| # MPM fndb a static libkpathsea can't read). The `.exe` is FULLY STATIC — | |
| # static C libs (x64-windows-static) + static CRT (+crt-static) — so it imports | |
| # only core OS DLLs and runs on any Windows with no VC++ redistributable. | |
| # The TL-window dumps are OS-agnostic gzipped text, embedded at build time like | |
| # the other legs (no TeX Live needed on THIS leg — dumps come from `dumps`). | |
| build-windows: | |
| name: Build Windows x86_64 ${{ github.ref_name }} | |
| runs-on: windows-latest | |
| # First-ever Windows release artifact; maxperf (fat-LTO) is heavier than the | |
| # bring-up job's `ci` profile — give generous wall-clock. Windows Actions | |
| # minutes are metered 2x on this private repo (see windows-ci-manual-trigger). | |
| timeout-minutes: 120 | |
| needs: [dumps, notices] | |
| env: | |
| CARGO_NET_GIT_FETCH_WITH_CLI: "true" | |
| RUST_BACKTRACE: "1" | |
| # Fully static: static C libraries AND static CRT. `x64-windows-static` | |
| # builds libxml2/libxslt against the static CRT (/MT), matching the | |
| # `-C target-feature=+crt-static` Rust flag on the build step below — the | |
| # whole image is one CRT model. Result: the `.exe` imports NO | |
| # VCRUNTIME140.dll and runs on ANY Windows with no VC++ redistributable | |
| # (ripgrep's posture; unlike the earlier static-md = dynamic-CRT build, | |
| # which needed the redist). Flipping the triplet also rotates the vcpkg | |
| # cache key below, forcing a fresh static-CRT build of the C libs. | |
| VCPKG_TRIPLET: x64-windows-static | |
| VCPKGRS_TRIPLET: x64-windows-static | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - uses: dtolnay/rust-toolchain@nightly | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| key: release-maxperf-windows | |
| - name: cache vcpkg installed tree | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ env.VCPKG_INSTALLATION_ROOT }}\installed | |
| key: vcpkg-${{ env.VCPKG_TRIPLET }}-libxml2-libxslt-v1 | |
| - name: vcpkg install libxml2 + libxslt (static) | |
| run: | | |
| & "$env:VCPKG_INSTALLATION_ROOT\vcpkg" install ` | |
| "libxml2:$env:VCPKG_TRIPLET" "libxslt:$env:VCPKG_TRIPLET" | |
| - name: export vcpkg root | |
| run: '"VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" >> $env:GITHUB_ENV' | |
| # Same TL-window dumps as the other legs (OS-agnostic gzipped text). | |
| - name: collect TL-window dumps | |
| uses: actions/download-artifact@v6 | |
| with: | |
| pattern: dumps-* | |
| path: resources/dumps/ | |
| merge-multiple: true | |
| - name: verify dump window completeness | |
| shell: bash | |
| run: | | |
| ls -la resources/dumps/ | |
| for year in 2022 2023 2024 2025 2026; do | |
| for f in plain.$year.dump.txt latex.$year.dump.txt texlive.$year.version; do | |
| if [ ! -s "resources/dumps/$f" ]; then | |
| echo "::error::release dump window incomplete: missing resources/dumps/$f" | |
| exit 1 | |
| fi | |
| done | |
| done | |
| # make_release.sh bundles THIRD-PARTY-NOTICES.dist when present, else it | |
| # silently falls back to the committed sections 1-4 (no Rust appendix). | |
| - name: fetch assembled THIRD-PARTY-NOTICES | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: third-party-notices | |
| # make_release.sh runs under Git Bash (present on windows-latest); its | |
| # os_family=windows branch builds maxperf and packages latexml_oxide.exe | |
| # into a `.zip` alongside the notices. | |
| # libxml2/libxslt are discovered from the vcpkg env set above. | |
| - name: build Windows release .zip | |
| shell: bash | |
| run: bash tools/make_release.sh | |
| env: | |
| GITHUB_REF_NAME: ${{ github.ref_name }} | |
| RELEASE_TARGET: x86_64-pc-windows-msvc | |
| # IN-PROCESS kpathsea via a STATIC libkpathsea built from source | |
| # (`kpathsea-build-from-source`). This removes the subprocess | |
| # `ls-R`-cache-build cost (~0.5 s/conversion — the RC's dominant | |
| # Windows overhead on simple docs; WINDOWS_COMPATIBILITY_PLAN Phase 5.1) | |
| # AND stays self-contained: a STATIC link has no runtime | |
| # `kpathsealibw64.dll` dependency, so it launches on any Windows / any | |
| # TeX distro — unlike the dynamic-DLL path (the Phase-2 launch landmine) | |
| # that the old `KPATHSEA_NO_LINK=1` subprocess backend avoided at a perf | |
| # cost. `kpathsea_sys` fetches the pinned kpathsea source at build time | |
| # (git on the runner; LGPL — see THIRD-PARTY-NOTICES, same posture as the | |
| # Linux/macOS `build_static_kpathsea.sh` legs). | |
| # * RELEASE_EXTRA_FEATURES → make_release.sh appends the feature. | |
| # * KPATHSEA_SKIP_TOOLCHAIN_CHECK=1 — this runner has no TeX; a linked | |
| # (in-process) build needs no build-time `kpsewhich`. | |
| # Combined with the `+crt-static` / x64-windows-static leg config below | |
| # (drops VCRUNTIME140), this yields a fully-static, redistributable `.exe`. | |
| RELEASE_EXTRA_FEATURES: kpathsea-build-from-source | |
| KPATHSEA_SKIP_TOOLCHAIN_CHECK: "1" | |
| # Static CRT: statically link the VC runtime + UCRT so the shipped | |
| # `.exe` has NO VCRUNTIME140.dll dependency and launches on a clean | |
| # Windows with no VC++ redistributable. MUST pair with the | |
| # x64-windows-static vcpkg triplet above (one CRT model across Rust | |
| # std + the C libs; mixing /MT and /MD is unsupported). RUSTFLAGS env | |
| # REPLACES `.cargo/config.toml` rustflags, so re-list the | |
| # frontend-parallelism flags to keep them. | |
| RUSTFLAGS: "-Zthreads=8 -Zunstable-options -C target-feature=+crt-static" | |
| # Launch smoke + self-containment guard. `--version` alone is NOT enough: | |
| # the runner has the VC++ redist, so even a dynamic-CRT `.exe` would launch | |
| # here. So we ALSO assert (via dumpbin) that the import table carries no | |
| # VCRUNTIME — the actual property that lets the `.exe` run on a clean | |
| # Windows. Conversion paths aren't exercised (no TeX Live on this leg); | |
| # windows-ci-manual-trigger.yml runs the full suite with TeX Live. | |
| - name: launch smoke + self-containment guard | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| exe=$(find target/release-artifacts -name '*.exe' -type f | head -1) | |
| echo "checking $exe" | |
| "$exe" --version || { echo "::error::Windows .exe failed to launch (--version)"; exit 1; } | |
| echo "OK: the Windows .exe launches" | |
| # `--version` alone is NOT enough: the runner has the VC++ redist + TeX | |
| # nowhere near, so even a non-portable exe launches. Assert (via dumpbin, | |
| # located through vswhere) that the import table carries NONE of the | |
| # things that would tie the exe to this machine: the VC runtime | |
| # (+crt-static), kpathsea's DLL (build_from_source static, not the | |
| # dynamic kpathsealibw64.dll path), or libxml2/libxslt (vcpkg static). | |
| vswhere="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" | |
| vsroot=$("$vswhere" -latest -property installationPath) | |
| dumpbin=$(find "$vsroot/VC/Tools/MSVC" -name dumpbin.exe -ipath '*hostx64*x64*' | head -1) | |
| deps=$("$dumpbin" //DEPENDENTS "$exe") | |
| if echo "$deps" | grep -qiE 'vcruntime|kpathsea|libxml|libxslt'; then | |
| echo "$deps" | |
| echo "::error::the .exe imports a non-OS DLL (VCRUNTIME / kpathsea / libxml*) — it is not fully static (check +crt-static, x64-windows-static, and kpathsea-build-from-source all took effect)" | |
| exit 1 | |
| fi | |
| echo "OK: imports only core OS DLLs — fully static, runs on any Windows / any TeX distro" | |
| - name: upload Windows artifact | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: windows-zip | |
| path: | | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-x86_64-pc-windows-msvc.zip | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-x86_64-pc-windows-msvc.zip.sha256 | |
| if-no-files-found: error | |
| release: | |
| name: Build and publish ${{ github.ref_name }} | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 60 | |
| needs: [dumps, notices, build-macos, build-macos-intel, build-linux-arm64, build-windows] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| # Full history so CHANGELOG.md slicing can see prior entries | |
| # if we extend the release-body extraction later. | |
| fetch-depth: 0 | |
| # Runtime + build-time apt deps. Matches CI.yml's set, minus | |
| # language packs the release build doesn't exercise. | |
| - name: install dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| libxml2-dev libxslt1-dev libkpathsea-dev libkpathsea6 \ | |
| mold \ | |
| dpkg-dev \ | |
| texlive texlive-latex-extra texlive-science \ | |
| texlive-bibtex-extra texlive-publishers \ | |
| poppler-utils | |
| - uses: dtolnay/rust-toolchain@nightly | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| # Release builds use `--profile maxperf`; keep its target dir | |
| # warm across re-runs of the same tag (rare, but possible | |
| # when fixing the workflow itself before re-tagging). | |
| cache-on-failure: true | |
| key: release-maxperf | |
| # Build cargo-deb from source rather than using a prebuilt binary | |
| # (taiki-e/install-action): the prebuilt cargo-deb is linked against a | |
| # newer glibc (2.39) than this ubuntu-22.04 runner ships (2.35, chosen for | |
| # broad binary compatibility — see the header), so it fails at runtime with | |
| # `GLIBC_2.39 not found`. Compiling here links it against the runner's own | |
| # glibc. rust-cache keeps the cargo registry warm across re-runs. | |
| - name: install cargo-deb | |
| run: cargo install cargo-deb --locked | |
| # Collect the TL-window dumps generated by the `dumps` job into | |
| # resources/dumps/ so the maxperf build embeds all of them | |
| # (latexml_engine/build.rs scans this directory). Strict | |
| # completeness gate: 5 years x (plain + latex + version stamp). | |
| - name: collect TL-window dumps | |
| uses: actions/download-artifact@v6 | |
| with: | |
| pattern: dumps-* | |
| path: resources/dumps/ | |
| merge-multiple: true | |
| - name: verify dump window completeness | |
| run: | | |
| ls -la resources/dumps/ | |
| # The full 5-year moving window. 2026 rejoined it 2026-07-23 (the | |
| # TL2026 container and the zero-error init gate both cleared — see | |
| # the matrix comment in release-dumps.yml), so a TL2026 host now gets | |
| # an exact-year dump instead of falling back to the 2025 one. | |
| for year in 2022 2023 2024 2025 2026; do | |
| for f in plain.$year.dump.txt latex.$year.dump.txt texlive.$year.version; do | |
| if [ ! -s "resources/dumps/$f" ]; then | |
| echo "::error::release dump window incomplete: missing resources/dumps/$f" | |
| exit 1 | |
| fi | |
| done | |
| done | |
| # Static, self-contained libkpathsea (in-process lookups, no runtime | |
| # libkpathsea6 dependency — dropping it from the .deb's $auto deps). | |
| # Built from source with --with-pic: the system libkpathsea-dev .a is | |
| # non-PIC and is rejected when our proc-macro cdylib links kpathsea. | |
| # Sets KPATHSEA_LIB_DIR + KPATHSEA_STATIC for the build below. | |
| - name: build static libkpathsea from source | |
| run: bash tools/build_static_kpathsea.sh | |
| # Static, self-contained libxml2 + libxslt + libexslt. Source-built PIC (the | |
| # system libxml2.a is non-PIC and our proc-macro cdylib links libxml; libxslt | |
| # ships no system .a at all). Makes the binary independent of the host libxml2 | |
| # SONAME (.so.2 -> .so.16 at libxml2 2.14) and drops libxml2/libxslt from the | |
| # .deb's $auto deps. Sets PKG_CONFIG_PATH + LIBXML2_STATIC + LIBXSLT_STATIC. | |
| - name: build static libxml2 + libxslt from source | |
| run: bash tools/build_static_libxml.sh | |
| # The complete THIRD-PARTY-NOTICES (hand-authored sections 1-4 + | |
| # cargo-about's per-crate licenses + the section 6 copyleft texts) is | |
| # assembled ONCE by the `notices` job and shared with every packaging leg, | |
| # so all platforms ship byte-identical notices. make_release.sh bundles it | |
| # into the tarball/.deb, and it publishes as a standalone asset below. | |
| # See docs/release/LICENSE_INVENTORY.md §A / RELEASE_CRITERIA §4 (F4/F7). | |
| - name: fetch assembled THIRD-PARTY-NOTICES | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: third-party-notices | |
| - name: build release artifacts | |
| run: bash tools/make_release.sh | |
| env: | |
| # make_release.sh verifies this matches the Cargo.toml version | |
| # and refuses to build on mismatch. | |
| GITHUB_REF_NAME: ${{ github.ref_name }} | |
| # Include the macOS install sections in the shared release body. The | |
| # tarball files themselves are collected from the build-macos / | |
| # build-macos-intel artifacts in a later step — make_release.sh wipes | |
| # target/release-artifacts/ at start, so the downloads MUST follow it. | |
| RELEASE_MACOS_TARBALL: latexml-oxide-${{ github.ref_name }}-aarch64-apple-darwin.tar.gz | |
| RELEASE_MACOS_INTEL_TARBALL: latexml-oxide-${{ github.ref_name }}-x86_64-apple-darwin.tar.gz | |
| RELEASE_LINUX_ARM64_TARBALL: latexml-oxide-${{ github.ref_name }}-aarch64-unknown-linux-gnu.tar.gz | |
| RELEASE_WINDOWS_ZIP: latexml-oxide-${{ github.ref_name }}-x86_64-pc-windows-msvc.zip | |
| # Artifact-property gate (RELEASE_CRITERIA §2/§7): fail the release if the | |
| # stripped binary blows the size budget (a runaway-growth alarm), and stage | |
| # the assembled notices for publishing. (Corpus smoke + telemetry are | |
| # deliberately NOT run in CI — too expensive.) | |
| - name: binary size budget + stage notices | |
| run: | | |
| bin=$(find target/release-artifacts -name latexml_oxide -type f | head -1) | |
| size=$(stat -c%s "$bin"); mb=$(( size / 1024 / 1024 )) | |
| echo "latexml_oxide (stripped): ${mb} MB (${size} bytes)" | |
| budget=64 | |
| if [ "$mb" -gt "$budget" ]; then | |
| echo "::error::binary ${mb} MB exceeds ${budget} MB budget (RELEASE_CRITERIA §2)"; exit 1 | |
| fi | |
| echo "OK: within ${budget} MB size budget" | |
| cp THIRD-PARTY-NOTICES.dist target/release-artifacts/THIRD-PARTY-NOTICES | |
| # Fail the release if the Linux binary still dynamically links a C library we | |
| # bundle — the point is a glibc-only, SONAME-independent artifact. Runs before | |
| # the macOS tarball is downloaded, so `find` inspects the Linux binary. | |
| - name: verify self-contained binary | |
| run: | | |
| bin=$(find target/release-artifacts -name latexml_oxide -type f | head -1) | |
| echo "checking $bin"; ldd "$bin" || true | |
| if ldd "$bin" | grep -Eiq 'libxml2|libxslt|libexslt|libkpathsea'; then | |
| echo "::error::release binary dynamically links a library that must be static"; exit 1 | |
| fi | |
| echo "OK: glibc-only (no dynamic libxml2/libxslt/kpathsea)" | |
| # Functional gate (complements the structural ldd above): our static | |
| # libxml2/libxslt are built with a deliberately minimal feature set, so a | |
| # mis-trimmed config could drop something LaTeXML needs — that would LINK | |
| # fine but fail at conversion. Convert a real document (exercises the | |
| # RelaxNG / XPath / XSLT paths) and assert zero Error/Fatal. TeX Live is | |
| # installed on this leg. | |
| out="$(mktemp -d)" | |
| "$bin" --destination="$out/hello.html" latexml_oxide/tests/hello/hello.tex 2>"$out/log" \ | |
| || { echo "::error::release binary failed to run a conversion"; cat "$out/log"; exit 1; } | |
| errs=$(sed 's/\x1b\[[0-9;]*m//g' "$out/log" | grep -acE '^(Error|Fatal):' || true) | |
| if [ "$errs" != "0" ] || [ ! -s "$out/hello.html" ]; then | |
| echo "::error::conversion smoke: $errs Error/Fatal, $(wc -c <"$out/hello.html" 2>/dev/null || echo 0) bytes out" | |
| sed 's/\x1b\[[0-9;]*m//g' "$out/log" | grep -E '^(Error|Fatal):' | head; exit 1 | |
| fi | |
| echo "OK: hello.tex converts clean ($(wc -c <"$out/hello.html") bytes, 0 errors)" | |
| # Prove the binary reads NONE of its own resources from disk: move the | |
| # dev-tree resources/ aside and re-run a conversion. Dumps, XSLT, RelaxNG, | |
| # and CSS must all come from the embedded tables (RELEASE_CRITERIA §7; | |
| # complements tests/001_single_binary_smoke.rs, which only isolates cwd). | |
| - name: embedded-resource smoke (no own-resource disk read) | |
| run: | | |
| bin=$(find target/release-artifacts -name latexml_oxide -type f | head -1) | |
| mv resources resources.hidden | |
| out="$(mktemp -d)"; set +e | |
| "$bin" --destination="$out/hello.html" latexml_oxide/tests/hello/hello.tex 2>"$out/log" | |
| rc=$?; set -e | |
| mv resources.hidden resources | |
| if [ "$rc" -ne 0 ] || [ ! -s "$out/hello.html" ]; then | |
| echo "::error::conversion failed with resources/ removed — self-containment regression" | |
| cat "$out/log"; exit 1 | |
| fi | |
| if ! grep -q 'ltx_' "$out/hello.html"; then | |
| echo "::error::output missing ltx_ classes — embedded CSS/XSLT not applied"; exit 1 | |
| fi | |
| echo "OK: converts with resources/ removed (fully self-contained)" | |
| # Pull the macOS tarball (+ sidecar) built by the build-macos job into | |
| # the staging dir so they publish alongside the Linux artifacts. | |
| - name: collect macOS tarball | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: macos-tarball | |
| path: target/release-artifacts/ | |
| - name: collect macOS Intel tarball | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: macos-intel-tarball | |
| path: target/release-artifacts/ | |
| # Pull the arm64 Linux tarball + .deb (+ sidecars) built by | |
| # build-linux-arm64 into the staging dir so they publish alongside the | |
| # x86_64 Linux + macOS assets. | |
| - name: collect Linux arm64 artifacts | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: linux-arm64-artifacts | |
| path: target/release-artifacts/ | |
| # Pull the single Windows .exe (+ sidecar) built by build-windows. | |
| - name: collect Windows .zip | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: windows-zip | |
| path: target/release-artifacts/ | |
| # PUBLISH. Behaviour is chosen by the tag shape (see draft/prerelease below): | |
| # * Final tag X.Y.Z -> a PUBLIC release, zero manual steps. | |
| # * RC tag X.Y.Z-rcN -> a DRAFT prerelease for cross-OS testing. | |
| # Auto-publishing a final tag is safe because every asset is gated before | |
| # it gets here: each build leg verifies static linkage (ldd/otool), runs a | |
| # real conversion smoke (Linux legs) or a launch smoke + code-signature | |
| # check (macOS legs) or a launch smoke (Windows leg), and enforces the size | |
| # budget; macOS conversion paths are additionally covered by CI.yml's macOS | |
| # test job. The build stays tied to a release tag (no workflow_dispatch, no | |
| # "random" builds). If a published asset is ever found broken, delete the | |
| # Release + tag, bump the patch, and re-tag (RELEASING.md "Failure recovery"). | |
| - name: publish GitHub release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ github.ref_name }} | |
| name: ${{ github.ref_name }} | |
| body_path: target/release-artifacts/RELEASE_BODY.md | |
| # Append GitHub's auto-generated "What's Changed" — every merged PR | |
| # since the previous tag — below our curated RELEASE_BODY summary | |
| # (softprops pre-pends `body` to the generated notes). Brief, high-level | |
| # highlights up top + the full per-PR list underneath. | |
| generate_release_notes: true | |
| files: | | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-x86_64-unknown-linux-gnu.tar.gz | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-x86_64-unknown-linux-gnu.tar.gz.sha256 | |
| target/release-artifacts/latexml-oxide_${{ github.ref_name }}-1_amd64.deb | |
| target/release-artifacts/latexml-oxide_${{ github.ref_name }}-1_amd64.deb.sha256 | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-aarch64-unknown-linux-gnu.tar.gz | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-aarch64-unknown-linux-gnu.tar.gz.sha256 | |
| target/release-artifacts/latexml-oxide_${{ github.ref_name }}-1_arm64.deb | |
| target/release-artifacts/latexml-oxide_${{ github.ref_name }}-1_arm64.deb.sha256 | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-aarch64-apple-darwin.tar.gz | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-aarch64-apple-darwin.tar.gz.sha256 | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-x86_64-apple-darwin.tar.gz | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-x86_64-apple-darwin.tar.gz.sha256 | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-x86_64-pc-windows-msvc.zip | |
| target/release-artifacts/latexml-oxide-${{ github.ref_name }}-x86_64-pc-windows-msvc.zip.sha256 | |
| target/release-artifacts/THIRD-PARTY-NOTICES | |
| # RC / pre-release tags (containing a '-', e.g. 0.7.4-rc1) publish as a | |
| # DRAFT prerelease — a maintainer tests the attached executables on each | |
| # OS, then clicks Publish (or deletes the draft). Final tags (bare | |
| # X.Y.Z) auto-publish a public release. | |
| draft: ${{ contains(github.ref_name, '-') }} | |
| prerelease: ${{ contains(github.ref_name, '-') }} | |
| fail_on_unmatched_files: true | |
| # Container images for a FINAL tag, gated on the release above actually | |
| # publishing (`needs: release`) — so `:latest` never points at a version with no | |
| # downloadable release. RC tags are skipped here: their draft is unreviewed, and | |
| # an RC must not take `:latest`. To build images for an RC, publish its draft | |
| # (docker.yml's `release: published` trigger) or dispatch docker.yml directly; | |
| # both leave `:latest` alone. | |
| # | |
| # Why a call and not a `release: published` listener inside docker.yml: this job | |
| # publishes with GITHUB_TOKEN, and GitHub will not start a workflow from a | |
| # GITHUB_TOKEN-authored event (recursion guard). 0.7.4's images were missing for | |
| # exactly that reason, with no failure to notice. See docs/release/RELEASING.md. | |
| containers: | |
| name: Container images ${{ github.ref_name }} | |
| needs: release | |
| if: ${{ !contains(github.ref_name, '-') }} | |
| # The workflow-level grant is `contents: write`; pushing to GHCR additionally | |
| # needs `packages: write`, and a called workflow cannot escalate past what the | |
| # calling job hands it — so it must be granted right here. | |
| permissions: | |
| contents: read | |
| packages: write | |
| uses: ./.github/workflows/docker.yml | |
| with: | |
| ref: ${{ github.ref_name }} | |
| latest: true |