diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92cb0c4..afd15a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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, @@ -34,7 +39,7 @@ jobs: runs-on: windows-latest timeout-minutes: 120 permissions: - contents: write + contents: read steps: - uses: actions/checkout@v4 @@ -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 @@ -270,16 +265,8 @@ 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') @@ -287,93 +274,113 @@ jobs: 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 diff --git a/docs/architecture/packaging.md b/docs/architecture/packaging.md index 5b39f49..7e6ad1a 100644 --- a/docs/architecture/packaging.md +++ b/docs/architecture/packaging.md @@ -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