grain-serve is set by flags, as knotless-serve is
ClosedThis moves grain-serve's settings from GRAIN_* variables to a table of --flags. Secrets, key files and the admin list stay in the environment, and GRAIN_ADMINS becomes GRAIN_ADMIN_DIDS. I read Settings::parse, FLAGS, USAGE and Settings in crates/grain-serve/src/lib.rs, and they look right. An empty flag counts as unset, an unknown flag returns the usage, and a flag with no value returns an error. The environment names that remain match the environment: line in the usage. The rest of the diff, including the per-part given readers, the tests and main.rs, is about 64k characters. My tools couldn't page through it, so I didn't read it.
All four checks (check, rust, test, harness) were still queued when I looked. Please make sure they pass, in particular the harness coverage gate, before this lands.
Two minor points, neither blocking:
- A value that is itself a flag name is accepted as a value. For example,
--jetstream --listensets the Jetstream to--listen. knotless-serve may behave the same way. - The "takes a value" error doesn't include the usage, unlike the unknown-flag error. If
main.rsdoesn't add it, you could appendUSAGEthere.
Both minor points are left as knotless-serve has them: its parse_args takes the next argument as the value whatever it is, and says only "<flag> takes a value" without the usage. Changing either would move grain away from the shape this copies.
Landed on feature/rust at fc5f686 through the batch pull Seven reviewed pulls land on feature/rust together, checked once, checked there as a whole. This pull's head is in feature/rust's history.
What changes
grain-serve is set by flags, as knotless-serve is. Where it listens, its index, its public URL, the Jetstream it follows, backfill, the CDN's address, the Bluesky appview, the sign-up PDS, how long old cookies carry, and the non-secret parts of push and Clef are each a
--flagwith a value (grain_serve::FLAGS,grain_serve::USAGE), and a flag given empty is as if not given, so a compose file can pass--jetstream "${GRAIN_JETSTREAM:-}"and turn a part off by leaving the variable unset. A flag it does not take, or one with no value, exits 2 with the usage, as knotless-serve does.GRAIN_LISTEN--listenGRAIN_INDEX--indexGRAIN_PUBLIC_URL--public-urlGRAIN_JETSTREAM(1for default)--jetstream <wss://host>GRAIN_BACKFILL=1+_DIRECTORIES--backfill <https://directory>[,...]GRAIN_BACKFILL_MIRROR/_SKIP/_CONCURRENCY--backfill-mirror/-skip/-concurrencyGRAIN_CDN_URL--cdnGRAIN_BSKY_APPVIEW(1for default)--bsky-appview <https://appview>GRAIN_SIGN_UP_PDS--sign-up-pdsGRAIN_CARRY_COOKIES_UNTIL--carry-cookies-untilGRAIN_APNS_KEY_ID/_TEAM_ID/_TOPIC--apns-key-id/-team-id/-topicGRAIN_APNS_SANDBOX=1--apns-url https://api.sandbox.push.apple.comGRAIN_FCM_PROJECT_ID--fcm-project-idGRAIN_CLEF_MODEL/_URL--clef-model/--clef-urlGRAIN_ADMINSGRAIN_ADMIN_DIDS(still the environment)The environment keeps what knotless keeps there: secrets, key files and identity lists.
GRAIN_CDN_KEYandGRAIN_CDN_SALT,GRAIN_APNS_KEY_FILEandGRAIN_FCM_KEY_FILE,CLOUDFLARE_ACCOUNT_IDandCLOUDFLARE_AI_TOKEN(the account goes with the token it is for),GRAIN_ADMIN_DIDS, renamed fromGRAIN_ADMINSasKNOTLESS_ADMIN_DIDSis named,RUST_LOG, andgrain::env's directories and resolvers (GRAIN_CONFIG_DIR,GRAIN_CACHE_DIR, ...), which the CLI reads too.The
1shorthands for Jetstream and the Bluesky appview are gone, as knotless-serve has none: a compose file gives the URL as its default (${GRAIN_JETSTREAM-wss://...}), andjetstream::DEFAULTandread::search::BSKY_APPVIEWstay as the values to give.Why
The audit against knotless: knotless-serve takes its settings as flags (
parse_args,crates/knotless-serve/src/lib.rs) and reads the environment only for secrets, directories and identity lists, and its compose passes${VAR:-}into flags. grain-serve read every setting fromGRAIN_*. Its compose (the image pull) comes after this, so it can pass flags from the start.Mirrors
parse_args,Optionsand its usage string incrates/knotless-serve/src/lib.rs,main.rs's exit 2 on a usage error, andKNOTLESS_ADMIN_DIDSincrates/knotless/src/env.rs. Kept grain-specific: the struct is stillSettings, notOptions, and its fields are as they were, since every pull in flight that builds one (Settings { index, ..Default::default() }) keeps compiling; and the flags are parsed from a table,FLAGS, into one lookup that each part's config reads (backfill::Config::given,push::Config::given,clef::Config::given), rather than one match arm a flag, so the push and Clef configs read their flags and their key files from one place.--backfill-mirror ""and--backfill-skip ""are none, not the default, as the empty variables were.How it was checked
The settings tests (
lib.rs,tests/backfill.rs,tests/jetstream.rs,push/tests.rs,clef.rs) parse flags now, every flag given empty is every part off, the old variable is not read for a setting, and a flag it does not take is refused with the usage;tests/main.rsruns the binary with flags, and checks it exits 2 with its usage. Clippy run here on grain-serve; the rest left to the checks.Shortcomings
GRAIN_WEBfallback; whichever lands second makes it--web <http://pages>, as knotless-serve's is.Review
Start at
Settings::parseincrates/grain-serve/src/lib.rs.🤖 Generated with Claude Code