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
199 changes: 103 additions & 96 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ name: Release
# differently staged sidecars (see scripts/build_sidecar.py). Unlike ci.yml
# neither is a gate — nothing here runs on a PR.
#
# They attach their files to one draft release and deliberately carry no
# `body`. Both ran in parallel against the same tag, and softprops/action-gh-release
# updates the release it finds, so whichever finished second replaced the
# other's notes — a Windows-only or Linux-only description on a release
# carrying both. `release-notes` below is the single writer of that text, and
# it runs after both, so the description describes what is actually attached.
# Neither touches the release. Each uploads its installer as a workflow
# artifact, and `release` below downloads both and creates the draft -- files
# and description -- in one `gh release create`, the only write to the release
# in the whole workflow.
#
# Three jobs used to write it, each through softprops/action-gh-release, which
# finds its draft by tag. A draft cannot be fetched by tag, and on v1.3.1 the
# third lookup missed: the action created a second draft holding the
# description, then "removed the duplicate" -- the new one -- and left the
# release with both installers and an empty body. Before that, the two build
# jobs replaced each other's notes. One writer has neither failure.
#
# macOS is absent on purpose. A `dmg` target and a `~/Library/Application
# Support` data root are already wired up (tauri.macos.conf.json,
Expand All @@ -34,7 +39,7 @@ jobs:
runs-on: windows-latest
timeout-minutes: 120
permissions:
contents: write
contents: read

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -136,22 +141,12 @@ jobs:
if-no-files-found: error
retention-days: 14

# No `body` — see the header. `release-notes` writes the description once,
# after both platforms have attached their files. The action keeps the
# existing body when this input is absent, so this never clobbers it.
- name: Attach to a draft release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
draft: true
files: desktop/src-tauri/target/release/bundle/nsis/*-setup.exe

linux-bundles:
name: Build .deb
runs-on: ubuntu-latest
timeout-minutes: 120
permissions:
contents: write
contents: read

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -270,110 +265,122 @@ jobs:
if-no-files-found: error
retention-days: 14

# No `body` — see the Windows job and the header.
- name: Attach to a draft release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
draft: true
files: desktop/src-tauri/target/release/bundle/deb/*.deb

release-notes:
name: Write the release description
release:
name: Create the draft release
runs-on: ubuntu-latest
needs: [windows-installer, linux-bundles]
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write

steps:
# The only writer of the release body. It runs after both builds, so
# every file the notes mention is already attached, and one description
# covers both platforms instead of the last job to finish deciding which
# platform the release appears to be for.
#
# `generate_release_notes` makes GitHub append its own commit/PR summary
# under this text, which is what replaced the "see the commit log" line
# each job used to carry.
- name: Publish the description
uses: softprops/action-gh-release@v2
- uses: actions/download-artifact@v4
with:
draft: true
generate_release_notes: true
body: |
PDFusion translates PDFs into Vietnamese (or another language)
while keeping the original layout, and can answer questions about
the document you have open.
pattern: pdfusion-*
path: installers
merge-multiple: true

- name: Write the description
run: |
cat > "$RUNNER_TEMP/notes.md" <<'NOTES'
PDFusion translates PDFs into Vietnamese (or another language)
while keeping the original layout, and can answer questions about
the document you have open.

## Downloads

## Downloads
| Platform | File | Notes |
|---|---|---|
| Windows 10/11 (x64) | `*-setup.exe` | Per-user install, no admin rights |
| Debian / Ubuntu (x64) | `*.deb` | Needs `libwebkit2gtk-4.1-0` and `libgtk-3-0` |

| Platform | File | Notes |
|---|---|---|
| Windows 10/11 (x64) | `*-setup.exe` | Per-user install, no admin rights |
| Debian / Ubuntu (x64) | `*.deb` | Needs `libwebkit2gtk-4.1-0` and `libgtk-3-0` |
### Windows

### Windows
Installs under `%LOCALAPPDATA%\Programs\PDFusion` — no UAC prompt,
and nothing is written to `Program Files`. The installer adds
PDFusion to the "Open with" list for `.pdf`; it never makes itself
your default PDF reader.

Installs under `%LOCALAPPDATA%\Programs\PDFusion` — no UAC prompt,
and nothing is written to `Program Files`. The installer adds
PDFusion to the "Open with" list for `.pdf`; it never makes itself
your default PDF reader.
**The installer is unsigned**, so SmartScreen warns the first time:
*More info* → *Run anyway*. A signing certificate is on the roadmap
and is not configured yet.

**The installer is unsigned**, so SmartScreen warns the first time:
*More info* → *Run anyway*. A signing certificate is on the roadmap
and is not configured yet.
### Linux

### Linux
The `.deb` takes its webview from the system. On Debian/Ubuntu:

The `.deb` takes its webview from the system. On Debian/Ubuntu:
```
sudo apt install ./PDFusion_*_amd64.deb
```

```
sudo apt install ./PDFusion_*_amd64.deb
```
It is unsigned. webkit2gtk **4.1** is required — 4.0 is the Tauri 1
series and will not run this build.

It is unsigned. webkit2gtk **4.1** is required — 4.0 is the Tauri 1
series and will not run this build.
**There is no AppImage in this release.** The translation engine
ships as a ~1 GB embedded Python runtime, and the AppImage
bundler insists on re-resolving the shared-library dependencies of
every binary inside it — including vendored libraries that only
resolve through their own `RPATH` at runtime. If you are not on a
Debian derivative, build from source for now.

**There is no AppImage in this release.** The translation engine
ships as a ~1 GB embedded Python runtime, and the AppImage
bundler insists on re-resolving the shared-library dependencies of
every binary inside it — including vendored libraries that only
resolve through their own `RPATH` at runtime. If you are not on a
Debian derivative, build from source for now.
## First run

## First run
The translation engine ships **inside** the installer (~290 MB of
layout models, fonts and the offline English→Vietnamese pack), so
the first translation works with no network and no API key.

The translation engine ships **inside** the installer (~290 MB of
layout models, fonts and the offline English→Vietnamese pack), so
the first translation works with no network and no API key.
Chat is the exception: its embedding model (~120 MB) downloads on
first use, into `~/.cache/huggingface`.

Chat is the exception: its embedding model (~120 MB) downloads on
first use, into `~/.cache/huggingface`.
Translating with OpenAI, Gemini or Anthropic needs your own API key,
entered in Settings. Each service can also be pointed at a
compatible endpoint, such as a proxy.

Translating with OpenAI, Gemini or Anthropic needs your own API key,
entered in Settings. Each service can also be pointed at a
compatible endpoint, such as a proxy.
## Where your data lives

## Where your data lives
| | Windows | Linux |
|---|---|---|
| Settings, caches, chat index | `%LOCALAPPDATA%\PDFusion` | `~/.local/share/PDFusion` |
| Logs | `%LOCALAPPDATA%\PDFusion\logs` | `~/.local/share/PDFusion/logs` |
| API keys protected by | DPAPI, scoped to your Windows account | Secret Service (gnome-keyring, KWallet) |

| | Windows | Linux |
|---|---|---|
| Settings, caches, chat index | `%LOCALAPPDATA%\PDFusion` | `~/.local/share/PDFusion` |
| Logs | `%LOCALAPPDATA%\PDFusion\logs` | `~/.local/share/PDFusion/logs` |
| API keys protected by | DPAPI, scoped to your Windows account | Secret Service (gnome-keyring, KWallet) |
On Linux with no keyring running — a headless box, or a session
with no D-Bus — keys fall back to an obfuscated value in
`config.toml` that anyone who can read the file can recover. The
app says so in `app.log` rather than refusing to start.

On Linux with no keyring running — a headless box, or a session
with no D-Bus — keys fall back to an obfuscated value in
`config.toml` that anyone who can read the file can recover. The
app says so in `app.log` rather than refusing to start.
Translated PDFs are temporary until you **Save** them. Nothing else
the pipeline writes is meant to be kept.

Translated PDFs are temporary until you **Save** them. Nothing else
the pipeline writes is meant to be kept.
## Not in this release

## Not in this release
A Linux AppImage — see the Linux section above.

A Linux AppImage — see the Linux section above.
macOS. The build is wired up, but an unsigned, un-notarized `.dmg`
is refused by Gatekeeper with no useful way past it, so shipping one
would be worse than shipping none.
NOTES

macOS. The build is wired up, but an unsigned, un-notarized `.dmg`
is refused by Gatekeeper with no useful way past it, so shipping one
would be worse than shipping none.
# Refuses rather than creating a second draft. GitHub allows any number
# of drafts per tag, so a re-run would otherwise leave two -- which is the
# bug this job exists to remove. Delete the stale draft and re-run.
#
# `--generate-notes` puts GitHub's commit/PR summary under the
# description; `--verify-tag` stops it inventing a tag if the push
# somehow went missing.
- name: Create the draft release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
existing=$(gh api "repos/$GH_REPO/releases" --paginate \
--jq '.[] | select(.tag_name == env.TAG) | .html_url')
if [ -n "$existing" ]; then
echo "::error::A release for $TAG already exists ($existing). Delete it and re-run this job."
exit 1
fi
ls -l installers
gh release create "$TAG" installers/* \
--draft --verify-tag --title "$TAG" \
--notes-file "$RUNNER_TEMP/notes.md" --generate-notes
13 changes: 10 additions & 3 deletions docs/architecture/packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,16 @@ sentence-transformers.

Releases are built by `.github/workflows/release.yml` on a `v*` tag, in two
jobs — `windows-installer` and `linux-bundles` — each staging the offline
assets, running `pnpm tauri build`, and attaching its artifacts to the same
**draft** release. Two jobs rather than a matrix because only one of them has a
signing step. Signing is opt-in and Windows-only — set the
assets, running `pnpm tauri build`, and uploading its installer as a workflow
artifact. A third, `release`, downloads both and creates the **draft** release
in a single `gh release create`: files, description and GitHub's generated
notes at once. Nothing else writes to the release. When the jobs each attached
their own files through `softprops/action-gh-release`, its find-the-draft-by-tag
lookup missed on v1.3.1, made a second draft for the description and then
deleted that one as the duplicate, so the release went out with an empty body.
The job refuses to run if a release for the tag already exists, rather than
adding a second draft; delete the stale one and re-run it. Two build jobs
rather than a matrix because only one of them has a signing step. Signing is opt-in and Windows-only — set the
`WINDOWS_SIGN_COMMAND` secret (Azure Trusted Signing) or
`bundle.windows.certificateThumbprint` (an OV cert in the runner's store); with
neither, the job warns and ships unsigned, which is also what the Linux job
Expand Down
Loading