Version 3.6.1 · All versions

All pages

These are the docs as released with Solid Memo 3.6.1. Read the latest docs.

Releasing

How Solid Memo's versions are chosen, what a version promises, and what a release does. Versions are released by semantic-release, not by hand: after CI passes on a push to main, it reads the commits since the last version and, when one of them is releasable, releases the next version. Only a version is deployed (deployment.md): once CI passes on the release commit, the site runs it, and a push to main that releases nothing waits for the next version. So the site always runs a version the releases page lists, and CHANGELOG.md says what changed since the last one.

The scheme

The app follows Semantic Versioning 2.0.0: MAJOR.MINOR.PATCH, tagged vMAJOR.MINOR.PATCH (v1.0.0). The first version is 1.0.0, and it is stable: the app and its data formats can be relied on, and a change that breaks them bumps the major version. No 0.x version was released.

Each commit says what kind of change it is, in its Conventional Commits type (CONTRIBUTING.md), and the release takes the largest bump among the commits since the last version:

BumpWhenCommitsExample
Major (1.4.2 → 2.0.0)Anything breaking (below)! after the type, or a BREAKING CHANGE: footerA deck format 8 the app writes; a configuration variable renamed
Minor (1.4.2 → 1.5.0)A new featurefeatA new screen; a new deck in the library; a new configuration variable
Patch (1.4.2 → 1.4.3)Fixes, and changes nobody has to act onfix, perf, revert (or GitHub's Revert "…")A bug fixed; a translation corrected; a dependency the app ships updated
NoneNothing users seedocs, test, refactor, style, build, ci, choreA page of the docs; a test; a development tool updated

A push with only commits of the last kind releases nothing; their changes go out with the next version that does. The commits made before Conventional Commits were adopted almost never have a type, and none has one that releases, so they count for nothing.

The version is the root package.json's version, which the footer shows beside the commit a build was made from (Footer.tsx, configuration.md). The workspace packages are private, not published to npm, and stay at 0.0.0: they are parts of one app, not libraries with versions of their own (architecture.md). Before the first release wrote 1.0.0 into it, the root's version was 0.0.0 too, which names no version: 0.1.0 was never released, so no build shows it.

The public API

Semantic Versioning asks a project to say what its versions promise: its public API. Solid Memo's is what its users, other apps that read the same pods and the people who run their own copy rely on:

Everything else may change in any version: the workspace packages' exports, which nothing outside the repository imports (architecture.md), the screens' layout, wording and styles, and whatever these docs do not name. These docs as the site serves them, at /docs/ and below it, are not part of it either: a page may be renamed, split or merged in any version (deployment.md), as may robots.txt, sitemap.xml and the archive of a release's docs (deployment.md).

What counts as breaking

Solid Memo keeps its data in its users' pods, not on a server of its own, so the data is what has to stay readable: by this app's older and newer versions, by other apps that read the same pod, and by people who run their own copy. A change to the public API is breaking when one of them has to act or loses something:

A new prerequisite for building a copy is not breaking when the build names it and a setting builds without it: npm run build draws the docs' diagrams in Chromium, and DOCS_DIAGRAMS=skip builds without them, on any machine and in any CI (configuration.md). The commit that brings one in names it in its subject, so the release notes say what a copy now needs.

A new vocabulary term that no shape names is not breaking (vocab.md); neither is a new deck in the library, nor a new version of a deck. A published shape is never edited, so a term that a shape gains, even an optional one, is in the shape's next version: a new format version the app writes, and so breaking. A change that is breaking says so in its commit, with ! and a BREAKING CHANGE: footer that tells users what to do, and it is agreed in an issue first (CONTRIBUTING.md).

The release reads the footer only as it is written: BREAKING CHANGE: or BREAKING-CHANGE: at the start of a line, in capitals and with its colon, as commitlint and npm run check:breaking read it (release.config.mjs's parserOpts). A line of a body that begins "breaking change" in a sentence is not a footer.

How CI holds to it

The public API is written down in api/, one promise a line: the formats the app writes and reads, where the data lives in a pod, the hashes, what the app keeps in the browser, what a copy of one's own relies on, and, in api/frozen/, the published documents and the vocabulary's terms. Each file is written by a test in the package whose code it reads, so a change to the code that changes a promise fails that test until the file is written again (npm run test:unit -- <the test> -u), in the same commit.

npm run check:breaking -- --base <git ref> (checkBreaking.ts) compares these files with the ones at the change's base, where it left <git ref>. CI's library job runs it on every pull request, in the merge queue and on every push to main, so before any release (testing.md):

A line added is a promise added, and needs nothing. What no file can hold is left to review: a feature removed, or a reader that keeps its format but reads less of it.

What has versions of its own

The app's version is one of several. The data that other apps and pods rely on is versioned on its own, by its own rules, and none of them follows the app's number:

WhatIts versionRules
The vocabularyv1 in its IRI, and owl:versionInfo (1.20) in ns/vocab/v1.ttlTerms are only added (vocab.md)
The shapesns/shapes/<class>/v<N>.ttl, one file per format versionA published version is never edited (shapes.md)
Format versionssm:formatVersion on every subject the app writesmigrations.md
The library's decksdecks/<name>/v<N>.ttlA published version is never edited (deck-library.md)

A new format version the app writes is a breaking change of the app, and so a major version; a new vocabulary term or a new deck version is not. The commit's subject names what it adds, so the changelog does too: "write deck format 8", "add a term to vocabulary 1.20".

The changelog

CHANGELOG.md is written by the release: each version's entry goes under the file's header, newest first, with the feat commits under "Features", the fix commits under "Bug Fixes", and so on, each line a commit's subject with its scope and a link to the commit. A breaking change is listed first, under "BREAKING CHANGES", with its footer's text. The commits of the other types are left out. The site's footer links to it, on main (Footer.tsx).

So the changelog is as good as the commits' subjects. A subject says what changed for users, other apps or people who run their own copy, in plain words. Nobody edits an entry by hand: a mistake in one stays, and the next version's entry can say what it should have said. The header is release.config.mjs's changelogTitle, and the two must stay the same (releaseConfig.test.ts checks it). At the end of the file, a section written by hand sums up what the first version, 1.0.0, contains, which its generated entry lists only in part.

What a release does

release.yml runs when CI has passed on a push to main in this repository, as the deploy does, and runs semantic-release (release.config.mjs) on main's tip. When main has moved on since the commit CI passed on, it releases the tip if CI has passed on it too, and otherwise leaves it to the tip's own run, once CI passes there. When there is something to release, semantic-release:

  1. chooses the version from the commits since the last tag (The scheme), and writes its notes;
  2. writes the notes into CHANGELOG.md;
  3. sets the version in the root package.json and package-lock.json, and the version and today's date in publiccode.yml (softwareVersion, releaseDate) and CITATION.cff (version, date-released), with npm run release:version (releaseVersion.ts);
  4. renders the version's docs as they are served under the version, every diagram drawn in Chromium, into release-assets/docs-X.Y.Z.tar.gz, with npm run docs:archive (deployment.md). git ignores the archive, so the release commit leaves it out;
  5. commits the five files of steps 2 and 3 to main as chore(release): X.Y.Z, with the notes as the body, and pushes the commit with the deploy key (below);
  6. tags that commit vX.Y.Z and pushes the tag;
  7. makes a GitHub release of the tag, "Solid Memo X.Y.Z", with the notes and the archive of its docs, "Documentation X.Y.Z (HTML)": it makes the release as a draft, uploads the archive to it, and only then publishes it, so a release is never published without its docs, and the deploy refuses a draft. Then the workflow attests the archive as made by the release workflow on main (actions/attest), which the deploy checks before it serves it (deployment.md). Zenodo, connected to the repository, archives each GitHub release and gives it a DOI. The concept DOI, 10.5281/zenodo.23351087, always resolves to the latest release, and is the one in CITATION.cff and the README.

The release commit is not marked to skip CI. CI runs on it, and once it passes, the deploy builds that commit, the one the tag names, and puts the new version on the site, its footer included; it deploys no other commit (deployment.md). The release workflow runs again too, finds no commit since the tag, and releases nothing. CI's run on a release commit has a concurrency group of its own, so a newer push to main cannot replace it while it waits, and hold back the deploy (testing.md).

Releases go one at a time (concurrency group release), and a running one is never canceled. GitHub keeps only one run waiting in the group, so a newer one replaces a run already waiting; whichever goes ahead releases the tip, as above. A fork, or a copy without the deploy key, runs the workflow and releases nothing.

A release made by mistake is not taken back: a tag is never moved or deleted once pushed. A mistake is fixed in the next version.

2.0.0 is one. No commit in it breaks anything, but semantic-release then read a line of a commit's body that began "breaking change", in lowercase and with no colon, as a breaking footer, and its changelog entry lists the rest of that line under "BREAKING CHANGES". Nothing needs to be done to move from 1.2.0 to 2.0.0. The release now reads the footer only as written (what counts as breaking), and the version after 2.0.0 says so (#84).

The deploy key

The ruleset on main requires a pull request for every change, merged through the merge queue (testing.md), so semantic-release needs a way past it to push the release commit. The workflow's own token could be let past it too, but a push made with that token starts no workflow, so CI and the deploy would never run on the release commit, and since only a release commit is deployed, the site would never get a new version. The release pushes with a deploy key instead: an SSH key with write access to this repository alone, which GitHub lets a ruleset's bypass list name, and whose pushes start workflows as anyone's do. Only the release workflow can use it: its private key is a secret of the release environment, which allows only main. A repository secret can be read by any workflow that runs in the repository, one pushed on any other branch included, so anyone with write access could have pushed past the ruleset with it and tagged a version to be deployed. An environment's secret is read only by a job that names the environment, in a run on a branch it allows, and release.yml is the only workflow on main that names release. That holds only once no repository secret of the same name is left: a job whose environment lacks a secret reads the repository's.

The maintainer sets it up once, before the workflow first runs on main, so that the merge that adds it releases 1.0.0 straight away:

  1. Make a key pair with no passphrase, outside the repository:

    ssh-keygen -t ed25519 -N "" -C "solid-memo release" -f release-key
  2. Add release-key.pub as a deploy key of the repository, with write access (Settings › Deploy keys › Add deploy key, "Allow write access").

  3. Make the environment release (Settings › Environments › New environment), or open it if a run has made it already: GitHub makes one, open to every branch, the first time a run names it. Under "Deployment branches and tags", choose "Selected branches and tags" and add the branch main, and nothing else. Add the private key, release-key, as its environment secret SEMANTIC_RELEASE_KEY. Then delete both files, and any repository secret of that name (Settings › Secrets and variables › Actions).

  4. Add "Deploy keys" to the bypass list of the ruleset on main (Settings › Rules), so that the release commit needs no pull request.

  5. Add a ruleset for tags matching refs/tags/v* that restricts their creation, update and deletion (Settings › Rules › Rulesets › New ruleset › New tag ruleset: enforcement Active, target v*), with "Deploy keys" and the Repository admin role on its bypass list. The deploy deploys the commit a version tag names, so without it anyone with write access could tag a commit and have it deployed; the admin's bypass is for tagging by hand (below).

Both bypass lists name "Deploy keys", which lets every deploy key with write access past them, not only this one. So this key stays the repository's only deploy key with write access: any other is added without "Allow write access".

The job that holds the key also draws the docs' diagrams for the release's archive: it runs Chromium, through Playwright, and Mermaid on the repository's own docs, with the key configured for the push. That is accepted. Both are pinned development dependencies whose updates Renovate proposes and CI checks like any other change (vendor-code.md), and they read only what is committed on main, where every change was reviewed. The other way was to build the archive in a job of its own, without the key, and attach it with the run's token; it was not taken, because semantic-release would then publish the release before its archive existed, and the deploy would serve the version without its docs.

Without the secret, the workflow says so and releases nothing; once it is set, run the workflow by hand (below) to release what is waiting. To change the key, add the new one as a deploy key, replace the environment's secret, then delete the old deploy key.

When a release fails

The workflow's run says which step failed and why, and nothing opens an issue or comments on one. Fix the cause, then run the workflow by hand (Actions › Release › Run workflow, from main): it releases main's tip once CI has passed on it, and refuses before that. The next push to main that passes CI also tries again.

A tag is never moved or deleted, even one whose release went wrong: Zenodo and anyone who fetched it may already hold it.