krz/orgo

Lightning fast org-mode static site generator. fast go org-mode static-site-generator

Commit bbe31403db

bbe31403db53cf5311232c64e95ce0b98b8713dd

parent: cbd8cc8c4b

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-11 20:48 UTC

0.18: make the version number worth reading

Everything here is about what happens *after* the code is right, which until now
was undefined: there was a `license = "MIT"` with no LICENSE file, no changelog,
no statement of what a version promises, and no build anywhere but this laptop.

- **A compatibility promise**, in the README and in the guide. Config keys,
  template variables, CLI flags and URLs are the stable surface — URLs
  deliberately, because a generator that moves your pages breaks every link
  anyone has to you. The incremental cache, rendered HTML details and the Rust
  API are explicitly not, so that the rest can hold still. HTML changes because
  it tracks Emacs; that is the product, and it gets called out in the changelog
  each time.

- **CI on Linux and macOS**: build, test, clippy as an error, and the docs site
  built with `--strict`. Emacs is installed on both runners so the oracle suite
  runs for real. Deliberately not gated on `cargo fmt` — the source is formatted
  by hand and rustfmt disagrees with most of it.

- **A checked MSRV.** Set to 1.88. I first wrote 1.82, from the newest std API
  in this crate, and the dependency floors said otherwise: `plist` and `time`,
  by way of syntect, both declare 1.88. That is the whole argument for checking
  it in CI rather than reasoning about it.

- **Release binaries** for macOS (arm64, x86_64) and Linux (gnu, musl), built on
  tag into a *draft* release — a release that publishes itself before anyone has
  read it cannot be edited quietly. The tag is checked against Cargo.toml first.

- A LICENSE file, crates.io metadata, and a release profile that ships 5.0 MB
  instead of 6.5 MB. `cargo package` verifies clean.

`repository` is deliberately left commented out in Cargo.toml: a wrong URL on a
crates.io page is worse than none, and this repository has no remote yet.

Layout: unified · split

