diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index c8089846..e67b9009 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -49,7 +49,32 @@ jobs: with: ref: ${{ github.event.release.tag_name || inputs.tag }} + # Chocolatey accepts a version exactly once, so pushing 2.7.2 again is an + # error. That is correct on the `release: published` path -- a second + # publish of the same version is a real problem -- and wrong on the + # `workflow_dispatch` path, whose only possible use is a tag that is + # already published: recovering after the snap job failed, or exercising + # this workflow without cutting a release. Both would report a Chocolatey + # failure that means nothing. + # + # So it is asked rather than assumed, and asked of the feed rather than + # inferred from the text of a push error. A check that cannot answer -- + # network, rate limit -- deliberately falls through to the push, which is + # the real gate; the point is to drop a known non-event, not to make + # publishing conditional on a lookup succeeding. + - name: Is this version already on Chocolatey? + id: already + shell: bash + run: | + VERSION="${TAG#v}" + feed="https://community.chocolatey.org/api/v2/Packages()?\$filter=Id%20eq%20'markpad-app'%20and%20Version%20eq%20'${VERSION}'" + if curl -fsS --max-time 30 "$feed" | grep -q "${VERSION}"; then + echo "::notice::markpad-app ${VERSION} is already on Chocolatey. Nothing to push." + echo "published=true" >> "$GITHUB_OUTPUT" + fi + - name: Take the executable from the release + if: steps.already.outputs.published != 'true' shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -69,6 +94,7 @@ jobs: cp LICENSE packaging/choco/tools/LICENSE.txt - name: Record what the package contains + if: steps.already.outputs.published != 'true' shell: bash run: | set -euo pipefail @@ -85,6 +111,7 @@ jobs: cat packaging/choco/tools/VERIFICATION.txt - name: Pack and publish + if: steps.already.outputs.published != 'true' shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 4100eb9f..868ef53c 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,9 @@ Every file below is on the [latest release](https://github.com/sftwrdotdev/Markp The two Windows files differ only in that one installs and one does not — `-setup.exe` puts Markpad in Program Files and the Start menu; the plain `.exe` runs from wherever you put it. -> After a direct `.dmg` (macOS), `*-setup.exe` (Windows NSIS) or `.AppImage` (Linux) install, Markpad self-updates from GitHub releases via the in-app *Check for Updates…* entry (macOS app menu, or Settings elsewhere). Snap, Chocolatey, `.deb` and `.rpm` users continue to update through their distribution channels. +> After a direct `.dmg` (macOS), `*-setup.exe` (Windows NSIS) or `.AppImage` (Linux) install, Markpad self-updates from GitHub releases via the in-app *Check for Updates…* entry (macOS app menu, or Settings elsewhere). Chocolatey and Snap keep Markpad current through those package managers instead. +> +> **`.deb` and `.rpm` are one-time installs.** `tauri-plugin-updater` cannot replace a package-managed install, and there is no apt or dnf repository to update from — upgrading means downloading a newer package from the releases page. *Check for Updates…* will report that a newer version exists but cannot install it ([#570](https://github.com/sftwrdotdev/Markpad/issues/570)). ## What Markpad renders diff --git a/RELEASING.md b/RELEASING.md index da9284e5..4fa10893 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -106,7 +106,7 @@ Mention this clearly in the release notes for the first auto-update-capable vers - **macOS** uses one universal binary (`darwin-aarch64` + `darwin-x86_64` share the same `.app.tar.gz` and signature). - **Windows** uses NSIS — the auto-updater downloads `*-setup.exe` (verified by `*-setup.exe.sig`) and runs it in `passive` install mode. The existing raw portable `.exe` distribution path is preserved alongside, so users who download the portable `.exe` directly continue to work; only the auto-updater path uses the NSIS installer. -- **Linux**: only `AppImage` users get auto-updates — `tauri-plugin-updater` doesn't support `.deb` or `.rpm`. `apt`/`rpm` users keep getting updates via their distro package manager (or by downloading a fresh package). +- **Linux**: only `AppImage` users get auto-updates — `tauri-plugin-updater` doesn't support `.deb` or `.rpm`, and there is no apt or dnf repository either. `.deb` and `.rpm` are therefore one-time installs: those users upgrade by downloading a newer package. *Check for Updates…* still offers them the update and then fails to install it, because `__TAURI_BUNDLE_TYPE` is not patched into the Linux binary and every Linux package therefore takes the AppImage install path — see #570. This is stated in the README as well; if a repository is ever published, both change together. - **Snap / Chocolatey**: independent distribution channels, published by `publish-packages.yml` after the release is published. Their update mechanisms are unaffected. The Chocolatey package wraps the release's own `Markpad__x64.exe` rather than a second build, which is what `packaging/choco/tools/VERIFICATION.txt` promises. ## Troubleshooting diff --git a/scripts/releaseWorkflow.test.ts b/scripts/releaseWorkflow.test.ts index ed3afb83..b746ea13 100644 --- a/scripts/releaseWorkflow.test.ts +++ b/scripts/releaseWorkflow.test.ts @@ -10,6 +10,7 @@ const testBuildWorkflow = readSource('.github/workflows/test_build.yml'); const releasing = readSource('RELEASING.md'); const snapcraft = readSource('snapcraft.yaml'); const stripAppImage = readSource('scripts/strip-appimage.sh'); +const readme = readSource('README.md'); const cargoToml = readSource('src-tauri/Cargo.toml'); const packageJson = JSON.parse(readSource('package.json')) as { scripts: Record; @@ -210,3 +211,50 @@ test('signing uses the environment variables the pinned CLI reads', () => { assert.doesNotMatch(workflow, /TAURI_PRIVATE_KEY/); assert.match(workflow, /TAURI_SIGNING_PRIVATE_KEY_PASSWORD/); }); + +test('the package managers we recommend are the ones we publish to', () => { + // Three places tell people a package manager can install Markpad: the README, + // the download table written into every release body, and the workflow that + // actually pushes. They drifted once already and nobody noticed for three + // months — the snap build was dead from v2.6.11 while both documents went on + // recommending `sudo snap install markpad` and the Snap Store served 2.6.11. + // + // That drift is about to be possible again in the other direction: #562 asks + // whether to retire the snap, and retiring it means deleting the job below. + // Doing that without touching the two documents leaves the same failure with + // the arrow reversed — a recommendation for a channel that no longer gets + // anything. This holds the three together so the deletion cannot be partial. + const releaseBody = sliceBetween(workflow, "echo '## Download'", 'RELEASE_BODY'); + for (const [name, install, push] of [ + ['Chocolatey', /choco install markpad-app/, /choco push/], + ['Snap', /snap install markpad/, /snapcraft push/], + ] as const) { + const publishes = push.test(publishWorkflow); + assert.equal( + install.test(readme), + publishes, + `README ${install.test(readme) ? 'recommends' : 'does not recommend'} ${name}, but the pipeline ` + + `${publishes ? 'does' : 'does not'} publish to it`, + ); + assert.equal( + install.test(releaseBody), + publishes, + `the release body ${install.test(releaseBody) ? 'recommends' : 'does not recommend'} ${name}, but the ` + + `pipeline ${publishes ? 'does' : 'does not'} publish to it`, + ); + } +}); + +test('the Chocolatey job skips a version that is already on the feed', () => { + // Chocolatey accepts a version once. On the `release: published` path a + // duplicate is a real problem; on the `workflow_dispatch` path it is the + // normal case, because the only possible use of that path is a tag that is + // already published — recovering after the snap job failed, or exercising + // this workflow without cutting a release. Without the skip, that escape + // hatch always reports a Chocolatey failure that means nothing. + // + // The guard must gate the push, not merely exist. + assert.match(publishWorkflow, /id: already/); + const pack = sliceFrom(publishWorkflow, '- name: Pack and publish'); + assert.match(pack, /if: steps\.already\.outputs\.published != 'true'/); +});