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
10 changes: 9 additions & 1 deletion .github/workflows/release-runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
push:
tags:
- "v*"
pull_request:
branches: ["main"]
workflow_dispatch:

permissions:
Expand All @@ -25,6 +27,7 @@ jobs:
- name: Checkout release source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
persist-credentials: false

- name: Setup uv with Python 3.12
Expand All @@ -49,6 +52,8 @@ jobs:
test "$actual_tag" = "$expected_tag"
test "$GITHUB_REF_TYPE" = "tag"
test "$GITHUB_REF_NAME" = "$expected_tag"
git show-ref --verify --quiet refs/remotes/origin/main
git merge-base --is-ancestor HEAD refs/remotes/origin/main
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "source_date_epoch=$(git show -s --format=%ct HEAD)" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -87,7 +92,7 @@ jobs:
publish:
name: Sign, attest, and publish
needs: macos-runtime
if: github.event_name == 'push'
if: github.event_name == 'push' && github.ref_type == 'tag'
runs-on: ubuntu-latest
permissions:
attestations: write
Expand All @@ -97,6 +102,7 @@ jobs:
- name: Checkout release source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
persist-credentials: false

- name: Setup uv with Python 3.12
Expand All @@ -117,6 +123,8 @@ jobs:
test "$actual_tag" = "$expected_tag"
test "$GITHUB_REF_TYPE" = "tag"
test "$GITHUB_REF_NAME" = "$expected_tag"
git show-ref --verify --quiet refs/remotes/origin/main
git merge-base --is-ancestor HEAD refs/remotes/origin/main
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=$expected_tag" >> "$GITHUB_OUTPUT"
echo "tag_version=${version/+/-}" >> "$GITHUB_OUTPUT"
Expand Down
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.3"
version = "0.6.4+gloss.4"
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.3"
__version__ = "0.6.4+gloss.4"
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.3"
__version__ = "0.6.4+gloss.4"

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

Expand Down
2 changes: 1 addition & 1 deletion babeldoc/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

logger = logging.getLogger(__name__)
# Gloss downstream release identity; see DOWNSTREAM.md.
__version__ = "0.6.4+gloss.3"
__version__ = "0.6.4+gloss.4"


def create_parser():
Expand Down
50 changes: 50 additions & 0 deletions docs/release-notes/v0.6.4-gloss.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# BabelDOC 0.6.4+gloss.4

This downstream release supersedes the unpublished `0.6.4+gloss.3` build and
completes the managed runtime path used by the Gloss macOS application.

## Performance and observability

- Repeated fallback-font resource lookups are cached for the lifetime of each
isolated PDF worker.
- Eligible single-part PDFs can reuse a private, bounded layout-IR session
cache. The cache key is derived inside BabelDOC from stable input bytes,
runtime version, page count, and parser-affecting options; clients cannot
choose cache paths or keys.
- Progress and terminal results expose monotonic phase timings for launch,
parse, translation, typesetting, save, and final validation, together with a
stable layout-cache status.

## Distribution

- Release automation builds reproducible wheel, sdist, and exact-source
archives.
- Native arm64 and x86_64 macOS archives contain a self-contained
`gloss-babeldoc` runtime and are smoke-tested after extraction.
- The runtime manifest lists verified artifact digests and the executable path
for each architecture, and requires Gloss 0.8.0 or newer. Archives contain
only regular files and directories, including a real mode-0755 executable,
LICENSE, NOTICE, a runtime README, and its machine-readable version record.
A detached Ed25519 signature is mandatory, and the workflow fails closed if
the configured key is absent or does not match the pinned release public key.
- Checksums, AGPL source, notices, machine-readable provenance, and GitHub
artifact attestations accompany the release.

## Release reliability