.github/workflows/ci.yml added +104
@@ -0,0 +1,104 @@
1name: CI
2
3# Build, test and lint on both platforms org-ssg is used from, plus a compiler-floor job.
4#
5# WHAT THIS CATCHES THAT LOCAL WORK DOES NOT:
6#
7# 1. Linux. Development happens on macOS, and the two differ where this project is most
8# likely to break: filesystem event paths (the watcher had a real `/var` vs
9# `/private/var` bug on macOS), case-insensitive filenames, and path separators.
10# 2. A clean checkout. The oracle tests skip when Emacs is absent and the cache is
11# gitignored, so a machine that has been building all afternoon is not a fair test of
12# what a fresh clone does.
13# 3. The MSRV. A stabilised API used without noticing is invisible on a current
14# toolchain and is a build failure for anyone on a distribution compiler.
15#
16# NOT GATED ON `cargo fmt`. The source is formatted by hand — comment tables, aligned
17# match arms, and prose wrapped to fit the argument being made — and rustfmt disagrees
18# with most of it. Clippy is the lint that catches defects; fmt would only catch taste.
19
20on:
21 push:
22 branches: [main]
23 pull_request:
24 workflow_dispatch:
25
26env:
27 CARGO_TERM_COLOR: always
28 # A failing build should print the error, not a backtrace-shaped wall.
29 RUST_BACKTRACE: 1
30
31jobs:
32 test:
33 name: test (${{ matrix.os }})
34 runs-on: ${{ matrix.os }}
35 strategy:
36 # Both platforms report, so a macOS-only failure is distinguishable from a real one.
37 fail-fast: false
38 matrix:
39 os: [ubuntu-latest, macos-latest]
40 steps:
41 - name: Checkout
42 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
43
44 - name: Install Rust
45 uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable
46 with:
47 toolchain: stable
48 components: clippy
49
50 - name: Cache cargo
51 uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
52
53 # Emacs makes the oracle suite run for real instead of skipping. It is the only
54 # reason to trust that output still matches org's own exporter, so it is worth the
55 # install minute.
56 - name: Install Emacs (Linux)
57 if: runner.os == 'Linux'
58 run: sudo apt-get update && sudo apt-get install -y --no-install-recommends emacs-nox
59
60 - name: Install Emacs (macOS)
61 if: runner.os == 'macOS'
62 run: brew install emacs
63
64 - name: Build
65 run: cargo build --all-targets --locked
66
67 - name: Test
68 run: cargo test --locked
69
70 - name: Clippy
71 run: cargo clippy --all-targets --locked -- -D warnings
72
73 # The documentation site is built by the tool it documents, so a docs page that no
74 # longer builds is a product defect. `--strict` fails on broken internal links,
75 # which is the failure mode a docs site actually has.
76 - name: Build the documentation site
77 run: cargo run --locked -- build docs -o docs/_site --strict
78
79 msrv:
80 name: minimum supported Rust (1.88)
81 runs-on: ubuntu-latest
82 steps:
83 - name: Checkout
84 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
85
86 # Pinned to the version in Cargo.toml's `rust-version`. When that moves, this moves
87 # with it in the same commit — a floor nobody checks is a floor nobody has.
88 - name: Install Rust 1.88
89 uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable
90 with:
91 toolchain: "1.88"
92
93 - name: Cache cargo
94 uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
95
96 # Build only. The tests pull in dev-dependencies whose own floors move
97 # independently, and chasing those would make this job about someone else's MSRV.
98 #
99 # The floor is set by dependencies rather than by org-ssg — its own code compiles on
100 # 1.82 — which is precisely why it is checked here instead of reasoned about: a
101 # dependency raising its floor is invisible until someone on an older compiler
102 # tries to build.
103 - name: Build
104 run: cargo build --locked
.github/workflows/release.yml added +113
@@ -0,0 +1,113 @@
1name: Release
2
3# Build binaries for a tag and attach them to a GitHub release.
4#
5# WHY BINARIES AT ALL, given `cargo install org-ssg` exists: installing from source needs
6# a Rust toolchain and about a minute of compiling syntect. Someone evaluating a site
7# generator should be able to download one file and point it at their notes.
8#
9# The tag is the source of truth for the version. The build checks it against Cargo.toml
10# rather than trusting them to match, because a release tagged v0.18.0 containing a binary
11# that reports 0.17.0 is the kind of thing nobody notices for months.
12
13on:
14 push:
15 tags: ["v*"]
16 workflow_dispatch:
17 inputs:
18 tag:
19 description: "Tag to build (for a re-run of a failed release)"
20 required: true
21
22env:
23 CARGO_TERM_COLOR: always
24
25jobs:
26 build:
27 name: ${{ matrix.target }}
28 runs-on: ${{ matrix.os }}
29 strategy:
30 fail-fast: false
31 matrix:
32 include:
33 # Apple Silicon and Intel Macs, built natively on their own runners so neither
34 # is a cross-compile nobody has run.
35 - { os: macos-latest, target: aarch64-apple-darwin }
36 - { os: macos-13, target: x86_64-apple-darwin }
37 # glibc for ordinary distributions, musl for containers and anything older than
38 # the runner's glibc — a dynamically linked binary is the usual reason a
39 # download does not run.
40 - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
41 - { os: ubuntu-latest, target: x86_64-unknown-linux-musl }
42 steps:
43 - name: Checkout
44 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
45 with:
46 ref: ${{ github.event.inputs.tag || github.ref }}
47
48 - name: Install Rust
49 uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable
50 with:
51 toolchain: stable
52 targets: ${{ matrix.target }}
53
54 - name: Install musl tools
55 if: endsWith(matrix.target, '-musl')
56 run: sudo apt-get update && sudo apt-get install -y --no-install-recommends musl-tools
57
58 - name: Check the tag against Cargo.toml
59 shell: bash
60 run: |
61 tag="${{ github.event.inputs.tag || github.ref_name }}"
62 crate=$(cargo metadata --no-deps --format-version 1 \
63 | python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])')
64 if [ "$tag" != "v$crate" ]; then
65 echo "tag $tag does not match Cargo.toml version $crate" >&2
66 exit 1
67 fi
68
69 - name: Build
70 run: cargo build --release --locked --target ${{ matrix.target }}
71
72 # A tarball rather than a bare binary: it keeps the executable bit through GitHub's
73 # download path, and carries the licence with the thing it licenses.
74 - name: Package
75 shell: bash
76 run: |
77 staging="org-ssg-${{ github.event.inputs.tag || github.ref_name }}-${{ matrix.target }}"
78 mkdir "$staging"
79 cp "target/${{ matrix.target }}/release/org-ssg" "$staging/"
80 cp README.md LICENSE CHANGELOG.md "$staging/"
81 tar czf "$staging.tar.gz" "$staging"
82 shasum -a 256 "$staging.tar.gz" > "$staging.tar.gz.sha256"
83
84 - name: Upload
85 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
86 with:
87 name: ${{ matrix.target }}
88 path: |
89 *.tar.gz
90 *.tar.gz.sha256
91
92 release:
93 name: publish the release
94 needs: build
95 runs-on: ubuntu-latest
96 permissions:
97 contents: write
98 steps:
99 - name: Download every build
100 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
101 with:
102 merge-multiple: true
103
104 # A draft, deliberately. The changelog entry is written by a person, and a release
105 # that publishes itself before anyone has read it cannot be edited quietly.
106 - name: Create the draft release
107 uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
108 with:
109 draft: true
110 tag_name: ${{ github.event.inputs.tag || github.ref_name }}
111 files: |
112 *.tar.gz
113 *.tar.gz.sha256
CHANGELOG.md added +156
@@ -0,0 +1,156 @@
1# Changelog
2
3What changed and why, newest first. Entries name the *behaviour* that moved, since that is
4what a rebuild will show you.
5
6Two conventions worth knowing before reading:
7
8- **A cache-format bump is not a change you need to act on.** The incremental cache is
9 versioned and discards itself; a bump means the next build re-renders everything once.
10- **Output changes are called out.** org-ssg aims at what Emacs exports from the same
11 file, so an entry that says "now renders X" means your pages will change. That is the
12 product, not a regression — but it belongs in a changelog rather than a diff you find
13 later.
14
15Versions follow the compatibility promise in the README: config keys, template variables,
16CLI flags and URLs are the stable surface.
17
18## 0.18.0
19
20Release engineering, so that a version number is worth reading.
21
22- **A written compatibility promise.** Config keys, template variables, CLI flags and URLs
23 are the stable surface; the incremental cache, HTML details and the Rust API are not.
24 In the README, and in the guide under *Versioning and upgrades*.
25- **CI** on Linux and macOS: build, test, clippy as an error, and the documentation site
26 built with `--strict`. Emacs is installed on both, so the oracle suite runs for real
27 instead of skipping.
28- **A checked MSRV**, 1.88 — which is how it came to be 1.88 rather than the 1.82
29 org-ssg's own code needs. The floor comes from dependencies, and nobody finds that out
30 by reasoning about it.
31- **Release binaries** for macOS (arm64, x86_64) and Linux (gnu, musl), built on tag into
32 a draft release. The tag is checked against `Cargo.toml` before anything is built.
33- A `LICENSE` file to go with the MIT declaration, crates.io metadata, and a release
34 profile that produces a 5.0 MB binary rather than 6.5 MB.
35- This changelog, and `RELEASING.md`.
36
37## 0.17.0
38
39- **Asset directories outside the source.** `[build] assets = ["../theme/static"]` copies
40 a directory's contents to the site root. A site's static files do not always live where
41 its writing does, and copying them next to the writing is how a repository ends up with
42 two of every stylesheet. `watch` and `serve` watch these directories too. Two files
43 claiming one URL is a build error naming both.
44- **Template hashing is per template.** A page's render key covered every template, so
45 editing a feed template re-rendered the whole site. It now covers the layout the page
46 uses plus what that layout extends, includes or imports. On a 196-page site, editing the
47 feed template renders one page instead of 196.
48- Cache format 7.
49
50## 0.16.0
51
52- **Org's entity table.** `\alpha`, `\rarr`, `20\deg` and the other 412 names, generated
53 from Emacs' own `org-entities`. An unknown name stays literal; `#+OPTIONS: e:nil` turns
54 the table off. *Output changes* for any page using entities.
55- **Table captions.** `#+CAPTION:` above a table becomes a numbered `<caption>`.
56- **`#+INCLUDE:` reports itself.** It was inert and silent, which publishes a page with
57 content missing and nobody told. Now a diagnostic, and `--strict` makes it a failure.
58- The Emacs oracle separates deliberate divergence from defects. Every difference from
59 org's exporter is named and justified, and a test asserts there are no others.
60
61## 0.15.0
62
63Export parity, from a page-by-page diff of a 179-file corpus against the site Emacs
64publishes from the same sources. **All of these change output.**
65
66- Heading levels are relative to a document's shallowest heading, as org exports them.
67- Org's text conversions: `--`, `---`, `...`, and `x^2` / `a_{b}`. Never inside verbatim,
68 code, source blocks or LaTeX. `#+OPTIONS: -:nil`, `^:nil` and `^:{}` all work.
69- Captioned figures are numbered `Figure N:`.
70- A caption attaches to the element *directly* below it; a blank line between attaches to
71 nothing.
72- Checkboxes render as org writes them, which keeps the `[-]` partly-done state a disabled
73 `<input>` could not express. `[@4]` sets a list item's number.
74- A table's special marker column and its marker rows stay out of the output.
75- `#+BEGIN_NOTE` and any other unrecognised name is a special block: a div holding parsed
76 org rather than a `<pre>` of literal text. Verse keeps its line breaks.
77- Emphasis borders forbid whitespace and nothing else, so `="proxied":false=` is verbatim
78 and `~~/.config/doom/config.el~` is a path that starts with a tilde.
79- Listings sort on the time of day when a timestamp carries one.
80- Cache format 6.
81
82## 0.14.0
83
84- **Per-page layouts.** `[[pages]]` rules map a source path to a template, and
85 `#+TEMPLATE:` on a page overrides any rule. A missing template fails the build naming
86 the page, the template, and what does exist.
87- `page.year`, for grouping a listing by year with minijinja's `groupby`.
88- An explicit nav can order generated pages among authored ones. `nav.mode = "none"` now
89 really means none.
90
91## 0.13.0
92
93- Bundled TOML and Org syntax definitions, a `syntaxes_dir` for your own, and org's comma
94 escape (`,* heading` inside a block).
95
96## 0.12.0
97
98- `serve`: a development server with live reload, bound to loopback.
99- A documentation site under `docs/`, built by org-ssg itself.
100
101## 0.11.0
102
103- Table of contents as `page.toc`, section numbers, and org's `#+OPTIONS:` per-file
104 switches.
105
106## 0.10.0
107
108- Excerpts, word count, reading time, a `truncate` filter, and `#+DRAFT:` pages.
109
110## 0.9.0
111
112- `watch`: rebuilds on OS filesystem events, debounced.
113
114## 0.8.0
115
116- `site.base_url`, the `absolute` and `rfc822` filters, canonical links, and an RSS feed
117 in the scaffold that validates.
118
119## 0.7.0
120
121- Pagination for large listings, with a `paginator` template context that composes with
122 grouping.
123
124## 0.6.0
125
126- Grouped collections: one page per tag plus a tag index.
127- Generated listing pages (`[[collections]]`), sorted indexes, and feeds via XML
128 templates.
129- A config file, user templates, nav modes, an `init` scaffold, and discovery that will
130 not publish `.git`.
131
132## 0.5.0
133
134- Parse diagnostics carry `file:line`, and pages render in parallel.
135- The corpus audit (`org-ssg audit`) and the `emacs --batch` oracle.
136- `#+SLUG:` decides a page's output filename — found by auditing a real corpus, where it
137 affected 169 of 182 URLs.
138
139## 0.4.0
140
141- The full v1 construct scope, with the IN/OUT line under test.
142
143## 0.3.0
144
145- The incremental build layer: content, config and template hashing, a dependency graph,
146 per-page render keys, and a persisted cache manifest. A full build and an incremental
147 build produce byte-identical output.
148
149## 0.2.0
150
151- Multi-file site builds: a symbol table, internal link resolution, minijinja templates,
152 tables and footnotes.
153
154## 0.1.0
155
156- Parse and render a single `.org` file to HTML.
Cargo.lock +1 −1
@@ -675,7 +675,7 @@ dependencies = [
675 675
676[[package]] 676[[package]]
677name = "org-ssg" 677name = "org-ssg"
678version = "0.17.0" 678version = "0.18.0"
679dependencies = [ 679dependencies = [
680 "anyhow", 680 "anyhow",
681 "blake3", 681 "blake3",
Cargo.toml +25 −1
@@ -1,9 +1,26 @@
1[package] 1[package]
2name = "org-ssg" 2name = "org-ssg"
3version = "0.17.0" 3version = "0.18.0"
4edition = "2021" 4edition = "2021"
5description = "Org-mode static site generator that renders the org element tree straight to HTML" 5description = "Org-mode static site generator that renders the org element tree straight to HTML"
6license = "MIT" 6license = "MIT"
7readme = "README.md"
8keywords = ["org-mode", "static-site-generator", "emacs", "html", "blog"]
9categories = ["command-line-utilities", "text-processing"]
10# Set before the first `cargo publish`: crates.io shows it on the crate page, and a
11# missing link is the first thing anyone evaluating a generator looks for.
12# repository = "https://git.krz.sh/cmc/org-ssg.git/"
13
14# The compiler floor, checked in CI rather than assumed. org-ssg's own code needs 1.82
15# (`Option::is_none_or`); the floor is 1.88 because dependencies in Cargo.lock declare it
16# — `plist` and `time`, both by way of syntect. Raising this is a minor-version change,
17# never a patch.
18rust-version = "1.88"
19
20# Published crates carry the source, the fixtures the tests need, and nothing else. The
21# documentation site is 14 org files plus its build output, which nobody installing a
22# binary wants to download.
23exclude = ["docs/", "target/", "/.github/"]
7 24
8[lib] 25[lib]
9name = "org_ssg" 26name = "org_ssg"
@@ -37,3 +54,10 @@ tiny_http = "0.12.0"
37 54
38[dev-dependencies] 55[dev-dependencies]
39insta = { version = "1", features = ["json"] } 56insta = { version = "1", features = ["json"] }
57
58# Release binaries are downloaded by people evaluating the tool, so they are built to be
59# small and quick to start rather than quick to compile. Thin LTO keeps CI build times
60# reasonable; full LTO bought a few percent for minutes per job.
61[profile.release]
62lto = "thin"
63strip = "symbols"
LICENSE added +21
@@ -0,0 +1,21 @@
1MIT License
2
3Copyright (c) 2026 Christian Cleberg
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
README.md +28 −2
@@ -387,7 +387,7 @@ all-of-org. Phase 0 checked this line against a real 179-file corpus and found i
387| **19** | **Export parity: relative heading levels, special strings, sub/superscript, caption numbering, checkbox and counter markup, table marker columns, special blocks** | **done** | 387| **19** | **Export parity: relative heading levels, special strings, sub/superscript, caption numbering, checkbox and counter markup, table marker columns, special blocks** | **done** |
388| **20** | **Correctness debt: org's entity table, table captions, a reported `#+INCLUDE:`, and an oracle that separates deliberate divergence from defects** | **done** | 388| **20** | **Correctness debt: org's entity table, table captions, a reported `#+INCLUDE:`, and an oracle that separates deliberate divergence from defects** | **done** |
389| **21** | **Extra asset roots; per-template hashing so one layout edit does not re-render the site** | **done** | 389| **21** | **Extra asset roots; per-template hashing so one layout edit does not re-render the site** | **done** |
390| 22 | Release engineering: CI, MSRV, published binaries, changelog, a written compatibility promise | 1.0 | 390| **22** | **Release engineering: CI on both platforms, a checked MSRV, release binaries, a changelog, and a written compatibility promise** | **done** |
391 391
392### v0.2 in / out 392### v0.2 in / out
393 393
@@ -688,6 +688,32 @@ anchored — `:CUSTOM_ID:`/`:ID:` else a slug of its text) and trailing tags; pa
688plain lists (unordered + ordered) with checkboxes; source blocks; inline markup (`*bold*`, 688plain lists (unordered + ordered) with checkboxes; source blocks; inline markup (`*bold*`,
689`/italic/`, `_underline_`, `+strike+`, `=verbatim=`, `~code~`); links and bare URLs. 689`/italic/`, `_underline_`, `+strike+`, `=verbatim=`, `~code~`); links and bare URLs.
690 690
691## Compatibility
692
693Versions mean something as of 1.0. The **stable surface** — changing incompatibly requires
694a major version — is what you actually build a site against:
695
696| Stable | Detail |
697|---|---|
698| `org-ssg.toml` keys | Names, types and meaning. New keys are minor releases; removing one is major. |
699| Template context | `page`, `site`, `nav`, `root`, `pages`, `group`, `groups`, `paginator`, `stylesheet`, and the `absolute` / `rfc822` / `truncate` filters. |
700| CLI | Command names, flags, and exit codes. |
701| URLs | How a source path becomes an output path, including `#+SLUG:`. A generator that moves your URLs breaks every link anyone has to you. |
702
703Explicitly **not stable**, so that the above can be:
704
705- **The incremental cache.** Versioned, discarded on mismatch, never a correctness
706 dependency. It changes whenever it needs to, in any release.
707- **Rendered HTML details.** org-ssg tracks what Emacs exports from the same file, and
708 closing a gap changes markup. Changes that affect output are called out in
709 [CHANGELOG.md](CHANGELOG.md) — the class names the documentation names (`post-list`,
710 `figure-number`, `section-number-N`, `footnote-ref`) are the ones to write CSS against.
711- **The Rust API.** The crate is published so the binary can be installed with
712 `cargo install`; the library exists to serve it, and its types move as the tool does.
713
714The **MSRV is 1.88**, checked in CI on every change. org-ssg's own code compiles on
7151.82; the floor comes from dependencies. Raising it is a minor version, never a patch.
716
691## Dependencies 717## Dependencies
692 718
693Parser is hand-written recursive descent (not `nom`/`chumsky`/`pest` — org is 719Parser is hand-written recursive descent (not `nom`/`chumsky`/`pest` — org is
@@ -702,7 +728,7 @@ development server), `toml` (config), `chrono`, `camino`, `walkdir`, `clap`, `an
702 728
703``` 729```
704cargo build 730cargo build
705cargo test # 156 tests 731cargo test # 191 tests
706cargo run -- init my-site # scaffold a new site 732cargo run -- init my-site # scaffold a new site
707cargo run -- build fixtures/minimal.org -o minimal.html # single file 733cargo run -- build fixtures/minimal.org -o minimal.html # single file
708cargo run -- build fixtures/site -o _site # whole site (incremental) 734cargo run -- build fixtures/site -o _site # whole site (incremental)
RELEASING.md added +78
@@ -0,0 +1,78 @@
1# Releasing
2
3A release is three things that must agree: a version in `Cargo.toml`, a git tag, and a
4changelog entry. The release workflow checks the first two against each other and refuses
5to build if they differ, because a release tagged `v0.18.0` containing a binary that
6reports `0.17.0` is the kind of mistake nobody notices for months.
7
8## Before the first publish
9
10`repository` in `Cargo.toml` is commented out, because a wrong URL on a crates.io page is
11worse than none. Set it, then:
12
13```bash
14cargo login # a crates.io token, once per machine
15cargo publish --dry-run
16```
17
18## Every release
19
201. **Write the changelog entry first.** [CHANGELOG.md](CHANGELOG.md) names behaviour, not
21 commits — someone reading it wants to know what their next build will do differently.
22 Anything that changes rendered HTML gets said out loud.
23
242. **Bump the version** in `Cargo.toml`, and build once so `Cargo.lock` follows.
25
26 Patch for fixes that change nothing about the stable surface. Minor for new config
27 keys, new template variables, an MSRV bump, or output that changes to track Emacs more
28 closely. Major for anything that breaks the promises in the README's Compatibility
29 section — config keys, template context, CLI, or URLs.
30
313. **Check it.**
32
33 ```bash
34 cargo test
35 cargo clippy --all-targets -- -D warnings
36 cargo run -- build docs -o docs/_site --strict
37 cargo package
38 ```
39
40 `cargo package` is the one people forget: it builds the crate exactly as crates.io will
41 receive it, and catches a file the `exclude` list should not have removed.
42
434. **Verify against a real corpus.** The test suite says the code does what it did; a
44 corpus says the *site* does. Build a site you know with `--no-cache` and diff the
45 output against the previous version's. A release that quietly changes 200 pages should
46 do so on purpose.
47
485. **Commit, tag, push.**
49
50 ```bash
51 git commit -am "0.18: <what changed>"
52 git tag -a v0.18.0 -m "0.18.0"
53 git push && git push --tags
54 ```
55
566. **Publish the crate.**
57
58 ```bash
59 cargo publish
60 ```
61
62 This is irreversible: a published version can be yanked but never replaced.
63
647. **Finish the GitHub release.** Pushing the tag builds binaries for macOS (arm64 and
65 x86_64) and Linux (gnu and musl) and opens a *draft* release with them attached. Paste
66 the changelog entry in and publish it. The draft is deliberate — a release that
67 publishes itself before anyone has read it cannot be edited quietly.
68
69## If a release goes wrong
70
71Yank rather than delete, and ship a fix as a new version:
72
73```bash
74cargo yank --version 0.18.0
75```
76
77Yanking stops new dependents from selecting it; anyone who already has it keeps working.
78Then release `0.18.1` with the fix and a changelog entry that says what happened.
docs/guide/10-deploying.org +7
@@ -112,3 +112,10 @@ built 182 page(s) (182 rendered, 0 cached), copied 3 asset(s) from content -> _s
112Both zeros matter. Unresolved links are internal links pointing at nothing; diagnostics 112Both zeros matter. Unresolved links are internal links pointing at nothing; diagnostics
113are malformed org that degraded rather than failing. With =--strict= neither can reach 113are malformed org that degraded rather than failing. With =--strict= neither can reach
114this line, because either would have failed the build. 114this line, because either would have failed the build.
115
116* After an upgrade
117
118The first build on a new version is worth running with =--no-cache=, so you compare the
119new output to the old rather than to a cache written by both. What a version number
120promises — and what it does not — is in [[file:11-versioning.org][Versioning and
121upgrades]].
docs/guide/11-versioning.org added +72
@@ -0,0 +1,72 @@
1#+TITLE: Versioning and upgrades
2#+DESCRIPTION: What a version number promises, what it does not, and how to upgrade safely.
3#+LEDE: The stable surface is what you build a site against, not what the code happens to do.
4
5A generator you point at ten years of writing needs to be boring about compatibility.
6This page says exactly what is promised.
7
8* The stable surface
9
10Changing any of this incompatibly requires a major version.
11
12| Stable | What that covers |
13|--------+------------------|
14| =org-ssg.toml= keys | Their names, types and meaning. |
15| Template context | =page=, =site=, =nav=, =root=, =pages=, =group=, =groups=, =paginator=, =stylesheet=, and the =absolute=, =rfc822= and =truncate= filters. |
16| The CLI | Command names, flags and exit codes. |
17| URLs | How a source path becomes an output path, =#+SLUG:= included. |
18
19*URLs are on that list deliberately.* A generator that quietly moves your pages breaks
20every link anyone has ever made to you, and no upgrade note fixes an inbound link.
21
22Adding things — a new config key, a new template variable — is a minor release. Nothing
23you already wrote stops working.
24
25* What is not stable
26
27Three things move freely, so the list above can hold still.
28
29** The incremental cache
30
31=<output>/.org-ssg-cache.json= is versioned and discards itself on a mismatch. A cache
32format bump means one full rebuild, and nothing else. It is never a correctness
33dependency: a missing, stale or corrupt cache produces exactly the same site, more slowly.
34
35** Rendered HTML details
36
37org-ssg aims at what Emacs exports from the same file, and closing a gap changes markup.
38That is the product working rather than a regression — but it is called out in the
39changelog every time, because your stylesheet is downstream of it.
40
41The class names the documentation names are the ones to write CSS against:
42=post-list=, =post-list-item=, =figure-number=, =table-number=, =section-number-N=,
43=footnote-ref=, =verbatim=, and the =on=/=off=/=trans= classes on checkbox items.
44
45** The Rust API
46
47The crate is on crates.io so the binary can be installed with =cargo install=. The library
48exists to serve the binary, and its types move as the tool does.
49
50* The compiler floor
51
52The MSRV is *1.88*, checked in CI on every change rather than assumed — which is how it
53came to be 1.88 rather than the 1.82 org-ssg's own code needs. The floor is set by
54dependencies, and a dependency raising its own is invisible until someone on an older
55compiler tries to build.
56
57Raising it is a minor version, never a patch.
58
59* Upgrading
60
61#+BEGIN_SRC sh
62cargo install org-ssg # or download a release binary
63org-ssg build content -o _site --no-cache --strict
64#+END_SRC
65
66=--no-cache= makes the first build after an upgrade a full one, so you are comparing the
67new version's output to the old version's output rather than to a cache written by a
68mixture of both. =--strict= turns a link that stopped resolving into a failure.
69
70If you keep your built site in version control, the diff after that command *is* the
71upgrade report — which is the most useful review a generator can give you, and the reason
72the changelog names behaviour rather than commits.