Skip to content

2.7.5: the first signed macOS release, and the snap publishes on its own again - #719

Merged
alecdotdev merged 3 commits into
masterfrom
release/2.7.5
Aug 25, 2026
Merged

2.7.5: the first signed macOS release, and the snap publishes on its own again#719
alecdotdev merged 3 commits into
masterfrom
release/2.7.5

Conversation

@PathGao

@PathGao PathGao commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

The bump, and the notes for it. 25 commits since v2.7.4 went out on 2026-08-13.

The diff is six files: the three the version lives in — package.json,
src-tauri/Cargo.toml, and the Markpad entry in src-tauri/Cargo.lock, written by
npm run release 2.7.5, which is also what checks the three agree — plus the syntax
reference in both languages and the two warnings on the release page itself, both
brought up to what this release actually does.

master is green: 984 npm test, 398 vitest, svelte-check clean over 816 files.

Patch rather than minor, the way 2.7.4 was — this project ships features in
patch releases routinely, and 2.7.4 itself carried eight of them. If you would
rather mark the macOS identity change below with a version number, 2.8.0 is the
alternative and nothing in the release depends on which you pick.


Before you cut

This is the first signed macOS release

#707 gives macOS builds a
code-signing identity that stays the same from one release to the next. macOS binds
a persisted file-access grant — the Full Disk Access checkbox included — to the app's
designated requirement. An unsigned bundle has no certificate to pin, so that
requirement is a content hash that changes with every build, and every update has
looked like a different app to the OS. That is
#209: folder access re-granted
after each release, for as long as Markpad has shipped.

All three secrets are set (MACOS_CERTIFICATE, MACOS_CERTIFICATE_PASSWORD,
MACOS_SIGNING_IDENTITY, created after v2.7.4 was built), so this is the first
release that actually gets signed.

Which costs the grant one last time. Swapping the ad-hoc hash for a certificate
is itself an identity change, so to macOS the first signed release is a different
app. RELEASING.md carries the wording:

macOS will ask for folder access once more after this update. This is the last
time
— from this release on, the permission carries across updates.

It reads as a regression unless the notes say it is the end of them. No other
platform is affected.

It is not notarization. The app stays un-notarized, so a downloaded .dmg still
warns on first launch, exactly as today. Nothing about Gatekeeper changes.

Check the signature took before you publish. The certificate import step exits 0
when the secrets are absent, by design, so an unsigned macOS build is green and
silent. Mount the .dmg and run codesign -d -r- against the .app inside it: it
must print certificate leaf = H"…". code object is not signed at all means
shipping it costs every macOS user their folder grants for nothing. This is now part
of the runbook's draft-release asset check.

The snap channel publishes on its own again

snapcraft push was removed in snapcraft 8, and the job installs snapcraft from the
store on every run — so the publish step broke with no commit here. v2.7.4 packed
markpad_2.7.4_amd64.snap green and then exited 64 on the upload:
The 'push' command was renamed to 'upload'.
#715 switched it to
snapcraft upload.

That has already been exercised against a real tag. With #715 in, Publish Packages
was re-dispatched at v2.7.4 and succeeded; api.snapcraft.io now answers
stable / amd64 / 2.7.4 / revision 16, its first move since revision 15 —
2.6.11, 2026-06-03.

So 2.7.5 is not the release that rescues the channel; the re-run did that. What it is
is the first release where clicking Publish reaches the Snap Store by itself,
with no dispatch by hand. Watch that job finish rather than assuming it passed — the
last two release-triggered runs both failed on this half.

Also in #715, on the decision it unblocks: the snap stays. The dashboard reports
~170 weekly active devices and the figure rose through the three months the channel
was dead, so those are new installs landing on 2.6.11 rather than people who have not
uninstalled.


Features

