2.7.5: the first signed macOS release, and the snap publishes on its own again - #719
Merged
Conversation
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.
Collaborator
|
perfect, will cut and test this today once i get access to a mac again |
Collaborator
|
signature test on mac worked perfectly, thanks for the walkthrough. publishing now and the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The bump, and the notes for it. 25 commits since
v2.7.4went out on 2026-08-13.The diff is six files: the three the version lives in —
package.json,src-tauri/Cargo.toml, and theMarkpadentry insrc-tauri/Cargo.lock, written bynpm run release 2.7.5, which is also what checks the three agree — plus the syntaxreference in both languages and the two warnings on the release page itself, both
brought up to what this release actually does.
masteris green: 984npm test, 398 vitest,svelte-checkclean 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 firstrelease 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.mdcarries the wording: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
.dmgstillwarns 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
.dmgand runcodesign -d -r-against the.appinside it: itmust print
certificate leaf = H"…".code object is not signed at allmeansshipping 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 pushwas removed in snapcraft 8, and the job installs snapcraft from thestore on every run — so the publish step broke with no commit here. v2.7.4 packed
markpad_2.7.4_amd64.snapgreen 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 Packageswas re-dispatched at
v2.7.4and succeeded;api.snapcraft.ionow answersstable / 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 screenshotswith 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 itsextension. Set it to
${filename}.assets, and images pasted or dropped intonotes/trip.mdare written tonotes/trip.assets— a folder that belongs to onedocument.
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 carrydirectory semantics that this one does not. What expands here is a single path
component —
trip.assetsstill goes throughsafe_path_componentin Rust, whichrefuses separators,
.,..and absolute paths, and keeps the folder inside thedocument's own directory. A nested template such as
./images/${filename}/wouldneed 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.
keywordfor 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_wordalso stops rendering as emphasis, which was the guess in the other direction.$delimiters and control sequences take the markup colour, so\fracno longer arrives identical to theait applies to, and the body is italic the way TeX typesets variables. It also stops the grammar readingx_i + y_jas an emphasis run and italicising the middle of the formula.[**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.Enter continues a block quote (#705)
Writing a quote meant typing
>on every line: Enter continued lists and task itemsand left
> quotedalone. 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 andleaves 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 for1., 4 for10.), and Tab moved a line to the one positionwhere its number is read — the first item of a new sub-list — so the
2.that Enterhad 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
prefers-reduced-motionlike 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.Mod+Shift+P, working in reading and edit mode both, printed beside the menu item. Not theMod+Shift+Ethe request suggested; Inline Code holds that.Copying from the preview keeps its formatting (#680)
⌘C in the preview writes
text/htmlbeside 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 afile: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-changedevent was somebody else's write used to be guessed from the clock: eachof our own writes opened a 400ms window in which every event for that path was
discarded as ours. That was wrong in both directions.
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.
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_writewrites a tempfile 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+Lagree in every mode (#696) — two places answered "where isAuto-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
\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.##.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}.mdis the file every release links as "every syntaxMarkpad renders is in one document", and it documents the editing behaviour around each
construct, not only the spellings. Four things above never reached it:
Tabchanges 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.
#705, including that one keystroke
clears an empty quoted line at any depth.
${filename}(#716). It says the token expands to
a folder name rather than a path, so nobody tries
./images/${filename}/.(#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 onevery release page from whatever
masterholds when the workflow is dispatched. Bothwere 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 info → Run anyway, and the antivirus claim gone. Say the word and I will
drop it completely.
To cut it — @alecdotdev
Merge this, then:
~30 min for the matrix builds, plus ~2 min for
generate-update-feed. The draft releasethen 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.
It must print:
It has to be the
.appinside that downloaded.dmg. Running it against/Applications/Markpad.appreads your installed 2.7.4, which will saycode object is not signed at allno matter what this release did.If the release's own
.appsaysnot signed at all, the threeMACOS_secrets are notreaching the workflow. Don't publish that build — say so here and it is a five-minute
fix. (
gh release downloadworks on a draft; it is how this was verified before thechange was proposed.)
Then Publish
That is the gate. It activates auto-update for installed copies and starts
publish-packages.yml.After Publish: watch the
snapjobpublish-packages.ymlonly runs once the release is published, so this is apost-Publish check rather than part of the one above. It is the first release-triggered
snap run since the
uploadrename, and the previous two both died there — the store saton 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.mdhas the wording: