jcoffey-dev is traveling from Thursday 1 October through Sunday 4 October. Issues and pull requests are welcome, and will get an answer after that. Thanks for your patience.
Gitea stays where the project lives and push-mirrors every branch and tag
to GitHub. With the Actions variable BUILD_ON set to 'github' on both
forges, the GitHub copy does the building and reports back to Gitea as a
commit status; unset, nothing changes and Gitea builds as before.
.github/workflows/ci.yml replaces the GitHub-era files. Branch pushes run
what Gitea's ci.yml checks (fork checks, dev build, test targets, the
release profile on main). v* tags run what publish.yml does, with the same
two guards: the image per architecture on native runners side by side,
the multi-arch index and :latest, the Gitea Release if the tag has none,
and the host-install binaries taken out of the image. A final job posts
"github/ci (branch)" or "github/ci (tag)" to the commit on Gitea.
On Gitea, the heavy jobs skip under BUILD_ON=github and a `github` job
waits for that status and passes or fails with it, so pull requests and
merges still look at a Gitea run. The weekly release, the upstream watch
and the announcement stay on Gitea.
Removed: cleanup.yml and publish.yml (GHCR), release.yml (a second weekly
schedule), and dependabot.yml, whose pull request branches every mirror
sync would delete.
The registry knows, for every expression field, the constants it may
evaluate to and the variables its conditions may read, and enforces both.
The schema served to INBUXA Admin described every one as a bare
x:Expression, so the console could offer nothing better than free text.
tools/fork/expr-schema.py reads those contexts from the generated registry
code and writes them onto each field's type as
expression: {constants, variables}. All 124 expression fields are covered.
CI runs it with --check so the schema can't drift from the registry.
Phase 2 of the personal-data catalog spec.
resources/privacy/catalog.toml classifies every object in the schema
(316) and inbuxa's own JMAP objects (12): each property that can hold
personal data, with its categories, and for objects that hold any,
whose data it is, where it lives, its scope and what bounds its
retention (a named setting where there is one). Twenty sources that
are no object -- the log file, exporters, webhooks, spam lookups, the
Explain cache, relays and hooks, push, legacy-use records -- carry the
same facts plus the settings that turn them on, whether the data
leaves the host, and the code that writes it. Classifications of
objects that hold data about people are from the spec's source map;
the rest are typed from the schema alone (address, IP, secret).
tools/fork/privacy-check.py fails CI when an object or inbuxa object
has no entry, when a property the schema types as an address, IP or
secret is left to its object's default, when an entry names an
object, property, setting or code path that is gone, or when it uses
a word outside the catalog's vocabulary. --unlisted prints starting
entries. strip.py's report gains "Unclassified in the privacy
catalog": objects and fields new in an import and not classified,
informational like the Enterprise flags.
Tested: 13 unit tests (tools/fork/tests): the check passes on this
tree; fails on an unclassified object, an address hidden behind a
default, a secret in a set or object reference, stale properties,
objects, settings and code paths, an unlisted inbuxa object and a
word outside the vocabulary; --unlisted's entries; and the strip
report on a synthetic import. The check and the tests run in the
fork-checks job.
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.
v2026.9.24 was tagged on a commit CI had passed, and its release build could
not compile crates/scim at all:
error: queries overflow the depth limit!
= note: query depth increased by 130 when computing layout of
{async fn body of context::<impl ...>::writable_domain()}
The crate is ours, and the failure is profile-dependent: the release profile
computes those async fn layouts in one go and goes past rustc's default query
depth, while the dev profile never gets that far. CI builds dev, so CI was
green on a commit that could not be released. The tag produced no image and
no release, which is the one merciful part.
Two changes:
- #![recursion_limit = "256"] on the crate, which is what rustc itself
suggests, with a note saying why it only shows up in release. Proved by
building -p scim in release locally: it now finishes.
- CI builds the release profile too, on pushes to main. Pull requests stay
on dev, where the wait is worth less. A few minutes per merge is cheaper
than learning this from a tag, which throws away a multi-architecture
build and leaves a version half-cut.
strip.py compiles the stripped tree, so a dual-licensed file that only
serves an Enterprise feature fails the import instead of the merge, as
v0.16.23's tests/src/directory/issuer.rs does. Upstream's tests of the
features the fork rebuilt are expected not to compile there and are listed
in build-check-known.txt; an error anywhere else fails the run. Checked
against both imports: v0.16.22 passes with its 16 expected errors, v0.16.23
fails on issuer.rs alone. Imports the strip leaves unused are reported.
It also renames the upstream name where clients, users or operators meet
it as an identifier, from tools/fork/renames.py: wire-protocol names, the
web interface's client id, store keys, configuration defaults and the
served schema. main is renamed with the same module, so a re-import
arrives purged and those lines don't conflict.
notice-check.py fails CI when an upstream file the fork changed, measured
against the upstream branch, lacks its AGPL 5(a) notice; --fix adds it.
It runs beside the name check in a renamed fork-checks job.
Also commits v0.16.23's strip report under docs/fork/strip-reports/, which
the import in #18 left out.
tools/fork/name-check.py reads every string literal in crates/ (comments
and test directories skipped) and fails on any that carries the upstream
name without an entry in name-allowlist.txt. An upstream merge can bring
such strings in without a conflict, so it runs on every push and PR.
The first run found three the earlier sweeps missed, fixed here: the SMTP
HELP reply pointed at upstream's website (now brand_url!), the event
collector thread was named after upstream, and the FreeBSD default data
path still said /var/db/stalwart/ where Linux already had /var/lib/inbuxa/.
Two operator-visible defaults are allowlisted as open, pending a decision:
the log file prefix and the SQL stores' default database and user.
The build now mounts the named volume inbuxa-server-cargo at /cache and keeps
CARGO_HOME and CARGO_TARGET_DIR there, so a push reuses the compiled
dependency tree (RocksDB included) instead of rebuilding it from scratch.
Both runners allow that one volume; each host keeps its own copy.
With the cache in place the job moves to runs-on: light, so it can run on
host2 as well. Cargo's parallelism now follows the job's CPU cap rather than
the host's core count, and the target dir is dropped past 25 GB.