${filename} in the image directory (#716)

The Image Directory setting was taken literally, so every document in a folder shared
one img/, and a directory of notes accumulated a single flat pile of screenshots
with no way to tell which document any of them belonged to. Asked for in
#714.

${filename} in that setting now expands to the document's name without its
extension. Set it to ${filename}.assets, and images pasted or dropped into
notes/trip.md are written to notes/trip.assets — a folder that belongs to one
document.

The token is spelled the way Typora spells it (and SoloMD), so a user arriving
from either types what already worked there. It is deliberately not VS Code's
spelling: VS Code calls the equivalent ${documentBaseName}, and its tokens carry
directory semantics that this one does not. What expands here is a single path
componenttrip.assets still goes through safe_path_component in Rust, which
refuses separators, ., .. and absolute paths, and keeps the folder inside the
document's own directory. A nested template such as ./images/${filename}/ would
need that constraint relaxed and is not part of this.

All three call sites that read the setting — paste, drop, and the path completion
that lists the image folder — go through one expansion, so the completions come out
of the folder that will actually be written to.

Not changed: renaming a document still leaves its assets folder behind under the
old name (Typora moves it and rewrites the links). That is its own change.

Markdown is coloured in the editor

The largest group in this release, and the most visible on opening the app.

  • The built-in themes colour Markdown (#684, the ask in #676) — both themes shipped no rules at all and inherited Monaco's defaults, which were written for source code: headings, list markers and table pipes all arrived as one blue, links and horizontal rules got no colour, bold and italic got a font style and nothing else. Twelve rules now, in the app's own six colours. Scoped to Markdown, so the Python or JSON in a fenced block keeps the palette tuned for code, and inline HTML recedes instead of shouting.
  • The colours come from the renderer, not a regex (#686) — Monaco's Markdown grammar has about a dozen token names, one keyword for a heading's # and its words alike, a task checkbox read as a link, and no idea that ==highlight==, ++insert++, $math$, wikilinks or footnotes exist. The editor now asks the same comrak parse the preview is built from. snake_case_word also stops rendering as emphasis, which was the guess in the other direction.
  • A formula reads as markup around operands (#687) — $ delimiters and control sequences take the markup colour, so \frac no longer arrives identical to the a it applies to, and the body is italic the way TeX typesets variables. It also stops the grammar reading x_i + y_j as an emphasis run and italicising the middle of the formula.
  • An emphasis keeps its colour inside a link or a heading (#688) — [**bold**](url) showed the two ** in the bold colour and the word between them in no colour at all, purely because the text sits one node deeper.
  • Imported VS Code themes work — their bold and italic rules reach the editor at all (#678), and their heading, list, quote, table, inline-code, link and image colours stop being masked by the semantic layer's defaults (#689). A theme's colour is now resolved into the app's rules before Monaco sees either, so a construct's markers and its content no longer come out in two different colours.

Enter continues a block quote (#705)

Writing a quote meant typing > on every line: Enter continued lists and task items
and left > quoted alone. Reported in
#700. An empty quote clears in
one keystroke at any depth, so > > does not need one Enter per level to escape.

Lists share the new rule for where the caret counts as being inside the block —
after the marker's characters rather than after the space behind them — which is a
behaviour change worth knowing about: with the caret between - and the text,
Enter used to drop out of the list and now continues it.

Tab on a list item nests it, and renumbers (#713)

Tab sent editor.action.indentLines, which moves the line by the editor's tabSize and
leaves the marker alone. Both halves are wrong in Markdown, and each is wrong on its
own: an item nests only when its indentation reaches the parent's content column
(2 for - , 3 for 1. , 4 for 10. ), and Tab moved a line to the one position
where its number is read — the first item of a new sub-list — so the 2. that Enter
had written, invisible while the item was flat, rendered as 2.. Reported in
#711.

One level change is now three edits: indent to the parent's content column, renumber
the moved line, and renumber the siblings it left behind in the parent list.
Shift+Tab is the same with the sign flipped, so an item leaving a sub-list takes the
number of the list it rejoins.

Three smaller ones

  • The caret's glide is a setting (#712, asked for in #710) — the caret's slide between positions was hard-coded on, the only motion in the app with no way to turn it off. Defaults to on, so an existing install is unchanged, and it answers prefers-reduced-motion like the animated jump does. Its own preference rather than a second reader of the jump setting: a jump happens a few times a session, the glide is drawn under every arrow key.
  • The editor and preview can trade sides (#693, the ask in #184) — a swap control next to Sync Scroll, shown only while a tab is actually split.
  • Export as PDF gets a shortcut (#679, asked for in #673) — Mod+Shift+P, working in reading and edit mode both, printed beside the menu item. Not the Mod+Shift+E the request suggested; Inline Code holds that.

Copying from the preview keeps its formatting (#680)

⌘C in the preview writes text/html beside the plain text, so a paste into Word,
Mail or Docs arrives with its headings, emphasis, lists, tables and links — the open
product question left by #549, asked for in
#674. The fragment is pruned
first: KaTeX's hidden MathML half would otherwise paste every formula twice, fold
chevrons are controls rather than content, and an image's asset:// source becomes a
file: URL, since that scheme resolves nowhere but inside Markpad.


Fixes

Not losing work

A save asks the file who wrote it (#698 and #699, for #692) — whether a
file-changed event was somebody else's write used to be guessed from the clock: each
of our own writes opened a 400ms window in which every event for that path was
discarded as ours. That was wrong in both directions.

  • Too early: another program writing inside the window had its event dropped with
    nothing re-queued, so the buffer held our text, the disk held theirs, the tab looked
    clean, and the next keystroke's auto-save put ours back over theirs. Silent loss of
    somebody else's edit — the exact thing the guard exists to prevent.
  • Too late: an event arriving after the window read as external, so the conflict bar
    sometimes asked the user about their own save. A question that is sometimes false
    teaches people to dismiss it, including the times it is true.

The file is compared against the text last known to be in it instead, which answers
the question exactly and needs no window, and the same comparison runs immediately
before every write — the check VS Code, Vim and Emacs each have. It is a refusal, not
a merge: the conflict bar goes up, and "keep mine" authorises exactly the next save.

Two ways that bar could not actually be answered are closed too. Closing a tab
answered it silently in the destructive direction, by handing a dirty tab to auto-save;
it now asks. And the answer itself was invisible — Reload replaces the buffer and
clears the undo stack with it, so the bar was asking for an irreversible decision
without showing what would be lost. There is a Compare button beside the two
choices now, a read-only diff of the file against the buffer, the way VS Code and
Sublime do it.

Live Mode survives a save (#697) — a watch armed on a file does not survive that
file being replaced, and saving replaces it: Markpad's own atomic_write writes a temp
file alongside and renames it over the top, as do VS Code, Vim, JetBrains and Emacs. On
Linux the first save of a watched file left the watch holding an unlinked inode, and
Live Mode reported nothing for the rest of the session, silently, with the error
dropped. The watch moves to the containing directory. CI is where that was
established — the test passes on macOS and failed on Linux with "the watch died with
the replaced inode".

Auto-Reload and Mod+L agree in every mode (#696) — two places answered "where is
Auto-Reload available?" and gave exactly complementary answers: the button was hidden
in edit mode, and the chord was owned by Monaco so it fired only in edit mode.
Pressing Ctrl+L in the editor flipped the state with nothing on screen to show it had.
Both now work wherever there is a file on disk, including the preview, and entering
split view stops silently turning Live Mode off.

Elsewhere

  • The word count counts CJK (#695, reported in #691) — the count filtered tokens by \w, which is [A-Za-z0-9_], and split on whitespace, which is not a word boundary in Chinese or Japanese. A document with no Latin characters read 0. Han, Hiragana and Katakana are now counted per character, which is what Word, Typora and Obsidian report; Hangul stays out, since Korean is spaced and per-syllable counting would inflate every Korean document.
  • A cold start no longer takes focus from the app you switched to (#704, from #702) — the main window is built hidden so no empty frame is on screen while the webview boots, and the reveal seconds later activated the app unconditionally, long after the user had gone back to something else. The first show is quiet now; detached tab windows and the close walk still come to the front, because both answer something the user just did. No tray icon, which is what the issue asked for — the reasoning is in the PR.
  • The outline shows the document it is looking at (#694) — a pinned outline could come up reporting no headings over a document full of them and stay that way until collapsed and expanded by hand, because it scanned the preview one step before the article was patched. Also, its collapse button stops landing on the outline's own first entry.
  • Inline code in a heading sits on the heading's baseline (#685, reported in #681) — it floated above it, and the smaller the element the further it floated, which also affected images and KaTeX in headings. Inline code in a heading now takes 85% of the heading's size the way GitHub does, instead of staying at the Code Font Size setting's absolute px inside a 24px ##.
  • The find widget's tooltips stop being clipped away (#677, reported in #675) — Monaco places them above the button and only flips below when they would leave the window, which never fired here, so they landed behind the title bar.

Project

The issue forms ask for less, and for the right things (#703) — measured against the
27 external issues filed since they were last written. Bug report 10 fields → 6, after
dropping the ones answered 0/9 and 1/9 times. Feature request 3 → 4: it carried no
environment at all, so all 17 requests arrived without one, including Windows-only asks
that cannot be triaged without knowing the platform. Question 3 → 2. Two fields added
on evidence from the replies, where triage had been reconstructing the information by
hand: the triggering Markdown as text rather than a screenshot of text, and "has
another app got this right?".

The syntax reference is up to date, in both languages

samples/markdown-syntax{,.zh-CN}.md is the file every release links as "every syntax
Markpad renders is in one document"
, and it documents the editing behaviour around each
construct, not only the spellings. Four things above never reached it:

  • Lists — it already claimed Tab changes the level, which only became true with
    #713. It now says what the level
    change does: the parent's content column, and both numbered lists renumbered.
  • Quotes — gets the paragraph Lists has had, for
    #705, including that one keystroke
    clears an empty quoted line at any depth.
  • Images — where a pasted or dropped image lands, and ${filename}
    (#716). It says the token expands to
    a folder name rather than a path, so nobody tries ./images/${filename}/.
  • Not-syntax — preview copy keeps its formatting
    (#680), and the split panes can trade
    sides (#693).

The download table's two warnings — @alecdotdev, one of them differs from what you asked

Both notes live in build.yml's Compose the download table step, so they print on
every release page from whatever master holds when the workflow is dispatched. Both
were about to be wrong on this release, which is why they are in this PR rather than a
later one.

macOS — replaced. The last paragraph told users the app grants file access per
prompt and that signing it themselves in Keychain Access is the way out, redone after
every update. #707 is exactly what that paragraph asks for, so it would have printed the
workaround on the first release that no longer needs it. It now says the grant survives
an update, and that only someone coming from a release older than 2.7.5 is asked once
more — worded to stay true for 2.7.6 rather than describing this release. The Gatekeeper
paragraph above it is untouched: signing is not notarization, and the first-launch
dialog is unaffected.

Windows — trimmed, not removed. You asked for the SmartScreen warning to go, on the
strength of the VirusTotal scan. The scan settles half of it: the "false positive
Trojan"
claim is stale, and that half is deleted — it is what #334 and #466 were.
SmartScreen itself is the other half, and it fires on an unsigned binary regardless of
what any scanner reports, which is the whole reason SignPath is worth applying for
(#562 §1). Deleting the note entirely would leave the release page silent about a dialog
every Windows user still meets on first install. So it is now one sentence: unrecognized
app, More infoRun anyway, and the antivirus claim gone. Say the word and I will
drop it completely.


To cut it — @alecdotdev

Merge this, then:

git pull
git tag v2.7.5
git push origin master v2.7.5
gh workflow run build.yml --ref master

~30 min for the matrix builds, plus ~2 min for generate-update-feed. The draft release
then has its assets.

Before you click Publish: check the macOS signature took

This is the first release that carries one, and a missing signature is green and
silent
— the build succeeds, every asset is there, and the only symptom is every macOS
user re-granting folder access after they update, exactly as they do today.

gh release download v2.7.5 --repo sftwrdotdev/Markpad --pattern '*.dmg' --dir ~/Downloads/mp275
hdiutil attach -nobrowse ~/Downloads/mp275/Markpad_2.7.5_universal.dmg
codesign -d -r- /Volumes/Markpad/Markpad.app
hdiutil detach /Volumes/Markpad

It must print:

designated => identifier "com.alecdotdev.markpad" and certificate leaf = H"…"

It has to be the .app inside that downloaded .dmg. Running it against
/Applications/Markpad.app reads your installed 2.7.4, which will say code object is not signed at all no matter what this release did.

If the release's own .app says not signed at all, the three MACOS_ secrets are not
reaching the workflow. Don't publish that build — say so here and it is a five-minute
fix. (gh release download works on a draft; it is how this was verified before the
change was proposed.)

Then Publish

That is the gate. It activates auto-update for installed copies and starts
publish-packages.yml.

After Publish: watch the snap job

publish-packages.yml only runs once the release is published, so this is a
post-Publish check rather than part of the one above. It is the first release-triggered
snap run since the upload rename, and the previous two both died there — the store sat
on 2.6.11 for three months without anyone noticing, because a stale snap and a current
one look identical from outside. If it goes red, the recovery path is the same dispatch
you used yesterday: Actions → Publish Packages → Run workflow → v2.7.5.

One note for the release description

macOS users are asked for folder access one more time, because the signature changes the
app's identity once. RELEASING.md has the wording:

macOS will ask for folder access once more after this update. This is the last
time
— from this release on, the permission carries across updates.

Four things shipped since 2.7.4 that the file which documents what Markpad can
do never heard about. It carries the editing behaviour around each construct,
not only the spellings, so each one belongs to a section that already exists.

- Lists: `Tab` moved a line by tabSize and left the marker alone. #713 makes it
  a level change -- the parent's content column, and both numbered lists
  renumbered -- which is what the file already claimed and now describes
  accurately.
- Quotes: `Enter` continues a block quote (#705), so the section gets the
  paragraph Lists has had. Including that one keystroke clears an empty quoted
  line at any depth, which is the way out.
- Images: where a pasted or dropped image lands, and `${filename}` in that
  setting (#716). It expands to a folder name, not a path, and the note says so
  -- `./images/${filename}/` is not a thing you can write here.
- Not-syntax: copying from the preview keeps its formatting (#680), and the
  split panes can trade sides (#693).

Tests: 984 pass.
…2.7.4

The table is composed in `build.yml` and printed on every release page, so both
notes ship with whatever master holds when the workflow is dispatched. Both are
about to be wrong.

macOS: the last paragraph told users the app grants file access per prompt and
that self-signing in Keychain Access is the way out, redone after every update.
#707 is what that paragraph asks for, so it would print the workaround on the
first release that no longer needs it. Replaced with what is now true, worded so
it stays true for 2.7.6: the grant survives an update, and only a user coming
from a release older than 2.7.5 is asked once more, because the signature
changes the identity the old grants belonged to. The Gatekeeper paragraph above
it is untouched -- signing is not notarization, and the first-launch dialog is
unaffected.

Windows: the "false positive Trojan" half is the stale one -- VirusTotal no
longer flags the portable `.exe`, which is what #334 and #466 were. The
SmartScreen half is not: it fires on an unsigned binary regardless of what any
scanner says, and stays true until an Authenticode certificate exists (#562).
Dropping the whole note would leave the release page silent about a dialog every
Windows user still meets. So the antivirus claim goes and the unrecognized-app
one stays, in one shorter sentence.

Tests: 984 pass.
@alecdotdev

Copy link
Copy Markdown
Collaborator

perfect, will cut and test this today once i get access to a mac again

@alecdotdev
alecdotdev merged commit 34943bf into master Aug 25, 2026
4 checks passed
@PathGao
PathGao deleted the release/2.7.5 branch August 25, 2026 21:23
@alecdotdev

Copy link
Copy Markdown
Collaborator

signature test on mac worked perfectly, thanks for the walkthrough. publishing now and the publish-packages workflow triggers automatically

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants