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
27 changes: 27 additions & 0 deletions .github/workflows/publish-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<d:Version>${VERSION}</d: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 }}
Expand All @@ -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
Expand All @@ -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 }}
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<version>_x64.exe` rather than a second build, which is what `packaging/choco/tools/VERIFICATION.txt` promises.

## Troubleshooting
Expand Down
48 changes: 48 additions & 0 deletions scripts/releaseWorkflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
Expand Down Expand Up @@ -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'/);
});