Each service its own version: ipx, ipx-directory, the page (#207)

The workspace stops holding one version. ipx keeps 0.12.1-dev and its vX.Y.Z tags; the directory
starts at 0.1.0-dev, tagged directory-vX.Y.Z, and the page at 0.1.0-dev in package.json, tagged
web-vX.Y.Z, which web/build.mjs gives the script for the logo's tooltip beside ipx's. crates/feed
is a library, not released. CLAUDE.md's release steps are for one service at a time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 16:35:41 +00:00
parent fa949f467d
commit 38c1abba1d
14 changed files with 51 additions and 22 deletions

View File

@@ -266,18 +266,32 @@ say, such as what was wrong before or what it cost to find out, it goes in the c
body, where `git log` and `git blame` find it beside the change. (There was a long-form body, where `git log` and `git blame` find it beside the change. (There was a long-form
`docs/history.md` until 0.7.0; it grew too large to be useful and was removed. It is in git.) `docs/history.md` until 0.7.0; it grew too large to be useful and was removed. It is in git.)
Cutting a release, of the three together while they share one version: in each changelog, first read `[Unreleased]` against the last release, not line by line, and rewrite Each service has its own version and is released on its own, when its changelog has something to
it as what changed between the two. Lines go in as the work happens, so they record the way there: release:
a feature added and then reworked in the same release is one line saying what it is now, under
Added; something added and taken out again before the release, or a fix for a bug no release ever | service | version in | changelog | tag |
had, is not there at all; and "instead of" and "no longer" compare with the last release, not with |---|---|---|---|
last week. Issue numbers stay, all of them, on the line that survives. Then rename `[Unreleased]` to | ipx | `crates/ipx/Cargo.toml` | `crates/ipx/CHANGELOG.md` | `vX.Y.Z`, as every release before the split |
`## [X.Y.Z] - YYYY-MM-DD` and open a new empty `[Unreleased]` above it, set `version` in the root `Cargo.toml` (`[workspace.package]`) to `X.Y.Z` (dropping `-dev`), tag the commit | ipx-directory | `crates/directory/Cargo.toml` | `crates/directory/CHANGELOG.md` | `directory-vX.Y.Z` |
`vX.Y.Z`, and update the compare links at the bottom of each changelog. A service with nothing | ipx-web, the page | `package.json` | `web/CHANGELOG.md` | `web-vX.Y.Z` |
under `[Unreleased]` gets no entry for that release. Then, in the next commit,
set `version` to the next patch with `-dev` (after 0.9.0, `0.9.1-dev`), so a build between releases says so `crates/feed` is a library the two Rust services build in, not released (`0.0.0`); a change to it
in the logo's tooltip instead of claiming to be the last release. The release that follows can is a line in the changelog of each service it changes. The page shows ipx's version and its own in
still be a minor or major one; `-dev` only says the work comes after `X.Y.Z`. the logo's tooltip; the directory logs its own at start; each binary answers `--version`.
Cutting a release of one: first read its `[Unreleased]` against its last release, not line by
line, and rewrite it as what changed between the two. Lines go in as the work happens, so they
record the way there: a feature added and then reworked in the same release is one line saying
what it is now, under Added; something added and taken out again before the release, or a fix for
a bug no release ever had, is not there at all; and "instead of" and "no longer" compare with the
last release, not with last week. Issue numbers stay, all of them, on the line that survives. Then
rename `[Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD` and open a new empty `[Unreleased]` above it,
set the service's version to `X.Y.Z` (dropping `-dev`), tag the commit with the service's tag, and
update the compare links at the bottom of its changelog. Then, in the next commit, set its version
to the next patch with `-dev` (after 0.9.0, `0.9.1-dev`), so a build between releases says so
instead of claiming to be the last release. The release that follows can still be a minor or major
one; `-dev` only says the work comes after `X.Y.Z`. A service never released is at `0.1.0-dev`,
working towards its first release, `0.1.0`.
**Traces first, logs second.** Anything that does work (a service, a handler, a job, a call to **Traces first, logs second.** Anything that does work (a service, a handler, a job, a call to
another service) gets spans, and passes `traceparent` on to whatever it calls, so one trace in another service) gets spans, and passes `traceparent` on to whatever it calls, so one trace in

4
Cargo.lock generated
View File

@@ -1856,7 +1856,7 @@ dependencies = [
[[package]] [[package]]
name = "ipx-directory" name = "ipx-directory"
version = "0.12.1-dev" version = "0.1.0-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -1881,7 +1881,7 @@ dependencies = [
[[package]] [[package]]
name = "ipx-feed" name = "ipx-feed"
version = "0.12.1-dev" version = "0.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atom_syndication", "atom_syndication",

View File

@@ -2,9 +2,9 @@
members = ["crates/ipx", "crates/feed", "crates/directory"] members = ["crates/ipx", "crates/feed", "crates/directory"]
resolver = "3" resolver = "3"
# One version for the whole workspace: what a release sets, and what the logo's tooltip shows. # Each service has its own version, in its own Cargo.toml (ipx, ipx-directory) or package.json
# (the page), released and tagged on its own; see CLAUDE.md.
[workspace.package] [workspace.package]
version = "0.12.1-dev"
edition = "2024" edition = "2024"
[workspace.dependencies] [workspace.dependencies]

View File

@@ -3,6 +3,8 @@
The directory of public podcasts that ipx servers share (docs/directory.md). ipx's changelog is The directory of public podcasts that ipx servers share (docs/directory.md). ipx's changelog is
[crates/ipx/CHANGELOG.md](../ipx/CHANGELOG.md), the page's [web/CHANGELOG.md](../../web/CHANGELOG.md). [crates/ipx/CHANGELOG.md](../ipx/CHANGELOG.md), the page's [web/CHANGELOG.md](../../web/CHANGELOG.md).
Its version is in `crates/directory/Cargo.toml`, and a release is tagged `directory-vX.Y.Z`.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ipx-directory" name = "ipx-directory"
version.workspace = true version = "0.1.0-dev"
edition.workspace = true edition.workspace = true
[[bin]] [[bin]]

View File

@@ -98,7 +98,7 @@ async fn serve(path: &std::path::Path) -> Result<()> {
let dropped = dir.set_tokens(tokens).await?; let dropped = dir.set_tokens(tokens).await?;
let listener = tokio::net::TcpListener::bind(&cfg.bind).await.with_context(|| format!("listening on {}", cfg.bind))?; let listener = tokio::net::TcpListener::bind(&cfg.bind).await.with_context(|| format!("listening on {}", cfg.bind))?;
tracing::info!(bind = %cfg.bind, tokens = cfg.tokens.len(), dropped, "directory started"); tracing::info!(version = env!("CARGO_PKG_VERSION"), bind = %cfg.bind, tokens = cfg.tokens.len(), dropped, "directory started");
tokio::spawn(ipx_directory::refresher(dir.clone())); tokio::spawn(ipx_directory::refresher(dir.clone()));
tokio::spawn(ipx_directory::reloader(dir.clone(), path.to_owned())); tokio::spawn(ipx_directory::reloader(dir.clone(), path.to_owned()));
axum::serve(listener, ipx_directory::router(dir)) axum::serve(listener, ipx_directory::router(dir))

View File

@@ -1,6 +1,7 @@
[package] [package]
name = "ipx-feed" name = "ipx-feed"
version.workspace = true version = "0.0.0"
publish = false
edition.workspace = true edition.workspace = true
[dependencies] [dependencies]

View File

@@ -5,6 +5,8 @@ The daemon: the API, scans, downloads, DeadAir and the database. The page has it
[crates/directory/CHANGELOG.md](../directory/CHANGELOG.md). Until 0.12.0 this was the changelog of [crates/directory/CHANGELOG.md](../directory/CHANGELOG.md). Until 0.12.0 this was the changelog of
all of ipx, the page included; they were split with the services (#207). all of ipx, the page included; they were split with the services (#207).
Its version is in `crates/ipx/Cargo.toml`, and a release is tagged `vX.Y.Z`.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ipx" name = "ipx"
version.workspace = true version = "0.12.1-dev"
edition.workspace = true edition.workspace = true
[dependencies] [dependencies]

View File

@@ -1,5 +1,6 @@
{ {
"name": "ipx-web", "name": "ipx-web",
"version": "0.1.0-dev",
"private": true, "private": true,
"description": "Builds the ipx web pages from web/src (web/build.mjs, run by build.rs) and tests them. The Rust tests cover the server.", "description": "Builds the ipx web pages from web/src (web/build.mjs, run by build.rs) and tests them. The Rust tests cover the server.",
"scripts": { "scripts": {

View File

@@ -1444,6 +1444,8 @@ test('/api/status gives a dashboard the counts, with the shared token as a cooki
const s = await r.json(); const s = await r.json();
for (const k of ['feeds', 'pending', 'downloaded']) expect(typeof s[k], k).toBe('number'); for (const k of ['feeds', 'pending', 'downloaded']) expect(typeof s[k], k).toBe('number');
expect(s.version).toMatch(/^\d+\.\d+\.\d+/); expect(s.version).toMatch(/^\d+\.\d+\.\d+/);
// The logo names ipx's version and the page's, each its own (#207).
await expect(page.locator('#applogo')).toHaveAttribute('title', new RegExp(`^iPX ${s.version}, page \\d+\\.\\d+\\.\\d+`));
const none = await page.request.get('/api/status', { headers: { cookie: '' } }); const none = await page.request.get('/api/status', { headers: { cookie: '' } });
expect(none.status()).toBe(401); expect(none.status()).toBe(401);
}); });

View File

@@ -4,6 +4,8 @@ The page, and the container that serves it (Caddy). ipx's changelog is
[crates/ipx/CHANGELOG.md](../crates/ipx/CHANGELOG.md), which was the page's too until 0.12.0, and the [crates/ipx/CHANGELOG.md](../crates/ipx/CHANGELOG.md), which was the page's too until 0.12.0, and the
directory's [crates/directory/CHANGELOG.md](../crates/directory/CHANGELOG.md). directory's [crates/directory/CHANGELOG.md](../crates/directory/CHANGELOG.md).
Its version is in `package.json`, and a release is tagged `web-vX.Y.Z`.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
@@ -11,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- The page has a version of its own, which the logo's tooltip shows beside ipx's (#207).
- The page is served by its own container, ipx-web (Caddy), which passes the API to ipx on the same address, so a change to the page deploys without rebuilding ipx (#203). - The page is served by its own container, ipx-web (Caddy), which passes the API to ipx on the same address, so a change to the page deploys without rebuilding ipx (#203).
- Admin, Directory sets the directory's address and this server's token, and whether to share, and says how the last share went (#205). - Admin, Directory sets the directory's address and this server's token, and whether to share, and says how the last share went (#205).
- A feed's settings have a Private switch, which keeps it out of the directory; on and locked for a feed whose address or login shows it is private (#205). - A feed's settings have a Private switch, which keeps it out of the directory; on and locked for a feed whose address or login shows it is private (#205).

View File

@@ -17,6 +17,8 @@ import swc from '@swc/core';
import html from '@swc/html'; import html from '@swc/html';
const here = path.dirname(fileURLToPath(import.meta.url)); const here = path.dirname(fileURLToPath(import.meta.url));
// The page's own version (package.json), given to its script in place of __WEB_VERSION__.
const WEB_VERSION = JSON.parse(fs.readFileSync(path.join(here, '..', 'package.json'), 'utf8')).version;
// The files are one script, concatenated in this order, not modules: they share one top-level // The files are one script, concatenated in this order, not modules: they share one top-level
// scope, as the single inline script did, and code that runs at load needs what came before it. // scope, as the single inline script did, and code that runs at load needs what came before it.
@@ -57,7 +59,8 @@ export function buildStyle({ minify = true } = {}) {
export function buildPage(name, { minify = true } = {}) { export function buildPage(name, { minify = true } = {}) {
const { script, src: files } = PAGES[name]; const { script, src: files } = PAGES[name];
const src = files.map(f => fs.readFileSync(path.join(here, 'src', f + '.ts'), 'utf8')).join('\n') const src = files.map(f => fs.readFileSync(path.join(here, 'src', f + '.ts'), 'utf8')).join('\n')
.replaceAll('__HLS_JS__', HLS_URL); .replaceAll('__HLS_JS__', HLS_URL)
.replaceAll('__WEB_VERSION__', WEB_VERSION);
const js = swc.transformSync(src, { const js = swc.transformSync(src, {
filename: name + '.ts', filename: name + '.ts',
jsc: { jsc: {

View File

@@ -719,7 +719,8 @@ $('#signout').onclick=async()=>{ await api('/api/logout',{method:'POST'}); locat
api('/api/me').then(u=>{ api('/api/me').then(u=>{
S.me=u; S.me=u;
$('#who').textContent=u.name+(u.admin?' · admin':''); $('#who').textContent=u.name+(u.admin?' · admin':'');
$('#applogo').title=`iPX ${u.version}`; // ipx's version and the page's, each released on its own.
$('#applogo').title=`iPX ${u.version}, page __WEB_VERSION__`;
// Whether the link to Admin shows, now and before the first frame of the next visit. // Whether the link to Admin shows, now and before the first frame of the next visit.
document.documentElement.toggleAttribute('data-admin',!!u.admin); document.documentElement.toggleAttribute('data-admin',!!u.admin);
try{ localStorage.setItem('ipx.admin',u.admin?'1':'0'); }catch{} try{ localStorage.setItem('ipx.admin',u.admin?'1':'0'); }catch{}