- The macOS Intel runtime uses the newest supported ONNX Runtime line that
publishes both x86_64 and arm64 wheels.
- Internal Python framework directory aliases are safely materialized in the
runtime archive; dangling, cyclic, out-of-bundle, and special-file entries
remain rejected.
- Both native runtime builds and their extracted smoke tests now run on pull
requests, before a release tag can be created.
- Release tags must identify the matching project version and a commit contained
in the repository's `main` history.

## Security

- The XML control-character range is expressed unambiguously.
- Reviewed CodeQL annotations document the MD5 and SHA-2 transforms mandated
by ISO 32000 encrypted-PDF compatibility without disabling those queries for
other application code.
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Gloss downstream modification: release identity and runtime handshake, 2026-07-21.
[project]
name = "BabelDOC"
version = "0.6.4+gloss.3"
version = "0.6.4+gloss.4"
description = "Gloss-maintained downstream of the BabelDOC document translator"
license = "AGPL-3.0"
readme = "README.md"
Expand All @@ -25,8 +25,7 @@ dependencies = [
"huggingface-hub>=0.27.0",
"numpy>=2.0.2",
"onnx>=1.21.0",
"onnxruntime>=1.16.1,<1.24; python_version < '3.11'",
"onnxruntime>=1.16.1; python_version >= '3.11'",
"onnxruntime>=1.16.1,<1.24",
"openai>=1.59.3",
"orjson>=3.10.14",
"charset-normalizer >= 2.0.0",
Expand Down Expand Up @@ -171,7 +170,7 @@ pythonpath = [".", "src"]
testpaths = ["tests"]

[bumpver]
current_version = "0.6.4+gloss.3"
current_version = "0.6.4+gloss.4"
version_pattern = "MAJOR.MINOR.PATCH+gloss.NUM"

[bumpver.file_patterns]
Expand Down
29 changes: 26 additions & 3 deletions scripts/release/archive_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@

def create_archive(source: Path, output: Path, *, epoch: int) -> None:
source = source.resolve(strict=True)
try:
output.resolve(strict=False).relative_to(source)
except ValueError:
pass
else:
raise ValueError("runtime archive output must be outside the source bundle")
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("wb") as raw_output:
with gzip.GzipFile(
Expand All @@ -29,6 +35,7 @@ def create_archive(source: Path, output: Path, *, epoch: int) -> None:
source.name,
source_root=source,
epoch=epoch,
active_directories=frozenset(),
)


Expand All @@ -39,16 +46,31 @@ def _add_path(
*,
source_root: Path,
epoch: int,
active_directories: frozenset[tuple[int, int]],
) -> None:
if path.is_symlink():
resolved_path = path.resolve(strict=True)
try:
resolved_path = path.resolve(strict=True)
except (OSError, RuntimeError) as exc:
raise ValueError(f"runtime symlink cannot be resolved: {path}") from exc
try:
resolved_path.relative_to(source_root)
except ValueError as exc:
raise ValueError(f"runtime symlink escapes the bundle: {path}") from exc
if not resolved_path.is_file():
raise ValueError(f"runtime symlink must resolve to a regular file: {path}")
if not resolved_path.is_file() and not resolved_path.is_dir():
raise ValueError(
f"runtime symlink must resolve to a regular file or directory: {path}"
)
path = resolved_path
if not path.is_file() and not path.is_dir():
raise ValueError(f"runtime entry must be a regular file or directory: {path}")
next_active_directories = active_directories
if path.is_dir():
directory_stat = path.stat()
directory_identity = (directory_stat.st_dev, directory_stat.st_ino)
if directory_identity in active_directories:
raise ValueError(f"runtime symlink creates a directory cycle: {path}")
next_active_directories = active_directories | {directory_identity}
info = archive.gettarinfo(os.fspath(path), arcname=archive_name)
if info.issym() or info.islnk():
raise ValueError(f"runtime archive cannot contain links: {archive_name}")
Expand All @@ -70,6 +92,7 @@ def _add_path(
f"{archive_name}/{child.name}",
source_root=source_root,
epoch=epoch,
active_directories=next_active_directories,
)


Expand Down
89 changes: 87 additions & 2 deletions tests/test_release_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import base64
import hashlib
import json
import os
import tarfile
from pathlib import Path

Expand All @@ -17,8 +18,8 @@
from scripts.release.sign_manifest import sign_manifest
from scripts.release.sign_manifest import verify_manifest

VERSION = "0.6.4+gloss.3"
TAG_VERSION = "0.6.4-gloss.3"
VERSION = "0.6.4+gloss.4"
TAG_VERSION = "0.6.4-gloss.4"
SOURCE_DATE_EPOCH = 1_750_000_000


Expand Down Expand Up @@ -125,6 +126,90 @@ def test_runtime_archive_rejects_symlinks_outside_bundle(tmp_path: Path) -> None
)


