Commit bbe31403db
Verified · cmc
Layout: unified · split
.github/workflows/ci.yml added +104
| @@ -0,0 +1,104 @@ | ||
| 1 | name: 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 | ||
| 20 | on: | |
| 21 | push: | |
| 22 | branches: [main] | |
| 23 | pull_request: | |
| 24 | workflow_dispatch: | |
| 25 | ||
| 26 | env: | |
| 27 | CARGO_TERM_COLOR: always | |
| 28 | # A failing build should print the error, not a backtrace-shaped wall. | |
| 29 | RUST_BACKTRACE: 1 | |
| 30 | ||
| 31 | jobs: | |
| 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 @@ | ||
| 1 | name: 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 | ||
| 13 | on: | |
| 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 | ||
| 22 | env: | |
| 23 | CARGO_TERM_COLOR: always | |
| 24 | ||
| 25 | jobs: | |
| 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 | ||
| 3 | What changed and why, newest first. Entries name the *behaviour* that moved, since that is | |
| 4 | what a rebuild will show you. | |
| 5 | ||
| 6 | Two 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 | ||
| 15 | Versions follow the compatibility promise in the README: config keys, template variables, | |
| 16 | CLI flags and URLs are the stable surface. | |
| 17 | ||
| 18 | ## 0.18.0 | |
| 19 | ||
| 20 | Release 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 | ||
| 63 | Export parity, from a page-by-page diff of a 179-file corpus against the site Emacs | |
| 64 | publishes 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 | 676 | [[package]] |
| 677 | 677 | name = "org-ssg" |
| 678 | version = "0.17.0" | |
| 678 | version = "0.18.0" | |
| 679 | 679 | dependencies = [ |
| 680 | 680 | "anyhow", |
| 681 | 681 | "blake3", |
Cargo.toml +25 −1
| @@ -1,9 +1,26 @@ | ||
| 1 | 1 | [package] |
| 2 | 2 | name = "org-ssg" |
| 3 | version = "0.17.0" | |
| 3 | version = "0.18.0" | |
| 4 | 4 | edition = "2021" |
| 5 | 5 | description = "Org-mode static site generator that renders the org element tree straight to HTML" |
| 6 | 6 | license = "MIT" |
| 7 | readme = "README.md" | |
| 8 | keywords = ["org-mode", "static-site-generator", "emacs", "html", "blog"] | |
| 9 | categories = ["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. | |
| 18 | rust-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. | |
| 23 | exclude = ["docs/", "target/", "/.github/"] | |
| 7 | 24 | |
| 8 | 25 | [lib] |
| 9 | 26 | name = "org_ssg" |
| @@ -37,3 +54,10 @@ tiny_http = "0.12.0" | ||
| 37 | 54 | |
| 38 | 55 | [dev-dependencies] |
| 39 | 56 | insta = { 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] | |
| 62 | lto = "thin" | |
| 63 | strip = "symbols" | |
LICENSE added +21
| @@ -0,0 +1,21 @@ | ||
| 1 | MIT License | |
| 2 | ||
| 3 | Copyright (c) 2026 Christian Cleberg | |
| 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | of this software and associated documentation files (the "Software"), to deal | |
| 7 | in the Software without restriction, including without limitation the rights | |
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | copies of the Software, and to permit persons to whom the Software is | |
| 10 | furnished to do so, subject to the following conditions: | |
| 11 | ||
| 12 | The above copyright notice and this permission notice shall be included in all | |
| 13 | copies or substantial portions of the Software. | |
| 14 | ||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | SOFTWARE. | |
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 | 387 | | **19** | **Export parity: relative heading levels, special strings, sub/superscript, caption numbering, checkbox and counter markup, table marker columns, special blocks** | **done** | |
| 388 | 388 | | **20** | **Correctness debt: org's entity table, table captions, a reported `#+INCLUDE:`, and an oracle that separates deliberate divergence from defects** | **done** | |
| 389 | 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 | 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 | ||
| 688 | 688 | plain lists (unordered + ordered) with checkboxes; source blocks; inline markup (`*bold*`, |
| 689 | 689 | `/italic/`, `_underline_`, `+strike+`, `=verbatim=`, `~code~`); links and bare URLs. |
| 690 | 690 | |
| 691 | ## Compatibility | |
| 692 | ||
| 693 | Versions mean something as of 1.0. The **stable surface** — changing incompatibly requires | |
| 694 | a 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 | ||
| 703 | Explicitly **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 | ||
| 714 | The **MSRV is 1.88**, checked in CI on every change. org-ssg's own code compiles on | |
| 715 | 1.82; the floor comes from dependencies. Raising it is a minor version, never a patch. | |
| 716 | ||
| 691 | 717 | ## Dependencies |
| 692 | 718 | |
| 693 | 719 | Parser 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 | ``` |
| 704 | 730 | cargo build |
| 705 | cargo test # 156 tests | |
| 731 | cargo test # 191 tests | |
| 706 | 732 | cargo run -- init my-site # scaffold a new site |
| 707 | 733 | cargo run -- build fixtures/minimal.org -o minimal.html # single file |
| 708 | 734 | cargo run -- build fixtures/site -o _site # whole site (incremental) |
RELEASING.md added +78
| @@ -0,0 +1,78 @@ | ||
| 1 | # Releasing | |
| 2 | ||
| 3 | A release is three things that must agree: a version in `Cargo.toml`, a git tag, and a | |
| 4 | changelog entry. The release workflow checks the first two against each other and refuses | |
| 5 | to build if they differ, because a release tagged `v0.18.0` containing a binary that | |
| 6 | reports `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 | |
| 11 | worse than none. Set it, then: | |
| 12 | ||
| 13 | ```bash | |
| 14 | cargo login # a crates.io token, once per machine | |
| 15 | cargo publish --dry-run | |
| 16 | ``` | |
| 17 | ||
| 18 | ## Every release | |
| 19 | ||
| 20 | 1. **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 | ||
| 24 | 2. **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 | ||
| 31 | 3. **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 | ||
| 43 | 4. **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 | ||
| 48 | 5. **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 | ||
| 56 | 6. **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 | ||
| 64 | 7. **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 | ||
| 71 | Yank rather than delete, and ship a fix as a new version: | |
| 72 | ||
| 73 | ```bash | |
| 74 | cargo yank --version 0.18.0 | |
| 75 | ``` | |
| 76 | ||
| 77 | Yanking stops new dependents from selecting it; anyone who already has it keeps working. | |
| 78 | Then 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 | ||
| 112 | 112 | Both zeros matter. Unresolved links are internal links pointing at nothing; diagnostics |
| 113 | 113 | are malformed org that degraded rather than failing. With =--strict= neither can reach |
| 114 | 114 | this line, because either would have failed the build. |
| 115 | ||
| 116 | * After an upgrade | |
| 117 | ||
| 118 | The first build on a new version is worth running with =--no-cache=, so you compare the | |
| 119 | new output to the old rather than to a cache written by both. What a version number | |
| 120 | promises — and what it does not — is in [[file:11-versioning.org][Versioning and | |
| 121 | upgrades]]. | |
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 | ||
| 5 | A generator you point at ten years of writing needs to be boring about compatibility. | |
| 6 | This page says exactly what is promised. | |
| 7 | ||
| 8 | * The stable surface | |
| 9 | ||
| 10 | Changing 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 | |
| 20 | every link anyone has ever made to you, and no upgrade note fixes an inbound link. | |
| 21 | ||
| 22 | Adding things — a new config key, a new template variable — is a minor release. Nothing | |
| 23 | you already wrote stops working. | |
| 24 | ||
| 25 | * What is not stable | |
| 26 | ||
| 27 | Three 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 | |
| 32 | format bump means one full rebuild, and nothing else. It is never a correctness | |
| 33 | dependency: a missing, stale or corrupt cache produces exactly the same site, more slowly. | |
| 34 | ||
| 35 | ** Rendered HTML details | |
| 36 | ||
| 37 | org-ssg aims at what Emacs exports from the same file, and closing a gap changes markup. | |
| 38 | That is the product working rather than a regression — but it is called out in the | |
| 39 | changelog every time, because your stylesheet is downstream of it. | |
| 40 | ||
| 41 | The 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 | ||
| 47 | The crate is on crates.io so the binary can be installed with =cargo install=. The library | |
| 48 | exists to serve the binary, and its types move as the tool does. | |
| 49 | ||
| 50 | * The compiler floor | |
| 51 | ||
| 52 | The MSRV is *1.88*, checked in CI on every change rather than assumed — which is how it | |
| 53 | came to be 1.88 rather than the 1.82 org-ssg's own code needs. The floor is set by | |
| 54 | dependencies, and a dependency raising its own is invisible until someone on an older | |
| 55 | compiler tries to build. | |
| 56 | ||
| 57 | Raising it is a minor version, never a patch. | |
| 58 | ||
| 59 | * Upgrading | |
| 60 | ||
| 61 | #+BEGIN_SRC sh | |
| 62 | cargo install org-ssg # or download a release binary | |
| 63 | org-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 | |
| 67 | new version's output to the old version's output rather than to a cache written by a | |
| 68 | mixture of both. =--strict= turns a link that stopped resolving into a failure. | |
| 69 | ||
| 70 | If you keep your built site in version control, the diff after that command *is* the | |
| 71 | upgrade report — which is the most useful review a generator can give you, and the reason | |
| 72 | the changelog names behaviour rather than commits. | |