Skip to main content

Crate cratefield_module_changelog

Crate cratefield_module_changelog 

Source
Expand description

cratefield-module-changelog: a project’s releases, mirrored into the venture’s own database and served over an API.

use cratefield_module_changelog::Changelog;

let module = Changelog::new()
    .repo("owner/name")
    .locale("en");

The read path never leaves the database. A refresh pulls GitHub (the Releases API, or a Keep-a-Changelog file) and stores each release — byte-for-byte from the Releases API; the CHANGELOG.md parser normalises line endings and trims section edges. GET / and GET /{version} then serve only what is stored, so GitHub being down, rate-limited or misconfigured costs a refresh, not the changelog. Before the first refresh the list is simply empty — a 200 with no releases, never an error and never an empty error page.

A refresh rewrites nothing it does not have to. Each fetch is compared against the stored rows field by field: a release upstream has not touched keeps its updated_at, and a fetch that saw the whole list alone may prune rows upstream deleted — a walk that hit the page cap applies what it fetched and prunes nothing. There is no LLM in this module — rewriting and translation arrive later behind a TextModel port that does not exist yet, and until then every response reports rendering.style = "original".

See README.md for the configuration keys and the response shapes.

§cratefield-module-changelog

A project’s release notes, mirrored into the venture’s own database and served over an API. A refresh pulls the releases of a GitHub repository (or a Keep-a-Changelog file in it) and stores each one — the author’s own markdown, byte-for-byte from the Releases API. The public routes then read the database, never GitHub: an outage, a rate limit or a misconfigured repository costs a refresh, not the changelog.

There is no LLM here. Rewriting and translation are a later addition behind a TextModel port that does not exist yet; until then every response reports rendering.style = "original" and serves the stored text as-is, and ?locale= / ?style= are accepted and select nothing.

§Routes

Mounted at /v1/changelog:

RouteAudienceWhat it does
GET /publicReleases, newest first. page (1-based, default 1), per_page (default 20, max 100), locale, style
GET /{version}publicOne release, 404 when the version is unknown
POST /admin/refreshadminPull from GitHub now (Authorization: Bearer <ADMIN_TOKEN>)

Before the first refresh the list is simply empty — 200 with "releases": [], never an error.

{
  "releases": [
    {
      "version": "v1.2.0",
      "title": "1.2.0",
      "body": "…original markdown…",
      "url": "https://github.com/owner/name/releases/tag/v1.2.0",
      "published_at": "2026-05-06T10:00:00Z",
      "prerelease": false,
      "rendering": { "style": "original", "locale": "en", "machine_generated": false }
    }
  ],
  "page": 1,
  "per_page": 20,
  "total": 37,
  "source": { "kind": "github-releases", "repo": "owner/name" },
  "refreshed_at": "2026-09-18T09:00:00Z"
}

§Configuration

Keys are SCREAMING_SNAKE, prefixed with the module name. All are optional except CHANGELOG_REPO; set them as Workers vars or secrets.

KeyDefaultMeaning
CHANGELOG_REPO— (required)owner/name of the repository to mirror
CHANGELOG_SOURCEgithub-releasesgithub-releases or changelog-md
CHANGELOG_PATHCHANGELOG.mdFile to read, for changelog-md
CHANGELOG_REFthe default branchGit ref for changelog-md
CHANGELOG_TOKENanonymousGitHub token; raises the rate limit
CHANGELOG_API_BASEhttps://api.github.comWhere GitHub’s API lives (tests, GitHub Enterprise)
CHANGELOG_INCLUDE_PRERELEASESfalseMirror prereleases too
CHANGELOG_INCLUDE_DRAFTSfalseMirror drafts too
CHANGELOG_LOCALEenThe locale the original notes are written in; reported in rendering
CHANGELOG_CACHE_TTL_SECONDS60The KeyValue read cache’s TTL; 0 disables the cache

A venture that only sets configuration and composes Changelog::new() is complete — everything above can be configuration alone. The same settings can be composed in code (.repo("owner/name"), .locale("en"), …); configuration wins when a key is set.

refresh lives at POST /v1/changelog/admin/refresh — the admin-audience action, behind Authorization: Bearer <ADMIN_TOKEN>. The /admin/ path prefix is a harness rule: core reserves admin-audience surface actions for /admin/* paths.

It answers with what the pull did:

{
  "ok": true,
  "fetched": 37,
  "inserted": 1,
  "updated": 2,
  "unchanged": 33,
  "skipped": 1,
  "removed": 0,
  "not_modified": false,
  "complete": true
}

complete says whether this fetch saw the whole source. It is false when the releases walk hit the module’s page cap (five pages of 100): everything the pages that did arrive held is still inserted and updated, but nothing is pruned — a capped walk cannot tell “upstream deleted it” from “it is on a page we did not reach”, so every stored row is kept, the refresh logs a warning, and the mirror waits for a walk that sees the end of the list to repair itself. A 304 Not Modified refresh is always complete: true: the shortcut is only ever taken on an etag a complete single-page fetch stored, so “not modified” certifies the whole mirror, not just page 1.

§What “verbatim” means

  • A github-releases body comes from the Releases API and is stored byte-for-byte: what GitHub returned is what is served.
  • The changelog-md source is parsed as text, not bytes: the file is split at ## headings, line endings are normalised to \n (a CRLF CHANGELOG.md is stored LF-normalised), and the whitespace at the edges of each section’s body is trimmed.
  • The split is textual all the way down: a ## heading inside a fenced code block is read as a section boundary, not as markdown. Keep ## out of fenced blocks in CHANGELOG.md.

§Storage

Two tables, and neither holds anything about a person:

  • changelog_release — one row per (source, version). body keeps the upstream text (byte-for-byte for github-releases; line-ending normalised and edge-trimmed for changelog-md); first_seen_at survives every update, so a refresh never churns a row it does not have to touch. Rows upstream deleted are pruned only after a fetch that saw the whole list — never after a walk that hit the page cap.
  • changelog_source — the etag, the cache generation and how the last refresh went. A failed refresh records its status and nothing else. The etag is stored only when the whole list fit one page: a page’s etag describes that page’s bytes, and a repository of more than one page could take an edit on a later page while page 1 stayed byte-identical, which a conditional request would then answer 304 to forever.

source is a stable identity string (github-releases:owner/name, changelog-md:owner/name:CHANGELOG.md@main), so changing configuration cannot silently mix two sources’ rows.

§Compose it

ⓘ
use cratefield_module_changelog::Changelog;

let module = Changelog::new().repo("owner/name");

Requires the Database, HttpClient and Clock ports; uses KeyValue when present to cache read responses (correctness never depends on it).


MIT. Built in the open for Cratefield, a Factory Zero venture.

Structs§

Changelog
A changelog module: mirror a project’s releases into the venture’s own database and serve them.

Enums§

SourceKind
Where the release notes come from. github-releases reads the Releases API (titles, bodies, prerelease and draft flags, an etag); changelog-md reads one Keep-a-Changelog file out of the repository and splits it into sections.