Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions .github/workflows/release-runtime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
name: Release self-contained Gloss runtime

on:
push:
tags:
- "v*"
workflow_dispatch:

permissions:
contents: read

jobs:
macos-runtime:
name: macOS runtime (${{ matrix.architecture }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15
architecture: arm64
- runner: macos-15-intel
architecture: x86_64
steps:
- name: Checkout release source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false

- name: Setup uv with Python 3.12
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
with:
python-version: "3.12"
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Install locked build dependencies
run: uv sync --frozen --no-default-groups --group release

- name: Validate release tag
id: release
shell: bash
run: |
version=$(uv run --frozen --no-default-groups --group release python -c \
'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
expected_tag="v${version/+/-}"
if [[ "$GITHUB_EVENT_NAME" = "push" ]]; then
actual_tag=$(git describe --tags --exact-match)
test "$actual_tag" = "$expected_tag"
test "$GITHUB_REF_TYPE" = "tag"
test "$GITHUB_REF_NAME" = "$expected_tag"
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "source_date_epoch=$(git show -s --format=%ct HEAD)" >> "$GITHUB_OUTPUT"

- name: Build self-contained runtime
shell: bash
run: |
mkdir -p release
test "$(uname -m)" = "${{ matrix.architecture }}"
bash scripts/release/build_macos_runtime.sh \
"${{ steps.release.outputs.version }}" \
"${{ steps.release.outputs.source_date_epoch }}" \
"$PWD/release"

- name: Smoke packaged services without system Python
shell: bash
run: |
tag_version=${{ steps.release.outputs.version }}
tag_version=${tag_version/+/-}
archive="release/gloss-babeldoc-${tag_version}-macos-${{ matrix.architecture }}.tar.gz"
test -s "$archive"
mkdir -p "$RUNNER_TEMP/runtime-smoke"
tar -xzf "$archive" -C "$RUNNER_TEMP/runtime-smoke"
uv run --frozen --no-default-groups --group release python \
scripts/release/smoke_runtime.py \
"$RUNNER_TEMP/runtime-smoke/gloss-babeldoc-runtime/gloss-babeldoc"

- name: Upload native runtime
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: gloss-babeldoc-macos-${{ matrix.architecture }}
path: release/*.tar.gz
if-no-files-found: error
retention-days: 7

publish:
name: Sign, attest, and publish
needs: macos-runtime
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
attestations: write
contents: write
id-token: write
steps:
- name: Checkout release source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false

- name: Setup uv with Python 3.12
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
with:
python-version: "3.12"
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Validate release tag
id: release
shell: bash
run: |
version=$(uv run --frozen --no-default-groups python -c \
'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
expected_tag="v${version/+/-}"
actual_tag=$(git describe --tags --exact-match)
test "$actual_tag" = "$expected_tag"
test "$GITHUB_REF_TYPE" = "tag"
test "$GITHUB_REF_NAME" = "$expected_tag"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=$expected_tag" >> "$GITHUB_OUTPUT"
echo "tag_version=${version/+/-}" >> "$GITHUB_OUTPUT"
echo "source_date_epoch=$(git show -s --format=%ct HEAD)" >> "$GITHUB_OUTPUT"

- name: Install locked release dependencies
run: uv sync --frozen --no-default-groups

- name: Download native runtimes
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: gloss-babeldoc-macos-*
path: release
merge-multiple: true

- name: Verify native runtime set
shell: bash
run: |
test -s "release/gloss-babeldoc-${{ steps.release.outputs.tag_version }}-macos-arm64.tar.gz"
test -s "release/gloss-babeldoc-${{ steps.release.outputs.tag_version }}-macos-x86_64.tar.gz"

- name: Build reproducible wheel and source distribution
shell: bash
env:
SOURCE_DATE_EPOCH: ${{ steps.release.outputs.source_date_epoch }}
run: |
mkdir -p "$RUNNER_TEMP/dist-first" "$RUNNER_TEMP/dist-second"
uv build --wheel --sdist --out-dir "$RUNNER_TEMP/dist-first"
uv build --wheel --sdist --out-dir "$RUNNER_TEMP/dist-second"
for first in "$RUNNER_TEMP/dist-first"/*; do
name=$(basename "$first")
cmp "$first" "$RUNNER_TEMP/dist-second/$name"
done
cp "$RUNNER_TEMP/dist-first"/* release/

- name: Build deterministic source archive
shell: bash
run: |
prefix="BabelDOC-${{ steps.release.outputs.tag_version }}/"
git archive --format=tar --prefix="$prefix" HEAD > "$RUNNER_TEMP/source.tar"
gzip -n -9 "$RUNNER_TEMP/source.tar"
mv "$RUNNER_TEMP/source.tar.gz" \
"release/BabelDOC-${{ steps.release.outputs.tag_version }}-source.tar.gz"
cp LICENSE NOTICE release/

- name: Generate runtime manifest and provenance
shell: bash
run: |
uv run --frozen --no-default-groups python \
-m scripts.release.runtime_manifest \
--release-dir release \
--repository "$GITHUB_REPOSITORY" \
--commit "$GITHUB_SHA" \
--source-date-epoch "${{ steps.release.outputs.source_date_epoch }}"

- name: Sign and verify runtime manifest
shell: bash
env:
BABELDOC_RUNTIME_SIGNING_KEY_BASE64: ${{ secrets.BABELDOC_RUNTIME_SIGNING_KEY_BASE64 }}
run: |
test -n "$BABELDOC_RUNTIME_SIGNING_KEY_BASE64"
uv run --frozen --no-default-groups python \
-m scripts.release.sign_manifest \
--manifest release/gloss-runtime-manifest.json \
--signature release/gloss-runtime-manifest.json.sig
uv run --frozen --no-default-groups python \
-m scripts.release.sign_manifest \
--manifest release/gloss-runtime-manifest.json \
--signature release/gloss-runtime-manifest.json.sig \
--verify
cp release/gloss-runtime-manifest.json.sig \
release/gloss-runtime-manifest.sig

- name: Generate and verify release checksums
shell: bash
run: |
(
cd release
while IFS= read -r file; do
sha256sum "$file"
done < <(find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%f\n' | LC_ALL=C sort) \
> SHA256SUMS
sha256sum --check SHA256SUMS
)

- name: Attest release artifacts
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: release/*

- name: Publish GitHub release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
tag_name: ${{ steps.release.outputs.tag }}
name: BabelDOC ${{ steps.release.outputs.version }}
body_path: docs/release-notes/v${{ steps.release.outputs.tag_version }}.md
files: release/*
fail_on_unmatched_files: true
generate_release_notes: false
draft: false
prerelease: false
make_latest: true
4 changes: 4 additions & 0 deletions DOWNSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ Downstream builds are distributed through GitHub Releases or a Gloss-managed
runtime manifest. They are not published to the public PyPI project owned by
upstream. Every binary release must link to its exact source tag and include
artifact checksums, dependency/build inputs, license notices, and provenance.
The runtime manifest is signed with the dedicated Ed25519 release key; Gloss
pins the corresponding public key and rejects missing or invalid signatures.
macOS releases include native arm64 and x86_64 self-contained archives, so an
end user does not need to install Python or BabelDOC separately.

## Upstream synchronization

Expand Down
6 changes: 4 additions & 2 deletions DOWNSTREAM_PATCHES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ upstream, and the condition under which it can be removed.
| `gloss-0002` | Active | Add a lightweight, versioned runtime capability handshake for Gloss. | Downstream integration boundary. | Upstream exposes an equivalent stable runtime protocol. |
| `gloss-0003` | Active | Add reproducible dependency locking and Linux/macOS package validation. | Candidate upstream CI improvement. | Upstream adopts equivalent locked cross-platform release checks. |
| `gloss-0004` | Active | Add an authenticated, reconnectable single-worker service boundary with targeted cancellation and process isolation. | Candidate upstream executor lifecycle and correctness fixes. | Upstream exposes an equivalent authenticated service contract adopted by Gloss. |
| `gloss-0005` | Active | Move font lookup caching, bounded layout-IR session reuse, and phase telemetry into the service runtime. | Candidate upstream PDF pipeline performance work. | Upstream exposes equivalent cache and telemetry contracts adopted by Gloss. |
| `gloss-0006` | Active | Publish signed, self-contained macOS runtimes plus reproducible Python/source artifacts and provenance. | Downstream release integration. | Gloss no longer installs BabelDOC as a managed runtime. |

Runtime, performance, and compatibility patches will be added in separate,
focused commits and PRs.
Compatibility patches will continue to be added in separate, focused commits
and PRs.
18 changes: 18 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,21 @@ public issue.
Gloss downstream security fixes are made on the latest supported downstream
release. An issue inherited from BabelDOC may also be reported privately to the
upstream maintainers.

## Code-scanning compatibility annotations

The vendored `babeldoc/pdfminer` implementation must decode encrypted PDF
files according to ISO 32000. Legacy revisions require MD5, while revisions 5
and 6 prescribe exact SHA-2 transforms. Those calls are format parsers, not
password-storage functions, and replacing them with Argon2, bcrypt, or PBKDF2
would make valid PDFs unreadable. Narrow, query-specific suppression annotations
on those mandated operations record that reviewed exception without disabling
the query for other code.

Legacy MD5 calls go through a dedicated constructor with Python's
`usedforsecurity=False` flag. The remaining SHA-2 suppressions use CodeQL's
otherwise-empty preceding-line form and apply only to the exact mandated
operations.
Control characters removed by the XML converter are expressed with explicit
raw-string hexadecimal ranges so scanners and reviewers see the intended XML
1.0 character set unambiguously.
2 changes: 1 addition & 1 deletion UPSTREAM_BASE.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Gloss downstream provenance. Update downstream.version for every downstream
# release; update the upstream block only in a reviewed upstream sync PR.
[downstream]
version = "0.6.4+gloss.2"
version = "0.6.4+gloss.3"
runtime_api_version = 1

[upstream]
Expand Down
2 changes: 1 addition & 1 deletion babeldoc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Gloss downstream release identity; see DOWNSTREAM.md.
__version__ = "0.6.4+gloss.2"
__version__ = "0.6.4+gloss.3"
8 changes: 8 additions & 0 deletions babeldoc/assets/assets.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import functools
import hashlib
import json
import logging
Expand Down Expand Up @@ -318,7 +319,14 @@ async def get_font_and_metadata_async(
return cache_file_path, font_metadata[font_file_name]


@functools.cache
def get_font_and_metadata(font_file_name: str):
"""Return one verified font resource without repeating disk/network lookup.

A translation can request the same fallback font from many paragraphs.
The returned path and embedded metadata are immutable runtime assets, so a
process-local cache avoids re-hashing the same font for every lookup.
"""
return run_coro(get_font_and_metadata_async(font_file_name))


Expand Down
2 changes: 1 addition & 1 deletion babeldoc/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pathlib import Path

# Gloss downstream release identity; see DOWNSTREAM.md.
__version__ = "0.6.4+gloss.2"
__version__ = "0.6.4+gloss.3"

CACHE_FOLDER = Path.home() / ".cache" / "babeldoc"

Expand Down
Loading