From cb1b5d1403229d2285c0ea4de3a68a86af6a1eb1 Mon Sep 17 00:00:00 2001 From: nammovuivui Date: Mon, 19 Oct 2026 23:36:43 +0700 Subject: [PATCH] fix(release): create the draft release in one step Three jobs wrote the release through softprops/action-gh-release, which finds its draft by tag. On v1.3.1 the description job's lookup missed: it created a second draft holding the description, then removed "the duplicate" (the new one), leaving the release with both installers and an empty body. The build jobs now only upload workflow artifacts (and drop to contents: read). A single `release` job downloads both and runs one `gh release create --draft --verify-tag` with the files, the description and GitHub's generated notes. It refuses to run if a release for the tag already exists, since GitHub allows any number of drafts per tag and a re-run would otherwise leave two. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 199 +++++++++++++++++---------------- docs/architecture/packaging.md | 13 ++- 2 files changed, 113 insertions(+), 99 deletions(-) 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