Author SHA1 Message Date
jcoffey-dev e223f7d327 Release 2026.9.24.3
ci / fork-checks (pull_request) Successful in 45s
ci / build (pull_request) Successful in 4m26s
2026.9.24.3 is 2026.9.24.2 plus the image build fix; 2026.9.24.2's tag never
published an image. Everything in 2026.9.24.2's notes applies.
2026-09-22 23:04:59 -07:00
jcoffey-dev 30df055e39 Image build: put the vendored crate where cargo chef cooks
#27 let the build context see vendor/, but the Dockerfile cooks the
dependencies before it copies the tree, from a recipe that carries only the
workspace's manifests. [patch.crates-io] points sieve-rs at vendor/, so the
cook failed the same way: failed to read /build/vendor/sieve-rs/Cargo.toml.
That's why 2026.9.24.2's publish failed. The builder stage now copies
vendor/ before cooking; a local build got past it into compiling the
dependencies.

context-check.py now also checks that each patched path is copied into the
cooking stage before the cook, and fails on the Dockerfile as it was.
2026-09-22 23:04:50 -07:00
jcoffey-dev 5393c4405a Merge pull request 'Release 2026.9.24.2' (#29) from release/2026.9.24.2 into main
ci / fork-checks (push) Successful in 50s
publish / version (push) Successful in 24s
publish / publish (push) Failing after 2m38s
publish / release (push) Skipped
publish / binaries (push) Skipped
ci / build (push) Successful in 22m35s
Reviewed-on: #29
2026-09-23 05:47:10 +00:00
jcoffey-dev cc532b914c Release 2026.9.24.2
ci / fork-checks (pull_request) Successful in 1m49s
ci / build (pull_request) Successful in 4m8s
Replaces 2026.9.24, whose tag predates the image build fix (#27) and never
published. Carries everything 2026.9.24 did -- upstream 0.16.23 and its
fixes, the scim release-profile fix -- and since then:

- identifiers renamed from the upstream name, with no aliases: the JMAP
  registry capability is urn:inbuxa:jmap:registry, WebDAV tokens
  urn:inbuxa:dav*, Sieve extensions vnd.inbuxa.*, the web interface client
  inbuxa-webui; INBUXA_* settings only. Deploy with admin and webmail
  releases that use the new names.
- the brand in lowercase where people see it.
- the spam filter rules bundled with the server; on first start they add
  the AI classifier's LLM_* scores.
- a Local AI page link in Settings › Spam Filter, for the admin release
  that draws it.
- two start-up migrations: the spam model moves to its renamed keys, and
  the web interface's old OAuth client is retired.
2026-09-22 22:40:17 -07:00
jcoffey-dev c09eff2214 Merge pull request 'Bundle the spam filter rules with the server, and link the Local AI page' (#28) from fork/bundled-spam-rules into main
ci / fork-checks (push) Successful in 20s
ci / build (push) Canceled after 7m16s
Reviewed-on: #28
2026-09-23 05:39:50 +00:00
jcoffey-dev d7c9416713 Merge pull request 'Let the image build see the dependency Cargo patches' (#27) from fix/vendor-in-build-context into main
ci / fork-checks (push) Successful in 14s
ci / build (push) Successful in 31m11s
2026-09-23 04:48:35 +00:00
jcoffey-dev 238079da66 Let the image build see the dependency Cargo patches
ci / fork-checks (pull_request) Successful in 49s
ci / build (pull_request) Successful in 4m22s
The rename pass vendored a patched sieve-rs and pointed Cargo.toml's
[patch.crates-io] at vendor/sieve-rs. .dockerignore ignores everything and
re-includes a short list that did not have vendor on it, so the image build
had no such directory and stopped at

    failed to load source for dependency `sieve-rs`
    failed to read /build/vendor/sieve-rs/Cargo.toml

CI could not have caught that: it builds from a checkout, where the
directory is simply there, and only the image build has a context to prune.
The first that was known about it was a tag that had already been pushed.

So: vendor is re-included, and tools/fork/context-check.py now asserts the
thing that was quietly assumed -- every path a [patch] section names exists
and survives .dockerignore. It runs beside the other fork checks and takes
no toolchain.

Also, the comments in .dockerignore started with // , which Docker does not
read as a comment: they were patterns that happened to match nothing. They
are # now.
2026-09-22 21:43:37 -07:00
5 changed files with 138 additions and 3 deletions
+8 -2
View File
@@ -1,10 +1,16 @@
// Ignore everything # Ignore everything
* *
// Allow what is needed # Allow what is needed
!crates !crates
!tests !tests
!resources !resources
# The patched dependency Cargo.toml's [patch.crates-io] points at. Without
# it the build context has no vendor/, and `cargo chef cook` fails on
# "failed to load source for dependency sieve-rs" -- which CI cannot see,
# because CI builds from a checkout and only the image build has a context.
!vendor
!Cargo.lock !Cargo.lock
!Cargo.toml !Cargo.toml
+5
View File
@@ -35,6 +35,11 @@ jobs:
- run: python3 tools/fork/name-check.py - run: python3 tools/fork/name-check.py
- if: always() - if: always()
run: python3 tools/fork/notice-check.py run: python3 tools/fork/notice-check.py
# Cargo can patch a dependency to a directory in this repository, and
# the image builds from a context .dockerignore prunes to almost
# nothing. CI never sees the difference; a release does.
- if: always()
run: python3 tools/fork/context-check.py
build: build:
# Either runner (host1 or host2): the build needs no docker socket. # Either runner (host1 or host2): the build needs no docker socket.
+4
View File
@@ -19,6 +19,10 @@ RUN export DEBIAN_FRONTEND=noninteractive && \
g++-x86-64-linux-gnu binutils-x86-64-linux-gnu g++-x86-64-linux-gnu binutils-x86-64-linux-gnu
RUN rustup target add "$(cat /target.txt)" RUN rustup target add "$(cat /target.txt)"
COPY --from=planner /recipe.json /recipe.json COPY --from=planner /recipe.json /recipe.json
# inbuxa: [patch.crates-io] points sieve-rs at vendor/, and the recipe only
# carries the workspace's own manifests, so cooking the dependencies needs the
# vendored crate itself (the context allows it since #27; this puts it here).
COPY vendor/ vendor/
RUN RUSTFLAGS="$(cat /flags.txt)" cargo chef cook --target "$(cat /target.txt)" --release --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" --recipe-path /recipe.json RUN RUSTFLAGS="$(cat /flags.txt)" cargo chef cook --target "$(cat /target.txt)" --release --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" --recipe-path /recipe.json
COPY . . COPY . .
RUN RUSTFLAGS="$(cat /flags.txt)" cargo build --target "$(cat /target.txt)" --release -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" RUN RUSTFLAGS="$(cat /flags.txt)" cargo build --target "$(cat /target.txt)" --release -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats"
+1 -1
View File
@@ -81,7 +81,7 @@ fn legacy_setting(name: &str, is_set: impl Fn(&str) -> bool) -> Option<String> {
#[macro_export] #[macro_export]
macro_rules! brand_version { macro_rules! brand_version {
() => { () => {
"2026.9.24" "2026.9.24.3"
}; };
} }
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Coffey Labs
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Every path Cargo patches has to be in the image's build context.
Cargo.toml's [patch.crates-io] can point at a directory in this repository,
and the Dockerfile builds from a context that .dockerignore prunes to almost
nothing. Those two facts met on 2026-09-23: a vendored, patched sieve-rs
landed, CI stayed green -- it builds from a checkout, where the directory is
simply there -- and the release build failed on
failed to load source for dependency `sieve-rs`
failed to read /build/vendor/sieve-rs/Cargo.toml
after a tag had already been pushed. This is seconds, and it runs beside the
other fork checks rather than waiting for a release to find out.
Being in the context isn't enough on its own: the Dockerfile cooks the
dependencies (`cargo chef cook`) before it copies the tree in, from a recipe
that carries only the workspace's manifests. So each patched path must also be
copied into that stage before the cook step, or the same error comes back
there -- as it did for 2026.9.24.2, the first tag after the context fix.
"""
import re
import sys
from pathlib import Path
root = Path(__file__).resolve().parents[2]
def patched_paths(manifest: Path) -> list[str]:
"""Directories named by a [patch...] section's `path = "..."` entries."""
out, in_patch = [], False
for line in manifest.read_text().splitlines():
stripped = line.strip()
if stripped.startswith("["):
in_patch = stripped.startswith("[patch")
continue
if not in_patch:
continue
m = re.search(r'path\s*=\s*"([^"]+)"', stripped)
if m:
out.append(m.group(1))
return out
def allowed(dockerignore: Path) -> set[str]:
"""The first path segment of every re-inclusion rule."""
keep = set()
for line in dockerignore.read_text().splitlines():
stripped = line.strip()
if stripped.startswith("!"):
keep.add(stripped[1:].strip("/").split("/")[0])
return keep
def copied_before_cook(dockerfile: Path) -> list[str] | None:
"""Sources COPY'd into the stage that runs `cargo chef cook`, before it.
None when no stage cooks. A `COPY . .` covers everything.
"""
stage: list[str] = []
for line in dockerfile.read_text().splitlines():
stripped = line.strip()
if re.match(r"(?i)^FROM\s", stripped):
stage = []
continue
if "cargo chef cook" in stripped:
return stage
m = re.match(r"(?i)^COPY\s+(?!--from)(.+)$", stripped)
if m:
parts = m.group(1).split()
stage.extend(p.strip("./").split("/")[0] or "." for p in parts[:-1])
return None
def main() -> int:
paths = patched_paths(root / "Cargo.toml")
if not paths:
print("no patched paths to check")
return 0
keep = allowed(root / ".dockerignore")
bad = []
for p in paths:
top = p.strip("/").split("/")[0]
if top not in keep:
bad.append((p, top))
elif not (root / p).is_dir():
bad.append((p, None))
for path, top in bad:
if top is None:
print(f"Cargo.toml patches {path}, which does not exist", file=sys.stderr)
else:
print(
f"Cargo.toml patches {path}, but .dockerignore does not re-include {top!r}:\n"
f" the image build would not see it, and cargo would fail on it.\n"
f" Add `!{top}` to .dockerignore.",
file=sys.stderr,
)
copied = copied_before_cook(root / "Dockerfile")
if copied is not None and "." not in copied:
for p in paths:
top = p.strip("/").split("/")[0]
if top not in copied:
print(
f"Cargo.toml patches {p}, but the Dockerfile doesn't copy {top!r} into the\n"
f" stage that runs `cargo chef cook` before that step, so cooking the\n"
f" dependencies fails on it. Add `COPY {top}/ {top}/` before the cook.",
file=sys.stderr,
)
bad.append((p, top))
if bad:
return 1
print(f"build context and cook stage include every patched path: {', '.join(paths)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())