def test_runtime_archive_materializes_directory_symlinks_inside_bundle(
tmp_path: Path,
) -> None:
bundle = tmp_path / "gloss-babeldoc-runtime"
resources = bundle / "_internal" / "Python.framework" / "Versions" / "3.12"
resources.mkdir(parents=True)
(resources / "Info.plist").write_text("framework metadata\n", encoding="utf-8")
framework = bundle / "_internal" / "Python.framework"
(framework / "Resources").symlink_to(
"Versions/3.12",
target_is_directory=True,
)

output = tmp_path / "runtime.tar.gz"
create_archive(bundle, output, epoch=SOURCE_DATE_EPOCH)

with tarfile.open(output, "r:gz") as archive:
alias = archive.getmember(
"gloss-babeldoc-runtime/_internal/Python.framework/Resources"
)
alias_file = archive.getmember(
"gloss-babeldoc-runtime/_internal/Python.framework/Resources/Info.plist"
)
assert alias.isdir()
assert alias_file.isfile()
assert archive.extractfile(alias_file).read() == b"framework metadata\n"
assert not any(
member.issym() or member.islnk() for member in archive.getmembers()
)


def test_runtime_archive_rejects_directory_symlink_cycles(tmp_path: Path) -> None:
bundle = tmp_path / "gloss-babeldoc-runtime"
nested = bundle / "nested"
nested.mkdir(parents=True)
(nested / "back").symlink_to(bundle, target_is_directory=True)

with pytest.raises(ValueError, match="directory cycle"):
create_archive(
bundle,
tmp_path / "runtime.tar.gz",
epoch=SOURCE_DATE_EPOCH,
)


def test_runtime_archive_rejects_unresolvable_symlinks(tmp_path: Path) -> None:
bundle = tmp_path / "gloss-babeldoc-runtime"
bundle.mkdir()
(bundle / "missing").symlink_to("not-present")

with pytest.raises(ValueError, match="cannot be resolved"):
create_archive(
bundle,
tmp_path / "runtime.tar.gz",
epoch=SOURCE_DATE_EPOCH,
)


def test_runtime_archive_rejects_special_entries(tmp_path: Path) -> None:
bundle = tmp_path / "gloss-babeldoc-runtime"
bundle.mkdir()
fifo = bundle / "runtime.fifo"
os.mkfifo(fifo)

with pytest.raises(ValueError, match="regular file or directory"):
create_archive(
bundle,
tmp_path / "runtime.tar.gz",
epoch=SOURCE_DATE_EPOCH,
)


def test_runtime_archive_rejects_output_inside_source(tmp_path: Path) -> None:
bundle = tmp_path / "gloss-babeldoc-runtime"
bundle.mkdir()

with pytest.raises(ValueError, match="outside the source bundle"):
create_archive(
bundle,
bundle / "runtime.tar.gz",
epoch=SOURCE_DATE_EPOCH,
)


def test_release_manifest_has_strict_gloss_asset_contract(tmp_path: Path) -> None:
for architecture in ("arm64", "x86_64"):
filename = f"gloss-babeldoc-{TAG_VERSION}-macos-{architecture}.tar.gz"
Expand Down
Loading
Loading