fix: make OCR portable and reliable across AMD64 and ARM64 (#519)

* fix: make OCR portable and reliable

* fix: harden OCR installation portability

* fix: pin OCR partials across downloads

* fix: make OCR execution reliably asynchronous

* fix: harden OCR portability and docs routes

* fix: preserve decoder and docs safeguards
This commit is contained in:
SnapOtter
2026-07-15 03:34:24 +08:00
committed by GitHub
parent 58121f205f
commit 991c981529
409 changed files with 67151 additions and 8076 deletions
+167
View File
@@ -0,0 +1,167 @@
# OCR runtime release trust runbook
This runbook is for SnapOtter maintainers who provision or rotate the Ed25519
identity used to sign accurate OCR runtime indexes. Run key-generation commands
on a clean, offline administration machine. Never generate or decode the private
key on a GitHub runner, paste it into an issue, or commit any generated file.
The release workflows intentionally accept only standard, padded, byte-for-byte
canonical base64. PEM bytes are significant: do not rewrap or edit them after
encoding.
## Generate an Ed25519 identity offline
Create a new encrypted working directory or removable volume, disconnect the
machine from all networks, and run:
```bash
umask 077
mkdir ocr-runtime-signing-2026-01
cd ocr-runtime-signing-2026-01
openssl genpkey -algorithm ED25519 -out ocr-runtime-index-private.pem
openssl pkey -in ocr-runtime-index-private.pem -pubout \
-out ocr-runtime-index-public.pem
openssl base64 -A -in ocr-runtime-index-private.pem \
-out OCR_RUNTIME_INDEX_SIGNING_KEY_B64.txt
openssl base64 -A -in ocr-runtime-index-public.pem \
-out OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64.txt
printf '%s\n' 'ocr-runtime-2026-01' > OCR_RUNTIME_INDEX_KEY_ID.txt
```
The key ID must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$` and must be new for
every rotation. `openssl base64 -A` emits one canonical base64 string without a
trailing newline. Verify both round trips and the key relationship before the
machine reconnects:
```bash
cmp ocr-runtime-index-private.pem \
<(openssl base64 -d -A -in OCR_RUNTIME_INDEX_SIGNING_KEY_B64.txt)
cmp ocr-runtime-index-public.pem \
<(openssl base64 -d -A -in OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64.txt)
openssl pkey -in ocr-runtime-index-private.pem -check -noout
cmp \
<(openssl pkey -in ocr-runtime-index-private.pem -pubout -outform DER) \
<(openssl pkey -pubin -in ocr-runtime-index-public.pem -outform DER)
```
## Provision GitHub
Confirm `gh auth status` shows the `snapotter-hq` account. Choose exactly one
scope. Repository settings are simplest; organization settings are appropriate
only when SnapOtter has explicitly been granted access to the org secret and
variables. Keep the secret and both variables in the same scope.
Repository-scoped provisioning commands (these commands write GitHub state; run
them only in an approved maintenance window):
```bash
gh secret set --repo snapotter-hq/SnapOtter OCR_RUNTIME_INDEX_SIGNING_KEY_B64 \
< OCR_RUNTIME_INDEX_SIGNING_KEY_B64.txt
gh variable set --repo snapotter-hq/SnapOtter OCR_RUNTIME_INDEX_KEY_ID \
--body "$(cat OCR_RUNTIME_INDEX_KEY_ID.txt)"
gh variable set --repo snapotter-hq/SnapOtter OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64 \
--body "$(cat OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64.txt)"
```
Organization-scoped alternative, restricted to this repository:
```bash
gh secret set --org snapotter-hq --repos SnapOtter \
OCR_RUNTIME_INDEX_SIGNING_KEY_B64 < OCR_RUNTIME_INDEX_SIGNING_KEY_B64.txt
gh variable set --org snapotter-hq --repos SnapOtter OCR_RUNTIME_INDEX_KEY_ID \
--body "$(cat OCR_RUNTIME_INDEX_KEY_ID.txt)"
gh variable set --org snapotter-hq --repos SnapOtter \
OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64 \
--body "$(cat OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64.txt)"
```
Do not add whitespace, quotes, or a newline to any stored value. GitHub cannot
read a secret back, so the signing step independently checks its canonical
encoding and proves that it derives the configured public key.
## offline backup
Keep two encrypted offline backup copies in separate controlled locations. Each
copy must contain the private PEM, its canonical base64 file, the public PEM,
its canonical base64 file, the key-ID file, creation date, responsible
maintainers, and SHA-256 checksums. Require two-person access, test restoration
on an offline machine, and record the test without recording key material.
After backup verification, destroy the temporary encrypted working volume.
Deleting individual files is not reliable erasure on SSDs. Never put the backup
in source control, CI artifacts, chat, or an unencrypted cloud drive.
## rotation
1. Stop or wait for all release workflows; never change trust material during a
release run.
2. Generate a new identity and a new key ID offline, and verify its backup.
3. Update the private secret and both public variables together in the selected
scope.
4. Run the preflight below, then create a new release. Its exact image digests
bake the new public identity, and its final two-target index is signed with
the matching private identity.
5. Confirm both native post-sign matrix jobs pass before public image manifests
move. Do not re-sign or overwrite an existing immutable release.
6. Retain the old private key offline while an immutable release may need an
authorized recovery rerun. Record a retirement date and destroy it under the
project's key-retention policy.
## recovery
If the active private key is lost but not suspected compromised, pause releases
and restore it from the offline backup. If no backup is recoverable, generate a
new identity for a new release; the old key cannot be reconstructed and an
existing immutable release must not be silently re-signed.
If compromise is suspected, disable release workflow access, preserve audit
logs, revoke the configured secret, generate a new identity offline, and rotate
the secret plus both variables before any new release. Do not republish objects
under an existing version. Document which image versions trust the affected key
and communicate the incident through the security process.
A public/private mismatch, non-canonical encoding, oversized signed index, or
image-baked key mismatch is a hard release failure. Fix configuration; never
bypass the gate.
## Runner and release preflight
The mandatory GPU runner must have every exact label in
`[self-hosted, linux, x64, snapotter-nvidia]`, a working NVIDIA Container
Toolkit, `nvidia-smi`, Docker access, and capacity for a 4 GiB constrained test.
Organization-shared runners are valid, but repository runner listings alone do
not prove that one is available.
Read-only GitHub checks:
```bash
gh auth status
gh variable get OCR_RUNTIME_INDEX_KEY_ID --repo snapotter-hq/SnapOtter
gh variable get OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64 \
--repo snapotter-hq/SnapOtter > /tmp/ocr-runtime-public.b64
test "$(openssl base64 -A -in <(openssl base64 -d -A \
-in /tmp/ocr-runtime-public.b64))" = "$(cat /tmp/ocr-runtime-public.b64)"
gh secret list --repo snapotter-hq/SnapOtter \
| grep -q '^OCR_RUNTIME_INDEX_SIGNING_KEY_B64[[:space:]]'
gh api repos/snapotter-hq/SnapOtter/actions/runners \
--jq '.runners[] | {name, status, labels: [.labels[].name]}'
```
If trust is organization-scoped, also audit `gh variable list --org
snapotter-hq` and `gh secret list --org snapotter-hq`, and verify their selected
repository access in GitHub settings. The GPU runner itself must pass:
```bash
nvidia-smi --query-gpu=name,driver_version --format=csv,noheader
docker info --format '{{json .Runtimes}}' | grep -q 'nvidia'
docker run --rm --gpus all nvidia/cuda:12.6.3-base-ubuntu24.04 nvidia-smi
```
Finally, parse both workflow files locally and run the focused release contract
test before starting semantic release:
```bash
ruby -e 'require "yaml"; YAML.parse_file(".github/workflows/release.yml"); YAML.parse_file(".github/workflows/ai-bundles.yml")'
pnpm exec vitest run --config vitest.config.ts \
tests/unit/infra/ocr-release-workflow.test.ts
```
File diff suppressed because it is too large Load Diff
+43 -3
View File
@@ -104,7 +104,18 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install system dependencies (HEIC + ExifTool + ImageMagick + exotic format tools)
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends libheif-examples libheif-plugin-x265 libheif-plugin-libde265 libimage-exiftool-perl libraw-bin imagemagick ghostscript libjxl-tools libopenjp2-tools
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends libheif-examples libheif-plugin-x265 libheif-plugin-libde265 libimage-exiftool-perl libraw-bin imagemagick ghostscript libjxl-tools libopenjp2-tools
if apt-cache show libmagickcore-6.q16-7-extra >/dev/null 2>&1; then
sudo apt-get install -y --no-install-recommends libmagickcore-6.q16-7-extra
elif apt-cache show libmagickcore-6.q16-6-extra >/dev/null 2>&1; then
sudo apt-get install -y --no-install-recommends libmagickcore-6.q16-6-extra
else
echo "No supported ImageMagick EXR coder package found" >&2
exit 1
fi
convert -list format | grep -Eq '^[[:space:]]*EXR([*[:space:]]|$)'
- uses: ./.github/actions/setup
- name: Pre-pull testcontainer images (retry transient Docker Hub errors)
@@ -133,8 +144,37 @@ jobs:
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install system dependencies (image formats + doc-engine qpdf/LibreOffice)
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends libheif-examples libheif-plugin-x265 libheif-plugin-libde265 libimage-exiftool-perl libraw-bin imagemagick ghostscript libjxl-tools libopenjp2-tools qpdf libreoffice-calc libreoffice-impress libreoffice-writer
- name: Install system dependencies (image formats + docs + Fast OCR)
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
libheif-examples libheif-plugin-x265 libheif-plugin-libde265 \
libimage-exiftool-perl libraw-bin imagemagick ghostscript \
libjxl-tools libopenjp2-tools \
qpdf libreoffice-calc libreoffice-impress libreoffice-writer \
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu \
tesseract-ocr-fra tesseract-ocr-spa \
tesseract-ocr-chi-sim tesseract-ocr-jpn
if apt-cache show libmagickcore-6.q16-7-extra >/dev/null 2>&1; then
sudo apt-get install -y --no-install-recommends libmagickcore-6.q16-7-extra
elif apt-cache show libmagickcore-6.q16-6-extra >/dev/null 2>&1; then
sudo apt-get install -y --no-install-recommends libmagickcore-6.q16-6-extra
else
echo "No supported ImageMagick EXR coder package found" >&2
exit 1
fi
convert -list format | grep -Eq '^[[:space:]]*EXR([*[:space:]]|$)'
sudo rm -f /usr/share/tesseract-ocr/5/tessdata/osd.traineddata
test ! -e /usr/share/tesseract-ocr/5/tessdata/osd.traineddata
expected=(chi_sim deu eng fra jpn spa)
mapfile -t actual < <(tesseract --list-langs 2>/dev/null | tail -n +2 | LC_ALL=C sort)
[[ "${actual[*]}" == "${expected[*]}" ]] || {
echo "::error::Fast OCR language payload changed: ${actual[*]}"
exit 1
}
- name: Install Pandoc 3.10 (sandboxed DOCX/EPUB)
run: |
File diff suppressed because it is too large Load Diff