|
| 1 | +--- |
| 2 | +name: sacho |
| 3 | +description: >- |
| 4 | + Help a maintainer adopt and operate Sacho, the opinionated changelog manager |
| 5 | + that keeps user-facing release notes as one small Markdown fragment per change |
| 6 | + under changes.d/ and compiles them into CHANGES.md at release time. Use this |
| 7 | + skill whenever a repository has a sacho.toml, a changes.d/ directory, or a |
| 8 | + CHANGES.md built from fragments; whenever the user wants to add Sacho to a |
| 9 | + project, write or edit a changelog entry for a change they just made, cut a |
| 10 | + release, publish release notes, or wire `sacho check` into CI or a pre-commit |
| 11 | + hook; whenever forward-porting a bugfix release across maintenance branches or |
| 12 | + resolving a changelog merge conflict; and whenever `sacho check` or the |
| 13 | + changelog merge driver reports a problem. Also use it any time the user |
| 14 | + mentions Sacho, changes.d fragments, or asks how to write a changelog entry |
| 15 | + for users instead of generating one from commit messages. |
| 16 | +metadata: |
| 17 | + type: reference |
| 18 | +--- |
| 19 | + |
| 20 | +Managing changelogs with Sacho |
| 21 | +============================== |
| 22 | + |
| 23 | +Sacho keeps a project's release notes as one small Markdown file per change, |
| 24 | +called a fragment, under `changes.d/`. At release time it sorts and compiles the |
| 25 | +fragments into a dated section of `CHANGES.md`, then deletes the consumed |
| 26 | +fragments. It works in any repository regardless of language, ships as a single |
| 27 | +static binary, and does not even assume Git. |
| 28 | + |
| 29 | +The single most important thing to hold onto: Sacho never generates entries from |
| 30 | +commit messages, and neither should you. A commit message explains to |
| 31 | +collaborators why a change was made; a changelog entry tells users what changed |
| 32 | +and what to do when they upgrade. Those are different documents for different |
| 33 | +readers. When you draft an entry, do not transform a diff or a `git log` line |
| 34 | +into prose. Read the change, work out its effect on someone who only sees the |
| 35 | +public surface, and write that. When a change has no user-visible effect, the |
| 36 | +honest entry is no entry. |
| 37 | + |
| 38 | + |
| 39 | +Delegate the details to the live docs |
| 40 | +------------------------------------- |
| 41 | + |
| 42 | +The commands and flags below are a working map, not the source of truth. For the |
| 43 | +exact syntax of the version installed in this repository, trust `sacho --help` |
| 44 | +and `sacho <command> --help` over anything written here or online, because the |
| 45 | +docs track the latest release while a project may pin an older binary. Use the |
| 46 | +Markdown documentation for concepts, workflow, and reasoning; reach for `--help` |
| 47 | +before you depend on a specific flag or output format. |
| 48 | + |
| 49 | + - `https://sacho.dev/llms.txt` lists every page. |
| 50 | + `https://sacho.dev/llms-full.txt` is the whole documentation in one file |
| 51 | + when you want to read broadly. |
| 52 | + - Every page has a `.md` twin. Fetch these directly: |
| 53 | + - `https://sacho.dev/why-sacho.md` and `https://sacho.dev/philosophy.md` |
| 54 | + for the reasoning, worth reading before you write entries for a project. |
| 55 | + - `https://sacho.dev/guide/getting-started.md` to adopt Sacho. |
| 56 | + - `https://sacho.dev/guide/everyday-workflow.md` for the per-change loop. |
| 57 | + - `https://sacho.dev/guide/releases.md` for cutting releases and |
| 58 | + forward-porting. |
| 59 | + - `https://sacho.dev/guide/ci-and-hooks.md` for coverage enforcement. |
| 60 | + - `https://sacho.dev/guide/version-control.md` for merge drivers and |
| 61 | + presets. |
| 62 | + - `https://sacho.dev/guide/package-monorepos.md` for deriving sections |
| 63 | + from regular package layouts. |
| 64 | + - `https://sacho.dev/concepts/fragments.md`, |
| 65 | + `https://sacho.dev/concepts/changelog-lifecycle.md`, and |
| 66 | + `https://sacho.dev/concepts/sections.md` for the model. |
| 67 | + - `https://sacho.dev/reference/commands.md` and |
| 68 | + `https://sacho.dev/reference/configuration.md` for exhaustive |
| 69 | + references. |
| 70 | + - `https://sacho.dev/reference/section-patterns.md` for the capture |
| 71 | + grammar and precedence rules. |
| 72 | + - `https://sacho.dev/troubleshooting.md` when something misbehaves. |
| 73 | + |
| 74 | + |
| 75 | +Orient yourself before acting |
| 76 | +----------------------------- |
| 77 | + |
| 78 | +Check what the repository already has. Is there a `sacho.toml` at the root, a |
| 79 | +`changes.d/` directory, a `CHANGES.md`? Is `sacho` on the `PATH` |
| 80 | +(`sacho --version`)? A repository with `sacho.toml` is already set up, so skip |
| 81 | +adoption and go to the workflow that fits the request. A repository without it |
| 82 | +needs `sacho init` first, but only after confirming the maintainer wants to |
| 83 | +adopt Sacho rather than asking about it. |
| 84 | + |
| 85 | +If the binary is missing, the recommended install is |
| 86 | +`mise use -g github:dahlia/sacho`; `npm install -g @sacho/sacho` and |
| 87 | +`cargo install sacho` work too. Read `getting-started.md` for the full list. |
| 88 | + |
| 89 | + |
| 90 | +Adopting Sacho in a project |
| 91 | +--------------------------- |
| 92 | + |
| 93 | +Run `sacho init` from the repository root. It creates `sacho.toml`, |
| 94 | +`changes.d/`, and `CHANGES.md`, and in a Git repository registers the merge |
| 95 | +drivers. Interactive setup infers issue-link templates from the remote and can |
| 96 | +install commit hooks. When an existing changelog is present, it also offers |
| 97 | +level-three headings found across the changelog as section candidates and |
| 98 | +suggests directories and source path globs. When multiple selected headings |
| 99 | +map to one unambiguous sibling package directory, it can store them as one |
| 100 | +section pattern. `sacho init --interactive` forces the questions and |
| 101 | +`--no-interactive` suppresses them. |
| 102 | + |
| 103 | +Leave old released sections exactly as they are. If the current `Unreleased` or |
| 104 | +`Version X` region contains entries, run `sacho import-unreleased` before |
| 105 | +creating fragments. Inspect and confirm its normalization diff; in a |
| 106 | +noninteractive workflow, review the diff from the refused run before using |
| 107 | +`--force`. The command creates deterministic *imported-unreleased.md* fragments |
| 108 | +and infers the next version from `Version X`. If there are no current entries, |
| 109 | +skip the import and set the version with `sacho next 1.2.0`. |
| 110 | + |
| 111 | +Two configuration decisions matter early, both covered in |
| 112 | +`reference/configuration.md`: whether to enforce fragment coverage |
| 113 | +(`[check].paths`), and, for a monorepo, whether to define explicit |
| 114 | +`[[sections]]` or one `[[section-patterns]]` family so each package's changes |
| 115 | +require a fragment in that package's section. |
| 116 | + |
| 117 | + |
| 118 | +Writing a fragment for a change |
| 119 | +------------------------------- |
| 120 | + |
| 121 | +The everyday loop is four commands. Create the fragment, write it, format, and |
| 122 | +check: |
| 123 | + |
| 124 | +~~~~ sh |
| 125 | +sacho add clear-function # add --section <id> <name> in a sectioned repo |
| 126 | +sacho fmt |
| 127 | +sacho preview |
| 128 | +sacho check |
| 129 | +~~~~ |
| 130 | + |
| 131 | +`sacho add` creates `changes.d/<name>.md` holding an empty list item and prints |
| 132 | +the path. Name the file after the change itself (`clear-function`), never after |
| 133 | +an issue or PR number. Topic names make the right follow-up natural: when a |
| 134 | +later commit reworks the same feature, you edit the existing fragment instead of |
| 135 | +adding a second one that documents a state users never saw. |
| 136 | + |
| 137 | +Keep the fragment in the same commit series as the implementation. Stored in the |
| 138 | +repository, it travels with the change through merges, rebases, cherry-picks, |
| 139 | +and reverts for free. That coupling is the whole point of fragments, so never |
| 140 | +park changelog text in an issue, a wiki, or a draft release note. |
| 141 | + |
| 142 | + |
| 143 | +Writing entries users can actually read |
| 144 | +--------------------------------------- |
| 145 | + |
| 146 | +This is the part no tool can do for the maintainer, and where you earn your |
| 147 | +keep. Fetch `philosophy.md` when you want the full argument; the working rules: |
| 148 | + |
| 149 | +Address the person upgrading, in the vocabulary of the public surface. A |
| 150 | +refactor that touches forty files but changes nothing users can observe is a |
| 151 | +real commit and a nonexistent entry. A one-line dependency bump that closes a |
| 152 | +vulnerability is a trivial commit and a critical entry. |
| 153 | + |
| 154 | +Write one entry per user-visible change. If a release adds `remove_all()` and a |
| 155 | +later commit in the same cycle renames it to `clear()`, the released changelog |
| 156 | +contains a single line, `Added clear() function`, not the two-step history. Edit |
| 157 | +the existing fragment; never narrate intermediate states that never shipped. |
| 158 | + |
| 159 | +Be concise, and keep internal names out. Private type names, module paths, and |
| 160 | +subsystem nicknames tell the reader this document is not for them. If a |
| 161 | +mechanism must be mentioned, describe what it does rather than naming a thing |
| 162 | +the reader cannot look up. |
| 163 | + |
| 164 | +Start each entry with a past-tense verb: Added, Changed, Deprecated, Fixed, |
| 165 | +Removed, Security. Sacho sorts by the entry text, so these verbs fall into the |
| 166 | +familiar Keep a Changelog order without any category metadata. |
| 167 | + |
| 168 | +Reference issues and PRs with shortcut links in trailing brackets, house style, |
| 169 | +at the end of the first paragraph: `[[#842], [#848]]`. Attribution is inline |
| 170 | +prose in the same bracket: `[[#857] by Lee Hoyeon]`. The compiler resolves the |
| 171 | +numbers into link definitions using the templates in `[links]`, and `check` |
| 172 | +fails on a reference no template can resolve. |
| 173 | + |
| 174 | +Because Sacho asks a human to write, present your drafts to the maintainer for |
| 175 | +approval rather than committing prose in their voice unreviewed. You are helping |
| 176 | +them write for their users, not automating the writing away. |
| 177 | + |
| 178 | + |
| 179 | +The fragment file format |
| 180 | +------------------------ |
| 181 | + |
| 182 | +A fragment is a UTF-8 Markdown file whose body is exactly one top-level |
| 183 | +unordered list, with optional YAML frontmatter. Nothing else at the top level: |
| 184 | +no headings, no stray paragraphs, no second list. Inside a list item any block |
| 185 | +content is fine, including nested lists and fenced code. One item is usual; |
| 186 | +related changes from one pull request can share a fragment as several items. |
| 187 | + |
| 188 | +~~~~ markdown |
| 189 | +--- |
| 190 | +priority: -10 |
| 191 | +--- |
| 192 | + |
| 193 | + - Added `clear()` to remove every entry at once. The function accepts an |
| 194 | + optional predicate to remove entries selectively. [[#842], [#848]] |
| 195 | +~~~~ |
| 196 | + |
| 197 | +`priority` (integer, default `0`) is the only frontmatter key; lower sorts |
| 198 | +earlier, and most fragments need no frontmatter at all. See |
| 199 | +`concepts/fragments.md` for the exact constraints. |
| 200 | + |
| 201 | + |
| 202 | +Cutting a release |
| 203 | +----------------- |
| 204 | + |
| 205 | +Read the compiled result as a user would before you freeze it: |
| 206 | + |
| 207 | +~~~~ sh |
| 208 | +sacho next 1.2.0 |
| 209 | +sacho preview |
| 210 | +sacho check |
| 211 | +~~~~ |
| 212 | + |
| 213 | +In the preview, merge entries that describe the same change, drop development |
| 214 | +history, and confirm each entry names public behavior. Then compile: |
| 215 | + |
| 216 | +~~~~ sh |
| 217 | +sacho release --next 1.3.0 # reads the version from changes.d/next |
| 218 | +sacho release 1.2.0 --next 1.3.0 # or state the version explicitly |
| 219 | +sacho release 1.2.0 --date 2026-07-19 --next 1.3.0 # when the date must be fixed |
| 220 | +~~~~ |
| 221 | + |
| 222 | +`release` stamps the dated section, deletes the consumed fragments, and sets the |
| 223 | +next version. The first release may proceed without fragments when the changelog |
| 224 | +has no released version sections. Later releases refuse to proceed when the |
| 225 | +fragments compile to nothing unless `--allow-empty` is passed. Empty list items |
| 226 | +and HTML comments are scaffolding, not release notes, and scaffold-only |
| 227 | +fragments require the explicit option even on the first release. Commit the |
| 228 | +changed `CHANGES.md`, the removed fragments, and the updated `changes.d/next` |
| 229 | +together. |
| 230 | + |
| 231 | + |
| 232 | +Publishing release notes |
| 233 | +------------------------ |
| 234 | + |
| 235 | +After the release commit, `sacho show` prints one frozen section: |
| 236 | + |
| 237 | +~~~~ sh |
| 238 | +sacho show 1.2.0 |
| 239 | +sacho show 1.2.0 --skip-heading --no-word-wrap --output-file release-notes.md |
| 240 | +~~~~ |
| 241 | + |
| 242 | +A tag-triggered GitHub Actions job can hand that file to the GitHub CLI: |
| 243 | + |
| 244 | +~~~~ sh |
| 245 | +version="${GITHUB_REF_NAME#v}" |
| 246 | +sacho show "$version" --skip-heading --no-word-wrap --output-file release-notes.md |
| 247 | +gh release create "$GITHUB_REF_NAME" --notes-file release-notes.md |
| 248 | +~~~~ |
| 249 | + |
| 250 | +`show` reads only released sections; it never compiles current fragments. |
| 251 | + |
| 252 | + |
| 253 | +Enforcing coverage in CI and hooks |
| 254 | +---------------------------------- |
| 255 | + |
| 256 | +Configure the paths whose changes require a fragment, then let a machine catch |
| 257 | +the omissions reviewers miss: |
| 258 | + |
| 259 | +~~~~ toml |
| 260 | +[check] |
| 261 | +paths = ["src/**", "packages/**"] |
| 262 | +~~~~ |
| 263 | + |
| 264 | +Locally, `sacho check --staged` inspects the Git index; |
| 265 | +`sacho init --install-hook` installs a pre-commit hook chain. In CI, check a |
| 266 | +branch commit-by-commit against its base: |
| 267 | + |
| 268 | +~~~~ sh |
| 269 | +sacho check --base origin/main |
| 270 | +~~~~ |
| 271 | + |
| 272 | +Make sure the checkout history includes the base revision; a shallow clone that |
| 273 | +omits it cannot support the comparison. A commit that genuinely needs no entry |
| 274 | +opts out with a `Changelog: none` trailer in its message (or the |
| 275 | +`[skip changelog]` family of substrings). The exemption applies only to the |
| 276 | +commit that carries it. Details in `guide/ci-and-hooks.md`. |
| 277 | + |
| 278 | + |
| 279 | +Forward-porting a bugfix release across maintenance branches |
| 280 | +------------------------------------------------------------ |
| 281 | + |
| 282 | +The scenario: a fix lands on `1.1-maintenance`, `1.1.5` ships, and the fix needs |
| 283 | +to reach `1.2-maintenance` and `main`. Two paths, and which one you are on |
| 284 | +decides the work. |
| 285 | + |
| 286 | +Merge before releasing, and there is nothing to do. If you merge |
| 287 | +`1.1-maintenance` into `1.2-maintenance` while the fix's fragment still exists, |
| 288 | +the VCS carries the fragment along, and each branch later consumes its own copy. |
| 289 | +Prefer this ordering when you can; it needs no Sacho command. |
| 290 | + |
| 291 | +Merge the release tag, and the fragment arrives already consumed. The incoming |
| 292 | +`1.1.5` release commit deleted the fragment, so the merge brings only the frozen |
| 293 | +`1.1.5` section. The merge driver inserts that section at its version position |
| 294 | +in `CHANGES.md`, keeps the receiving branch's `changes.d/next`, and prints a |
| 295 | +`sacho carry` hint on stderr. Now it is policy, not mechanics: if the receiving |
| 296 | +branch's own next release should also list the fix, run `sacho carry 1.1.5` to |
| 297 | +decompile that section back into `carried-from-1.1.5.md` fragments, edit them |
| 298 | +if this branch presents the change differently, and treat them as ordinary |
| 299 | +unreleased entries. If the imported `1.1.5` section is enough, do nothing. |
| 300 | + |
| 301 | +`carry` is idempotent by design: it always writes the same |
| 302 | +`carried-from-<version>.md` name and overwrites any file already there. That is |
| 303 | +safe on a fresh carry, but if you have already edited those carried fragments, |
| 304 | +re-running `carry` discards your edits, so check for existing |
| 305 | +`carried-from-*.md` files before repeating it. |
| 306 | + |
| 307 | +Under Jujutsu there is no per-path merge driver, so after resolving a concurrent |
| 308 | +fragment merge run `sacho sync --force` and inspect the result. |
| 309 | +`guide/releases.md` and `guide/version-control.md` cover both. |
| 310 | + |
| 311 | + |
| 312 | +When something goes wrong |
| 313 | +------------------------- |
| 314 | + |
| 315 | +`sacho check` reports across three layers: fragment validity and formatting, |
| 316 | +agreement between the materialized unreleased region and the fragments, and |
| 317 | +fragment coverage for changed paths. |
| 318 | + |
| 319 | +A formatting or drift failure is mechanically repairable. `sacho check --fix` |
| 320 | +runs `fmt` and re-syncs the generated region; it will not write prose. If |
| 321 | +`CHANGES.md` was hand-edited, remember that fragments are the source of truth: |
| 322 | +move the edited text into a fragment, then `sacho sync`. Sacho asks before |
| 323 | +discarding hand edits, and `sacho sync --force` skips that prompt, so read the |
| 324 | +diff first. |
| 325 | + |
| 326 | +A missing-fragment failure names the affected section and suggests `sacho add`. |
| 327 | +Either write the entry or, if the commit truly has no user-visible effect, use |
| 328 | +the `Changelog: none` exemption. For anything stranger, fetch |
| 329 | +`troubleshooting.md`. |
0 commit comments