deploy: the compose file names its own release, and the cut moves it with the version
Gates / frontend (push) Successful in 2m41s
Gates / test (push) Successful in 3m2s
Gates / test-aarch64 (push) Successful in 8m23s
Gates / package (push) Successful in 4m29s
Gates / container (push) Successful in 17s
CI / gates (push) Successful in 16m14s

This commit is contained in:
2026-10-20 11:46:13 +02:00
parent ea9f16a989
commit aa8c9e28af
7 changed files with 231 additions and 71 deletions
+1
View File
@@ -18,6 +18,7 @@ Sections are written by hand. Nothing here is generated from commit messages: th
### Changed ### Changed
- **The compose file names its own release.** `deploy/docker/compose.yaml` defaults its image to the release it is tagged with instead of `:latest`, so `docker compose up -d` with the file from a tag runs that tag's image, and the install and upgrade steps no longer set `NXDNS_VERSION`. The variable still picks another release, and `NXDNS_IMAGE` still names a local image. Compose files from `v0.0.22` and earlier keep the `:latest` default.
- **A DoH upstream that answers with an error status says which.** The Diagnostics episode and `/api/health` read `HttpStatus (status 505 HTTP Version Not Supported)` instead of `HttpStatus (cause HttpStatus)`. A 505 is a server that requires HTTP/2, which nxdns does not speak. - **A DoH upstream that answers with an error status says which.** The Diagnostics episode and `/api/health` read `HttpStatus (status 505 HTTP Version Not Supported)` instead of `HttpStatus (cause HttpStatus)`. A 505 is a server that requires HTTP/2, which nxdns does not speak.
## [0.0.22] - 2026-10-19 ## [0.0.22] - 2026-10-19
+2 -2
View File
@@ -44,10 +44,10 @@ cat > etc-nxdns/config.zon <<'EOF'
.web = .{ .password = "choose-a-real-password" }, .web = .{ .password = "choose-a-real-password" },
} }
EOF EOF
NXDNS_VERSION=<version> docker compose up -d docker compose up -d
``` ```
The compose file defaults to `:latest`; pin a version for anything you intend to keep running. To run an image you built yourself, name it — `NXDNS_IMAGE=nxdns docker compose up -d` — as [docs/how-to/install-with-docker.md](docs/how-to/install-with-docker.md) describes. DNS is on port 53, the web UI on <http://localhost:8080>. The compose file names the release it belongs to, so a checkout of a release tag runs that release's image. `NXDNS_VERSION=<version> docker compose up -d` runs another release. To run an image you built yourself, name it — `NXDNS_IMAGE=nxdns docker compose up -d` — as [docs/how-to/install-with-docker.md](docs/how-to/install-with-docker.md) describes. DNS is on port 53, the web UI on <http://localhost:8080>.
The compose file runs `nxdns run --config=/etc/nxdns/config.zon`, which makes that file the configuration: every start reconciles the database onto it, and the UI refuses configuration edits. Edit the file and restart to change anything. Drop the `command:` line to run bare `nxdns run` instead, where the database is the configuration and changes go through the UI, the API, or `nxdns export` / `nxdns import` — the packaged systemd unit does that. Which mode is live is printed at every start (`authority: database` / `authority: file (<path>)`); see [docs/explanation/configuration-model.md](docs/explanation/configuration-model.md). The compose file runs `nxdns run --config=/etc/nxdns/config.zon`, which makes that file the configuration: every start reconciles the database onto it, and the UI refuses configuration edits. Edit the file and restart to change anything. Drop the `command:` line to run bare `nxdns run` instead, where the database is the configuration and changes go through the UI, the API, or `nxdns export` / `nxdns import` — the packaged systemd unit does that. Which mode is live is printed at every start (`authority: database` / `authority: file (<path>)`); see [docs/explanation/configuration-model.md](docs/explanation/configuration-model.md).
+6 -5
View File
@@ -1,10 +1,11 @@
services: services:
nxdns: nxdns:
# Published multi-architecture image. Pin an exact version in production: # Published multi-architecture image. The default is the release this file
# NXDNS_VERSION=0.0.1 docker compose up -d. To run a locally built image # is tagged with: `zig build cut` moves it in the bump commit, so the file
# instead, build it first (see deploy/docker/Dockerfile) and set # from a tag runs that tag's image. NXDNS_VERSION picks another release. To
# NXDNS_IMAGE=nxdns. # run a locally built image instead, build it first (see
image: ${NXDNS_IMAGE:-git.mial.net/mokhtar/nxdns:${NXDNS_VERSION:-latest}} # deploy/docker/Dockerfile) and set NXDNS_IMAGE=nxdns.
image: ${NXDNS_IMAGE:-git.mial.net/mokhtar/nxdns:${NXDNS_VERSION:-0.0.22}}
restart: unless-stopped restart: unless-stopped
# `run --config` makes the file the sole source of configuration: nxdns # `run --config` makes the file the sole source of configuration: nxdns
# reconciles the database onto ./etc-nxdns/config.zon at every start, and # reconciles the database onto ./etc-nxdns/config.zon at every start, and
+7 -7
View File
@@ -132,17 +132,17 @@ chmod 0600 deploy/docker/etc-nxdns/config.zon
## 3. Run it ## 3. Run it
```sh ```sh
NXDNS_VERSION=$VERSION docker compose -f deploy/docker/compose.yaml up -d docker compose -f deploy/docker/compose.yaml up -d
docker compose -f deploy/docker/compose.yaml logs -f docker compose -f deploy/docker/compose.yaml logs -f
``` ```
`compose.yaml` reads the image from two variables: `${NXDNS_IMAGE:-git.mial.net/mokhtar/nxdns:${NXDNS_VERSION:-latest}}`. Set `NXDNS_VERSION` to pin a release; set `NXDNS_IMAGE` to run something else entirely, which is what the build-from-source section at the bottom does. `compose.yaml` names the release it was tagged with: its image is `${NXDNS_IMAGE:-git.mial.net/mokhtar/nxdns:${NXDNS_VERSION:-<release>}}`, and `zig build cut` writes `<release>` in the same commit that bumps `build.zig.zon`, so the file from the `v$VERSION` tag runs `$VERSION` with no variable set. Set `NXDNS_VERSION` to run a different release with the same file; set `NXDNS_IMAGE` to run something else entirely, which is what the build-from-source section at the bottom does. Compose files from `v0.0.22` and earlier default to `:latest` instead, so with one of those, set `NXDNS_VERSION=$VERSION` on every command.
> Verified on this host with `docker compose -f deploy/docker/compose.yaml > Verified on 2026-10-20 with `docker compose -f deploy/docker/compose.yaml
> config`, which resolves the variables without contacting a registry: no > config` on a tree whose `build.zig.zon` declares `0.0.22`, which resolves
> variables gives `git.mial.net/mokhtar/nxdns:latest`, `NXDNS_VERSION=0.0.1` > the variables without contacting a registry: no variables gives
> gives `git.mial.net/mokhtar/nxdns:0.0.1`, and `NXDNS_IMAGE=nxdns` gives > `git.mial.net/mokhtar/nxdns:0.0.22`, `NXDNS_VERSION=0.0.21` gives
> `nxdns`. > `git.mial.net/mokhtar/nxdns:0.0.21`, and `NXDNS_IMAGE=nxdns` gives `nxdns`.
Every block on this page runs from the repository root, and none of them change directory, so they can be pasted in order. `-f` is what makes that work: Compose resolves the relative paths inside `compose.yaml` — the `etc-nxdns` bind mount — against the directory holding the file, not against your shell, and it takes the project name `docker` from that directory either way, which is why the container is `docker-nxdns-1`. Every block on this page runs from the repository root, and none of them change directory, so they can be pasted in order. `-f` is what makes that work: Compose resolves the relative paths inside `compose.yaml` — the `etc-nxdns` bind mount — against the directory holding the file, not against your shell, and it takes the project name `docker` from that directory either way, which is why the container is `docker-nxdns-1`.
+25 -9
View File
@@ -168,20 +168,36 @@ journalctl -u nxdns -f
### Docker ### Docker
The compose file names the release it belongs to, so upgrading the image is upgrading the file. A new release's compose file can change more than the image — the `command:` line or a port — so both ways below replace yours: if you edited it by hand, carry your edits over to the new one. To keep your own file as it is, skip the `curl` or the `git checkout` and set `NXDNS_VERSION=$VERSION` on the `pull` and `up` lines instead: it overrides the release the file names. A compose file from `v0.0.22` or earlier defaults to `:latest`, so with one of those the variable is needed on both lines, or `pull` and `up` can land on different images if a release happens between them.
With the single downloaded file of [Install with Docker](install-with-docker.md), fetch the new release's copy over it in its directory, then pull and recreate:
```sh ```sh
NXDNS_VERSION=$VERSION docker compose -f deploy/docker/compose.yaml pull cd ~/nxdns
NXDNS_VERSION=$VERSION docker compose -f deploy/docker/compose.yaml up -d curl -fLO "$BASE/raw/tag/v$VERSION/deploy/docker/compose.yaml"
docker compose pull
docker compose up -d
```
With a checkout of the repository, fetch the tag and move to it, from the repository root:
```sh
git fetch origin tag "v$VERSION"
git checkout "v$VERSION"
docker compose -f deploy/docker/compose.yaml pull
docker compose -f deploy/docker/compose.yaml up -d
``` ```
Compose recreates the container against the same `nxdns-data` volume. What happens to the file in `etc-nxdns` depends on the `command:` in your compose file: with the shipped `run --config=/etc/nxdns/config.zon` the file is the configuration and the restart reconciles onto it; without it, the database in the volume is the configuration and the file is read by nothing. Compose recreates the container against the same `nxdns-data` volume. What happens to the file in `etc-nxdns` depends on the `command:` in your compose file: with the shipped `run --config=/etc/nxdns/config.zon` the file is the configuration and the restart reconciles onto it; without it, the database in the volume is the configuration and the file is read by nothing.
Set `NXDNS_VERSION` on both lines, or export it. Without it the compose file falls back to `:latest`, and `pull` and `up` could then land on different images if a release happens between them. > Not run on this host: `pull` and `up` were not run against a new release.
> What was run on 2026-10-20 is `docker compose -f deploy/docker/compose.yaml
> Not run on this host: `pull` needs a published image, and there is none. > config` on a tree whose `build.zig.zon` declares `0.0.22`, which resolves
> What was run is `docker compose -f deploy/docker/compose.yaml config`, which > the variables without contacting a registry: no variable gave
> resolves the variables without contacting a registry: `NXDNS_VERSION=0.0.1` > `image: git.mial.net/mokhtar/nxdns:0.0.22`, and `NXDNS_VERSION=0.0.21` gave
> gave `image: git.mial.net/mokhtar/nxdns:0.0.1`, and no variable at all gave > `image: git.mial.net/mokhtar/nxdns:0.0.21`.
> `:latest`. > The fetch half was run the same day against `v0.0.22`: the `curl` URL
> returned 200, and `git fetch --dry-run origin tag v0.0.22` exited 0.
## 4. Confirm the upgrade ## 4. Confirm the upgrade
+10
View File
@@ -102,3 +102,13 @@ On a resumed cut — the bump commit exists and its tag does not — the pin sta
### Tests ### Tests
The `gates.yml` env parser against a fixture and against the real file; the environment builder, asserting the nine keys and nothing else; and the `sh` wrapper, asserting it applies the umask, keeps the real command as the direct child, and reports its exit code. The stage's remaining steps are process spawns of `npm`, `zig` and `nix`, which are not mocked — the same rule the rest of this file follows. The `gates.yml` env parser against a fixture and against the real file; the environment builder, asserting the nine keys and nothing else; and the `sh` wrapper, asserting it applies the umask, keeps the real command as the direct child, and reports its exit code. The stage's remaining steps are process spawns of `npm`, `zig` and `nix`, which are not mocked — the same rule the rest of this file follows.
## Addendum: the compose file names its release (post-0.0.22)
Owner question (2026-10-20), on the site's `NXDNS_VERSION=0.0.22 docker compose up -d`: "why do we need the NXDNS_VERSION to pull up a docker compose file? shouldnt the version be specified in the file already??" Until 0.0.22, `deploy/docker/compose.yaml` read `${NXDNS_VERSION:-latest}`, so the file fetched from a tag ran whatever `:latest` was that day, and every install and upgrade command had to carry the variable.
The image default is now the release itself: `${NXDNS_IMAGE:-git.mial.net/mokhtar/nxdns:${NXDNS_VERSION:-<version>}}`. `NXDNS_VERSION` and `NXDNS_IMAGE` stay as overrides.
- **The bump moves it.** This amends step 2 of the pin stage: the cut reads the compose file and computes its rewrite before anything is written, so a file without exactly one `${NXDNS_VERSION:-…}` default is a refusal on a clean tree. Then `build.zig.zon` and the compose file are both written atomically and read back. Step 5's commit takes `build.zig.zon`, `flake.nix` and `deploy/docker/compose.yaml`, and the working-tree diff is asserted to be a subset of those three paths.
- **One guard, on every commit.** A cut-tool test reads the real `build.zig.zon` and `deploy/docker/compose.yaml` and asserts that the default equals the declared version. It runs in `zig build test`, so a hand edit of either file fails CI on that commit. The tag's tree is the bump commit's tree, which passed CI before the tag was made, so `verify-dist` gets no second copy of the check.
- **The docs drop the variable from the plain flow.** README, install-with-docker and upgrade run `docker compose up -d`; the variables are documented as overrides, and the pages say that a compose file from `v0.0.22` or earlier still defaults to `:latest`.
+180 -48
View File
@@ -447,6 +447,45 @@ fn rewriteZonVersion(arena: Allocator, source: []const u8, version: []const u8)
}, 0); }, 0);
} }
/// The compose file a user fetches from a release tag. Its image default names
/// that release, so `docker compose up -d` from the tag runs the tag's image and
/// `NXDNS_VERSION` stays an override, never a requirement.
const compose_path = "deploy/docker/compose.yaml";
const compose_image_prefix = "image: ${NXDNS_IMAGE:-git.mial.net/mokhtar/nxdns:${NXDNS_VERSION:-";
/// The byte range of `<version>` in the image line
/// `image: ${NXDNS_IMAGE:-git.mial.net/mokhtar/nxdns:${NXDNS_VERSION:-<version>}}`.
/// Only a line that starts with that text, after its indentation, is the image:
/// the same text in a comment or after another key pins nothing. The file must
/// hold exactly one such line, or the rewrite would have to guess.
fn findComposeVersion(source: []const u8) error{ NoComposeVersion, ManyComposeVersions }!VersionSpan {
var found: ?VersionSpan = null;
var offset: usize = 0;
var lines = std.mem.splitScalar(u8, source, '\n');
while (lines.next()) |line| {
const line_start = offset;
offset += line.len + 1;
const indent = line.len - std.mem.trimStart(u8, line, " ").len;
if (!std.mem.startsWith(u8, line[indent..], compose_image_prefix)) continue;
if (found != null) return error.ManyComposeVersions;
const start = indent + compose_image_prefix.len;
const rest = std.mem.trimEnd(u8, line[start..], " \r");
if (rest.len <= "}}".len or !std.mem.endsWith(u8, rest, "}}")) return error.NoComposeVersion;
const version = rest[0 .. rest.len - "}}".len];
if (std.mem.indexOfAny(u8, version, "{}$ ") != null) return error.NoComposeVersion;
found = .{ .start = line_start + start, .end = line_start + start + version.len };
}
return found orelse error.NoComposeVersion;
}
fn rewriteComposeVersion(arena: Allocator, source: []const u8, version: []const u8) ![]u8 {
const at = try findComposeVersion(source);
return std.mem.concat(arena, u8, &.{ source[0..at.start], version, source[at.end..] });
}
/// The primary certificate fingerprint of a `git verify-tag --raw` VALIDSIG /// The primary certificate fingerprint of a `git verify-tag --raw` VALIDSIG
/// line: its LAST field. The line is /// line: its LAST field. The line is
/// ///
@@ -2243,9 +2282,9 @@ fn cut(ctx: *Ctx, kind_text: []const u8) !void {
const token = checked.authorization orelse return CheckFailed; const token = checked.authorization orelse return CheckFailed;
// The one commit this cut makes is assembled in three moves, in this order // The one commit this cut makes is assembled in three moves, in this order
// and no other: the manifest declares the version, the pin stage builds that // and no other: the manifest and the compose file declare the version, the
// version and writes its hashes into `flake.nix`, and the commit takes both // pin stage builds that version and writes its hashes into `flake.nix`, and
// files. `verify-dist` refuses a build whose `-Dversion-string` disagrees // the commit takes all three files. `verify-dist` refuses a build whose `-Dversion-string` disagrees
// with the manifest, so the pins cannot be computed before the bump; and the // with the manifest, so the pins cannot be computed before the bump; and the
// pins belong to the commit, so the commit cannot be made before the pins. // pins belong to the commit, so the commit cannot be made before the pins.
// Both of these run before the manifest is rewritten: they can refuse, and // Both of these run before the manifest is rewritten: they can refuse, and
@@ -2258,7 +2297,11 @@ fn cut(ctx: *Ctx, kind_text: []const u8) !void {
}; };
defer environ.deinit(); defer environ.deinit();
if (bump_needed) try writeZonVersion(ctx, version, zon_source); if (bump_needed) {
const compose = try composeWithVersion(ctx, version);
try writeZonVersion(ctx, version, zon_source);
try writeComposeVersion(ctx, version, compose);
}
try pinStage(ctx, version, &environ, zig_exe); try pinStage(ctx, version, &environ, zig_exe);
try commitBump(ctx, version, bump_needed); try commitBump(ctx, version, bump_needed);
@@ -2942,11 +2985,50 @@ fn verifyOriginTag(ctx: *Ctx, tag: []const u8, tag_ref: []const u8, commit: []co
ctx.pass("origin-tag", "{s} ({s}) on origin is this tool's own signed tag at {s}", .{ tag, object, commit }); ctx.pass("origin-tag", "{s} ({s}) on origin is this tool's own signed tag at {s}", .{ tag, object, commit });
} }
/// Rewrites `build.zig.zon` and commits it, and nothing else. /// The files the bump commit may touch, and the only ones. `flake.nix` joins
/// The two files the bump commit may touch, and the only two. `flake.nix` joins
/// `build.zig.zon` because the pin stage writes the release hashes into it /// `build.zig.zon` because the pin stage writes the release hashes into it
/// between the manifest write and the commit. /// between the manifest write and the commit; the compose file joins because
const bump_paths = [_][]const u8{ "build.zig.zon", "flake.nix" }; /// its image default is the release a user of the tag gets.
const bump_paths = [_][]const u8{ "build.zig.zon", "flake.nix", compose_path };
/// Replaces `path` with `bytes` atomically: a cut interrupted mid-write must not
/// leave a half-written file that neither zig, compose nor a re-run can parse.
fn replaceFile(ctx: *Ctx, path: []const u8, bytes: []const u8) !void {
var atomic = Io.Dir.cwd().createFileAtomic(ctx.io, path, .{ .replace = true }) catch |err| {
ctx.soft("bump", "cannot open {s} for an atomic replace: {t}", .{ path, err });
return CheckFailed;
};
defer atomic.deinit(ctx.io);
atomic.file.writeStreamingAll(ctx.io, bytes) catch |err| {
ctx.soft("bump", "cannot write {s}: {t}", .{ path, err });
return CheckFailed;
};
atomic.file.sync(ctx.io) catch |err| {
ctx.soft("bump", "cannot flush {s} to disk: {t}", .{ path, err });
return CheckFailed;
};
atomic.replace(ctx.io) catch |err| {
ctx.soft("bump", "cannot replace {s}: {t}", .{ path, err });
return CheckFailed;
};
}
/// Read back from disk rather than trusting the buffer: this is the file the
/// rest of the release reads, and a rewrite that produced something its grammar
/// rejects must be found now.
fn readBack(ctx: *Ctx, path: []const u8) ![:0]u8 {
return Io.Dir.cwd().readFileAllocOptions(
ctx.io,
path,
ctx.arena,
.limited(max_input_bytes),
.of(u8),
0,
) catch |err| {
ctx.soft("bump", "cannot read {s} back: {t}", .{ path, err });
return CheckFailed;
};
}
/// Writes the new version into `build.zig.zon` and proves the file still parses. /// Writes the new version into `build.zig.zon` and proves the file still parses.
/// ///
@@ -2959,41 +3041,9 @@ fn writeZonVersion(ctx: *Ctx, version: []const u8, source: [:0]const u8) !void {
ctx.soft("bump", "cannot rewrite the .version field of build.zig.zon: {t}", .{err}); ctx.soft("bump", "cannot rewrite the .version field of build.zig.zon: {t}", .{err});
return CheckFailed; return CheckFailed;
}; };
try replaceFile(ctx, "build.zig.zon", rewritten);
// Atomic: a cut interrupted mid-write must not leave a half-written const written = try readBack(ctx, "build.zig.zon");
// manifest that neither zig nor a re-run can parse.
var atomic = Io.Dir.cwd().createFileAtomic(ctx.io, "build.zig.zon", .{ .replace = true }) catch |err| {
ctx.soft("bump", "cannot open build.zig.zon for an atomic replace: {t}", .{err});
return CheckFailed;
};
defer atomic.deinit(ctx.io);
atomic.file.writeStreamingAll(ctx.io, rewritten) catch |err| {
ctx.soft("bump", "cannot write build.zig.zon: {t}", .{err});
return CheckFailed;
};
atomic.file.sync(ctx.io) catch |err| {
ctx.soft("bump", "cannot flush build.zig.zon to disk: {t}", .{err});
return CheckFailed;
};
atomic.replace(ctx.io) catch |err| {
ctx.soft("bump", "cannot replace build.zig.zon: {t}", .{err});
return CheckFailed;
};
// Read back from disk rather than trusting the buffer: this is the file the
// rest of the release reads, and a rewrite that produced something the zon
// grammar rejects must be found now.
const written = Io.Dir.cwd().readFileAllocOptions(
ctx.io,
"build.zig.zon",
ctx.arena,
.limited(max_input_bytes),
.of(u8),
0,
) catch |err| {
ctx.soft("bump", "cannot read build.zig.zon back: {t}", .{err});
return CheckFailed;
};
const reparsed = parseZonVersion(ctx.arena, written) catch |err| { const reparsed = parseZonVersion(ctx.arena, written) catch |err| {
ctx.soft("bump", "the rewritten build.zig.zon does not parse: {t}", .{err}); ctx.soft("bump", "the rewritten build.zig.zon does not parse: {t}", .{err});
return CheckFailed; return CheckFailed;
@@ -3005,8 +3055,39 @@ fn writeZonVersion(ctx: *Ctx, version: []const u8, source: [:0]const u8) !void {
ctx.pass("bump", "build.zig.zon declares {s}", .{version}); ctx.pass("bump", "build.zig.zon declares {s}", .{version});
} }
/// Commits the manifest bump and the pins as one commit, after asserting the /// The compose file with its image default moved to `version`, computed before
/// working tree holds those two files and nothing else. /// anything is written: a compose file the rewrite cannot read is a refusal
/// while the tree is still clean.
fn composeWithVersion(ctx: *Ctx, version: []const u8) ![]u8 {
const source = Io.Dir.cwd().readFileAlloc(ctx.io, compose_path, ctx.arena, .limited(max_input_bytes)) catch |err| {
ctx.soft("bump", "cannot read {s}: {t}", .{ compose_path, err });
return CheckFailed;
};
return rewriteComposeVersion(ctx.arena, source, version) catch |err| {
ctx.soft("bump", "cannot rewrite the image default of {s}: {t}", .{ compose_path, err });
return CheckFailed;
};
}
fn writeComposeVersion(ctx: *Ctx, version: []const u8, rewritten: []const u8) !void {
try replaceFile(ctx, compose_path, rewritten);
const written = try readBack(ctx, compose_path);
const at = findComposeVersion(written) catch |err| {
ctx.soft("bump", "the rewritten {s} has no single image default: {t}", .{ compose_path, err });
return CheckFailed;
};
if (!std.mem.eql(u8, written[at.start..at.end], version)) {
ctx.soft("bump", "the rewritten {s} defaults to '{s}', expected '{s}'", .{
compose_path, written[at.start..at.end], version,
});
return CheckFailed;
}
ctx.pass("bump", "{s} defaults to {s}", .{ compose_path, version });
}
/// Commits the manifest bump, the compose default and the pins as one commit,
/// after asserting the working tree holds those files and nothing else.
/// ///
/// `bump_needed` is false on a resumed cut: the commit already exists, so the /// `bump_needed` is false on a resumed cut: the commit already exists, so the
/// pin stage has just recomputed pins that are already in it. An empty diff is /// pin stage has just recomputed pins that are already in it. An empty diff is
@@ -3036,8 +3117,8 @@ fn commitBump(ctx: *Ctx, version: []const u8, bump_needed: bool) !void {
for (bump_paths) |allowed| { for (bump_paths) |allowed| {
if (std.mem.eql(u8, path, allowed)) break; if (std.mem.eql(u8, path, allowed)) break;
} else { } else {
ctx.soft("bump", "the cut changed '{s}'; the bump commit is {s} and {s} and nothing else", .{ ctx.soft("bump", "the cut changed '{s}'; the bump commit is {s}, {s} and {s} and nothing else", .{
path, bump_paths[0], bump_paths[1], path, bump_paths[0], bump_paths[1], bump_paths[2],
}); });
return CheckFailed; return CheckFailed;
} }
@@ -3067,9 +3148,10 @@ fn commitBump(ctx: *Ctx, version: []const u8, bump_needed: bool) !void {
"-S", "-m", "-S", "-m",
ctx.fmt("build: bump version to {s}", .{version}), "--", ctx.fmt("build: bump version to {s}", .{version}), "--",
bump_paths[0], bump_paths[1], bump_paths[0], bump_paths[1],
bump_paths[2],
}); });
ctx.pass("bump", "one commit: {s} declares {s} and {s} pins its release hashes", .{ ctx.pass("bump", "one commit: {s} declares {s}, {s} defaults to it and {s} pins its release hashes", .{
bump_paths[0], version, bump_paths[1], bump_paths[0], version, bump_paths[2], bump_paths[1],
}); });
} }
@@ -3698,6 +3780,56 @@ test "the zon version survives a rewrite round trip" {
try testing.expectError(error.NoVersionField, findVersionValue("// .version = \"9.9.9\"")); try testing.expectError(error.NoVersionField, findVersionValue("// .version = \"9.9.9\""));
} }
test "the compose image default survives a rewrite round trip" {
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const source =
\\services:
\\ nxdns:
\\ # NXDNS_VERSION=0.0.1 docker compose up -d runs another release.
\\ image: ${NXDNS_IMAGE:-git.mial.net/mokhtar/nxdns:${NXDNS_VERSION:-0.0.7}}
\\ restart: unless-stopped
;
const at = try findComposeVersion(source);
try testing.expectEqualStrings("0.0.7", source[at.start..at.end]);
const rewritten = try rewriteComposeVersion(arena, source, "0.0.8");
const moved = try findComposeVersion(rewritten);
try testing.expectEqualStrings("0.0.8", rewritten[moved.start..moved.end]);
// Only the default moved: the comment naming another version and the
// outer `NXDNS_IMAGE` default are untouched.
try testing.expect(std.mem.indexOf(u8, rewritten, "# NXDNS_VERSION=0.0.1 docker") != null);
try testing.expect(std.mem.indexOf(u8, rewritten, "${NXDNS_IMAGE:-git.mial.net/mokhtar/nxdns:") != null);
try testing.expectEqual(source.len, rewritten.len);
const image = " image: ${NXDNS_IMAGE:-git.mial.net/mokhtar/nxdns:${NXDNS_VERSION:-";
try testing.expectError(error.NoComposeVersion, findComposeVersion(" image: nxdns:latest"));
try testing.expectError(error.NoComposeVersion, findComposeVersion(image ++ "}}"));
try testing.expectError(error.NoComposeVersion, findComposeVersion(image ++ "0.0.7}"));
try testing.expectError(error.NoComposeVersion, findComposeVersion(image ++ "${OTHER}}}"));
// The text in a comment, or after any other key, is not the image.
try testing.expectError(error.NoComposeVersion, findComposeVersion(" # " ++ image[4..] ++ "0.0.7}}"));
try testing.expectError(error.NoComposeVersion, findComposeVersion(" x-image: ${NXDNS_VERSION:-0.0.7}"));
try testing.expectError(error.ManyComposeVersions, findComposeVersion(image ++ "0.0.7}}\n" ++ image ++ "0.0.7}}"));
}
test "this repository's compose file defaults to the version build.zig.zon declares" {
// The bump commit moves both together; this catches a hand edit of either
// between two cuts, on every CI run rather than at the next release.
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
defer threaded.deinit();
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const zon = try Io.Dir.cwd().readFileAllocOptions(threaded.io(), "build.zig.zon", arena, .limited(max_input_bytes), .of(u8), 0);
const compose = try Io.Dir.cwd().readFileAlloc(threaded.io(), compose_path, arena, .limited(max_input_bytes));
const at = try findComposeVersion(compose);
try testing.expectEqualStrings(try parseZonVersion(arena, zon), compose[at.start..at.end]);
}
test "the changelog section must exist, be dated and say something" { test "the changelog section must exist, be dated and say something" {
const good = const good =
\\# Changelog \\# Changelog