go.mod: take the shared libraries' current heads
ecore for mcphttp and the hot-path work, core for the errors fix: its seven
gqlerror sentinels were package-level pointers that gqlgen stamped with the
first field path they ever took, which was a wrong answer, a cross-request
leak and a data race.
ci: report this build's coverage and benchmarks
The pipeline built and published an apk and said nothing about the code it
packaged. It now runs the suite as `make cover COVERPROFILE=$HOME/cover.out`
and POSTs the profile to cov.srht.bigb.es under ~bigbes/sr-ht-compare — the
name on the sources: line, not the renamed service's.
`cover` is not a second suite: it is `test` with two flags reached through
TESTFLAGS, so CI's command and a developer's `make test` cannot become two
different runs. -covermode=atomic because this is a concurrent server and the
default set mode records that a statement ran rather than how often.
There was nothing to upload to bench.sr.ht either, gitx having no benchmark at
all. The diffing path — what a request here is actually spent in — now has
five: Diff in both of the grammars a compare URL can carry, DiffStat,
CommitPatch, and the two pure functions under them, mapFilePatches and
cutPatch. They build their own fixture, 40 files of 120 lines with a branch
that diverged, because the test fixture's dozen lines measure opening a
repository rather than diffing one.
`make -s bench` writes benchfmt to $HOME/bench.txt and three greps refuse to
upload a file the expected names are missing from: `go test -bench` that
matches nothing prints ok and exits 0, and an empty file is valid benchfmt.
Both uploads turn tracing off around the token, use --fail-with-body so a
refusal is printed and still fatal, and strip both ref prefixes because this
pipeline builds tags too.
cover.out and bench.txt are declared as artifacts, so a failed upload leaves
the report recoverable, and gitignored, because written into the checkout they
would stamp the packaged binary +dirty.
The manifest is now within 560 bytes of the 16 KiB a submission may carry; the
reasoning lives in docs/ci.md, which says so.
ci: publish the apk into artifacts.sr.ht as well
The abuild output goes on to the S3 bucket phoebe re-indexes, and now also to
the ~bigbes/main channel of artifacts.sr.ht, which indexes and signs it in the
same request. One PUT per file with the shared working token; a 409 says the
job ran twice and is a warning rather than a failure.
rename the service to diff.sr.ht
The instance-facing name changes and nothing else does: the config.ini section,
the origin, the apk package, the nginx block and the systemd unit are all
diff.sr.ht now, and the brand's red service label reads "diff" because chrome
derives it from the section name.
What deliberately stays is everything whose rename would cost more than it is
worth: the Go module path, the repository name, the comparesrht binary, and the
/~owner/repo/compare/base...head routes people have in their history. README
says so at the top so the split is not read as an oversight.
ci: drop the 'all' from go mod download, which rewrote go.sum
The cache warm-up was 'go mod download all'. That resolves the entire
module graph, test dependencies of dependencies included, and appends
their hashes to go.sum — 145 lines in this repository, measured in a
fresh clone of this branch.
Which makes it the bug this wave exists to remove. A modified tracked
file in the checkout when go build runs is what stamps vcs.modified into
the binary, the same failure the version task avoids by not sed-ing the
APKBUILD; the warm-up would have reintroduced it one task later. The
bench sibling proved it first: build #359 stopped at check-version with
a -dirty binary and 'M go.sum' named as the thing in the way, 228 lines
there.
Without 'all' go.sum is untouched, go mod verify still passes, and what
is fetched is what the main module builds, which is all a warm-up needs.
Also removes the sentence that let this through. Both the manifest and
docs/ci.md claimed -mod=readonly is the default so neither command can
rewrite go.mod or go.sum. It is false, and it sat directly above the
line that disproved it. readonly governs updates to the module
requirements, not writes to go.sum: measured here with the flag set
explicitly on the command line, 'go mod download all' still appends the
same 145 lines and still exits 0, and go mod verify passes after it, so
verification guards nothing either.
Setting the flag on the command line is the point. This machine carries
go env -w GOFLAGS=-mod=mod, as bench's did, so a run that omits it
proves nothing about the builder.
What remains is the git status --porcelain at the end of the task, and
it is now documented as the check rather than as belt-and-braces: it is
the only thing that would notice a go.sum rewrite before the compile,
and on bench it is what did. docs/ci.md warns against restoring the
'all' and against replacing the print with an appeal to a flag.
compare's README makes no claim about module hygiene in CI, so nothing
to correct there
ci: a test task, and the truth about !check
gofmt, go vet and make test, between cache_restore and build. gofmt -l
exits 0 whether or not it printed anything, so its output goes through
tee and is checked with test -s.
No Postgres and no network: every suite here is hermetic, so nothing can
skip and there is no DSN to guard. gitx/fixture_test.go builds a bare
repository with the local git and web/web_test.go is httptest plus
ecoretest.
Which makes the APKBUILD's !check comment wrong, and it has been wrong
the whole time — the tests never needed a live git.sr.ht API. With the
task in place the option stops meaning 'these cannot run here' and starts
meaning 'these ran before abuild did', which is only true while make test
stays fatal. Said so where the option is.
ci: the family's build pipeline, and PKGVER by export
Brings this manifest to the shape the sibling services share.
cacher is installed by install.sh instead of a raw curl of the binary, in
two tasks: install.sh appends its PATH export to ~/.buildenv, which only
the next task sources. The old fetch had no -f, so an HTTP error body was
written to ~/.local/bin/cacher and chmod +x'd; the installer verifies
against checksums.txt.
The version task drops the sed of the tracked APKBUILD. Measured on
go1.26.5, rewriting a tracked file sets vcs.modified=true and Go stamps
+dirty into Main.Version for the whole abuild run, so PKGVER is exported
and APKBUILD reads pkgver="${PKGVER:-0.0.0}". One git describe decides
it: the tag, the tag plus commits, or the commit count. Tags join
allow-refs so pushing v0.9.0 produces the 0.9.0 apk. The export alone is
not enough — abuild works inside the checkout and puts src/, pkg/ and
GOTMPDIR=tmp/ there, so .gitignore covers all three.
cache_restore takes --optional rather than || true, which also swallowed
bad credentials and a dead bucket, and gains the half-restored module
cache repair block. cache_save moves after publish so an S3 hiccup cannot
strand a good apk, and loses its cacher exists guards: dir upload already
skips a present key. publish gains the no-credentials gate.
The stylesheet is now built in CI: sassc and minify, the scss task that
materializes the shared partials no apk ships, make css before the
compile and make check-css after it. The committed
web/static/main.min.79713f25.css stays tracked for now — build() prints
git status and the digest so the first CI run says whether the pipeline
reproduces those bytes, and dropping it from the index is a second step.
package() calls make install-files. abuild runs it in a fresh process
that never called build(), so make install would relink the binary from a
cold cache and ship a compilation nothing had checked.
Reasoning lives in docs/ci.md: the manifest is a varchar(16384) and a
branch over the cap gets no CI at all
deps: tidy after the uplift
chrome: the head links from ecore, and no shipped favicon
layout.html hardcoded <link rel="icon" href="/static/logo.svg">, and
web/static/logo.svg was this service's copy of the brand circle chrome
now inlines as a data: URI. Both go: the head renders srht-head-links,
which emits the stylesheet and the icon guarded on emptiness, and the
icon costs no request and cannot 404.
The static tree is two hashed artefacts now, so the arm of
TestStaticBundleAndCSS that asserted an hour-cacheable unhashed asset has
nothing left to ask about; TestFaviconIsInline takes its place and also
pins that the data: URI reaches the page rather than #ZgotmplZ.
chimw: the request line in the journal, HEAD routes, and a 405 page
Three things at once, all of them the chi half of the shared middleware.
RequestLogger replaces chi's Logger, which wrote an unstructured line to
stdout — the highest-volume record this daemon emits and the only one not
beside the rest on stderr. RequestID goes above it so the request line
and a panic report share an id, Recoverer below it so its own report goes
through the entry instead of to stdout, and /healthz is skipped.
GetHead registers every read route under HEAD as well. Until now `curl
-I` and every uptime probe were answered 405 plus a kilobyte of rendered
error page by pages whose whole job is to be cheap to ask about.
RenderRefusals installs both routing failures against renderError; this
service had the 404 alone and left the 405 to net/http's plain text. The
new test that pins it also corrects what TestUnsafeMethodRefused claimed:
the group's middleware runs before either refusal, so an unsafe method on
a GET-only path is a 403 from the same-origin guard, not a 405.
instconf: the origin ladder and the one-pass config check
validateConfig kept its shape and lost its body. instconf.Require is the
same decision — every missing key in one record, one exit, so an operator
edits config.ini once instead of restarting per gap — and firstConfigured
was this service's copy of the four-key API-origin ladder core-go panics
at the end of. NeedAny is handed APIOriginKeys() rather than the four
names written out, so the startup check and InternalAPIOrigin cannot
drift apart the way two literal lists eventually do.
logging: the instance's log policy instead of forty local lines
initLogging held compare's own copy of the level choice, the colour probe
and the mask list, and the mask list was the drifted one: three keys and
a four-word pattern, against an instance whose services between them
redact ten. logging.Defaults answers all of it, and the handler stays
here — scribe is auxilia's and does not belong in the library every
service links for its chrome.
isTerminal goes to logging.ColorEnabled, which also honours NO_COLOR, and
the level now reads $LOG_LEVEL after -d. Defaults(nil, "") because the
handler is installed before LoadConfig: a daemon that became verbose only
after startup is silent for the window -d was passed to watch.
login: ecore's cookie decoder, which validates the name ours did not
authz/identity.go was the fifth copy of a decode the instance has one of,
and it was one of the two that validated nothing: whatever name a sealed
payload carried went on to the GraphQL viewer field, the chrome's nav and
every log line the request produced. login.ValidName refuses it, and
there is no spelling of the API that turns the check off.
Gone with it: CookieName, UsernameFromRequest, Middleware, ForContext and
the ctxKey, plus identity_test.go, whose round-trip, tilde-stripping and
garbage-cookie cases are ecore's tests now. The Middleware becomes
login.Optional() — this service refuses nobody, git.sr.ht decides what an
anonymous viewer may see — and the default validator is kept rather than
core.ValidOwner, which is lowercase-only and would log a real account out
of compare alone.
The one behaviour change a viewer could notice: a cookie whose name
carries a '/', a control byte or a non-ASCII letter now reads as
anonymous instead of as that name.
deps: the ecore of the third uplift, and auxilia's first tag
sr-ht-ecore grew login, logging, instconf and chimw, which between them
replace this service's identity middleware, its logging setup and its
config validation. auxilia is a real v0.7.0 rather than a pseudo-version
now, so the requirement can name a tag.
deps: auxilia whose scribe.Err reads the whole error chain
Until now scribe.Err type-asserted the outermost error for slog.LogValuer,
so a culpa error under a single fmt.Errorf logged as a flat string and lost
its code, hint and stacktrace with no warning. It walks the chain now, which
is what makes the culpa wrapping in this service visible in the journal.
log: slog through auxilia's scribe, not logrus
This was the last service on the instance still logging through logrus,
and the ecore bump made it a correctness question rather than a
consistency one: the recovery middleware reports a panic through slog's
DEFAULT logger, so without a SetDefault the one record carrying a stack
trace would have gone to Go's plain handler while every other line went
through logrus.
initLogging installs a scribe tint handler on stderr — source on, colour
only when stderr is a terminal, and the mask rules for token, cookie and
authorization. It reads -d out of the argument vector rather than
waiting for core-go's parse, because a daemon that only became verbose
after it had finished starting would be silent for exactly the part of
its life an operator passes -d to watch. Terminal detection is a stdlib
Stat rather than golang.org/x/term, which would be a new dependency for
one predicate.
The startup config check keeps its shape deliberately: it still
accumulates every missing key and reports them in ONE record before ONE
exit, so an operator fixes the config in a single pass. The keys go in
as a slice attribute rather than a joined string, so the structured
sinks keep them as a list.
culpa goes in at the one boundary where it pays: the errors web.New
returns, which are the only errors this package produces and all of
which arrive at that single startup record. Each carries a hint naming
the config key or the build step that fixes it, and scribe.Err unfolds
message, code, hint and stacktrace into fields of their own. The failing
request path logs the same way, with ErrorContext so a cancelled request
reads as cancelled rather than as an unexplained 500.
TestPanicIsAnErrorPage covers the wiring end to end through Register: a
panicking authorizer yields the chrome-wrapped 500, the panic value does
not reach the viewer, and the report reaches slog's default logger.
deps: sr-ht-ecore whose middleware reports panics through slog
The panic report is structured now (method, path, panic, stack) and goes
through slog's default logger, so a service that never calls
slog.SetDefault gets those reports in Go's plain stderr handler. The
next commit is what makes this one land somewhere useful.
readme: name the ecore packages the web tier draws from
cmd: ecoretest keys in the startup smoke test
The synthesized config.ini takes ecore's fixed keyset and origins rather
than generating a keypair per run; a constant cannot be malformed by
accident, which matters for a subprocess whose only way of complaining
is a log.Fatal. Also notes the three middlewares Register now installs
inside its own group, and why chi's Recoverer stays out here.
authz: bootstrap the tests from ecoretest
Drops the fernet key generation and the ed25519 seed both test files
were doing by hand, and the ini.File assembled around them. The keys are
constants now, which is what makes InitCrypto idempotent across the two
packages of this service that both call it.