# Publish a changelog for your app





Any Pear application can ship a `CHANGELOG.md`. Stage it with the rest of your project and your users—or any third party—read your release notes straight from the app drive with [`pear changelog pear://<link>`](/pear/reference/pear/cli#pear-changelog), semver-filtered and pretty-printed. It is the same command that prints the Pear platform's own changelog when you run [`pear changelog`](/pear/reference/pear/cli#pear-changelog) with no link (a known regression makes the no-link form error on some 3.0.x builds—pass an explicit `pear://<link>`, as this guide does throughout).

The changelog lives in the app's [Hyperdrive](/p2p/reference/building-blocks/hyperdrive), so it is versioned, replicated, and readable peer-to-peer with no server and no separate publishing step—it travels with every stage and provision.

<Callout type="info">
  This is your **application's own** changelog. It is distinct from the docs-team [Release Overview](/pear/release-overview), which is a curated view of changes across Pear and its modules. To publish release notes for *your* app, follow this guide.
</Callout>

<Callout type="info">
  **Need the `pear` CLI?** Install it from **[install.pears.com](https://install.pears.com)**, or prefix any command below with `npx`. See [Install & upgrade](/pear/reference/pear/cli#install) for details.
</Callout>

## How it works [#how-it-works]

`pear changelog` reads a single file at the **root of your app drive**: `/CHANGELOG.md`. Under the hood the [`pear-changelog`](https://github.com/holepunchto/pear-changelog) module parses it into one entry per release, then the command filters by version and prints each entry's notes. The filename and drive-root location are the entire contract—there is no manifest field to set and nothing to register.

<Steps>
  <Step>
    ### Write `CHANGELOG.md` [#write-changelogmd]

    Create a `CHANGELOG.md` at your **project root** (next to `package.json`). The format is lightweight:

    * A header block at the top (typically a `# Title`)—everything before the first `##` heading is ignored by the parser. Don't start the file with a `##` heading: the parser only recognizes headings that follow a line, so the first release would be dropped.
    * One release per level-2 heading (`## <version>`), **newest first**.
    * The **first space-separated token of each heading must be a [SemVer](https://semver.org/) version** (a leading `v` is allowed and stripped). Everything under a heading until the next `##` is free-form Markdown.

    ```md file=<rootDir>/examples/how-to/operate-an-app/publish-a-changelog/CHANGELOG.md title="CHANGELOG.md" skip="example-import"
    # Acme Chat Changelog

    ## v1.2.0

    ### Features
    - Group threads with @mentions.

    ### Fixes
    - Reconnect automatically after the sidecar restarts.

    ## v1.1.0

    ### Features
    - Full-text message search.

    ## v1.0.0

    Initial release.
    ```

    <Callout type="warning">
      Keep the version as the **first word** of the heading, followed by a space. Bracketed [Keep a Changelog](https://keepachangelog.com/) headings such as `## [1.2.0] - 2026-01-01` aren't valid versions—those releases drop out of the default view and of any `--of` range (only `--full` still shows them). Use `## v1.2.0` or `## 1.2.0` instead. You can still add a date after the version, for example, `## v1.2.0 — 2026-01-01`.
    </Callout>
  </Step>

  <Step>
    ### Check the format locally [#check-the-format-locally]

    Before staging, confirm each release parses—with the same [`pear-changelog`](https://github.com/holepunchto/pear-changelog) module the CLI uses. This checks that headings are *recognized*; it does not apply the SemVer filter, so keep the version-format rules above in mind too. Install it as a dev dependency:

    ```sh example=publish-changelog step=setup
    npm install --save-dev pear-changelog
    ```

    Create `check-changelog.js` next to the changelog:

    ```js file=<rootDir>/examples/how-to/operate-an-app/publish-a-changelog/check-changelog.js title="check-changelog.js" skip="example-import"
    const { readFileSync } = require('node:fs')
    const { parse } = require('pear-changelog')

    for (const [version] of parse(readFileSync('CHANGELOG.md'))) {
      console.log('release:', version)
    }
    ```

    Run it with Node:

    ```sh example=publish-changelog step=run process=check expect="release: v1.0.0"
    node check-changelog.js
    ```

    It prints one line per release the parser found, newest first:

    ```text skip="sample-output"
    release: v1.2.0
    release: v1.1.0
    release: v1.0.0
    ```

    If a release is missing from the output, its heading didn't parse—`pear changelog` won't show it either. The reverse isn't guaranteed: a release can parse here yet still be filtered out by `pear changelog` when its version isn't valid SemVer (see the format warning above).
  </Step>

  <Step>
    ### Stage and seed it with your app [#stage-and-seed-it-with-your-app]

    A root `CHANGELOG.md` is staged like any other project file—no extra flags. Just make sure it is not excluded by a [`pear.stage.ignore`](/pear/reference/pear/configuration#pear-stage-ignore) entry or dropped by a [`pear.stage.only`](/pear/reference/pear/configuration#pear-stage-only) filter that omits it.

    <Callout type="warning">
      The deployment directory that [`pear build`](/pear/reference/pear/cli#pear-build) assembles—desktop or mobile—contains only `package.json`, `pear.json` (mobile), and `by-arch/`: your project's `CHANGELOG.md` is not copied in either way. Add it before staging: `cp CHANGELOG.md ../<name>-<version>/` on desktop, or `cp CHANGELOG.md dist/` for a mobile [OTA payload](/pear/how-to/operate-an-app/manual-deployment/mobile#build-the-ota-payload-steps-34).
    </Callout>

    ```bash skip="release-flow"
    pear stage --dry-run pear://<link>   # confirm CHANGELOG.md appears in the diff
    pear stage pear://<link>
    ```

    Then [`pear seed`](/pear/reference/pear/cli#pear-seed) the link so peers can fetch it. Once staged, the file is retrievable at `pear://<link>/CHANGELOG.md`. Bump `CHANGELOG.md` in the same commit as your version bump so each release carries its own notes—see [Ship your app](/p2p/getting-started/build-a-peer-to-peer-chat/ship) for the full stage → provision flow.
  </Step>

  <Step>
    ### Read it back [#read-it-back]

    Anyone with the link reads your changelog with:

    ```bash skip="live-swarm"
    pear changelog pear://<link>
    ```

    By default this prints up to the **10 newest releases within the current major version**, each with its full notes, separated by a divider. Tune it with the flags:

    | Flag              | Effect                                                                                                                               |
    | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
    | `--of <semver>`   | Filter to a version range, for example,  `--of 1.x.x` or `--of '>=1.1.0 <2.0.0'`. Defaults to the latest major.                      |
    | `--max`, `-m <n>` | Cap the number of entries shown (default `10`).                                                                                      |
    | `--full`          | Show every release (overrides `--max`); without `--of`, it also widens the range to all majors.                                      |
    | `--json`          | Emit newline-delimited JSON—one tagged object per release plus a final status object. Filter on `"tag": "changelog"` when scripting. |

    ```bash skip="live-swarm"
    pear changelog pear://<link> --of 1.x.x     # only the 1.x releases
    pear changelog pear://<link> --full         # the entire history
    pear changelog pear://<link> --json         # machine-readable
    ```
  </Step>
</Steps>

## Use it for release announcements [#use-it-for-release-announcements]

Because the changelog is queryable by version range, it doubles as your source of truth for release notes. When you cut a release, pull the exact entries for the range you shipped and paste them into your announcement:

```bash skip="live-swarm"
pear changelog pear://<link> --of '>=1.1.0'   # everything since 1.1.0
```

## Relationship to `pear dump` [#relationship-to-pear-dump]

`pear changelog` is the structured view of the same file that [`pear dump`](/pear/reference/pear/cli#pear-dump) returns raw:

```bash skip="live-swarm"
pear dump pear://<link>/CHANGELOG.md -   # raw Markdown, unparsed, to stdout
```

Use `pear dump` when you want the exact file bytes; use `pear changelog` when you want it parsed, semver-filtered, and formatted.

## See also [#see-also]

* [`pear changelog` reference](/pear/reference/pear/cli#pear-changelog)—the command and its flags.
* [`pear-changelog` module](https://github.com/holepunchto/pear-changelog)—the parser that defines the format, with a `diff()` API for comparing changelogs.
* [Ship your app](/p2p/getting-started/build-a-peer-to-peer-chat/ship)—the stage and provision flow the changelog travels with.
* [Deploy your application](/pear/how-to/operate-an-app/manual-deployment/deployment)—the full operator release flow.
* [Configuration](/pear/reference/pear/configuration#pear-stage)—`pear.stage` options that control what gets staged.
