Part of #17 (definition of done: #17 (comment)). It also covers #99, which is closed when this lands.
Today only the signing key comes from the environment, and only inside the Docker image, through signing_key: "${ENV:PIXA_SIGNING_KEY}" in config.docker.yml. Every other setting needs a config file mounted over /etc/pixa/config.yml. upaas gives an app environment variables and bind-mounted directories; it mounts no config file. So pixa must take its whole configuration from the environment. smartconfig's ${ENV:...} cannot do this for optional settings: it fails when the variable is unset.
Definition of done
Every key the loader accepts (isKnownConfigKey in internal/config/config.go, except env) can be set by an environment variable named PIXA_ plus the key in upper case, with . written as _: PIXA_STATE_DIR, PIXA_ALLOWLIST_HOSTS, PIXA_METRICS_USERNAME, and so on. The one exception is the port, read from PORT, the name REPO_POLICIES.md requires ("overridable with PORT"); there is no PIXA_PORT.
Precedence: environment variable, then config file, then default. A variable present in the environment is a set value, even when empty, and is parsed exactly as the same value in the config file would be. Only an absent variable falls through to the file, then the default.
A set value that cannot be parsed or fails validation aborts startup with an error naming the variable and the value. The values of PIXA_SIGNING_KEY and PIXA_METRICS_PASSWORD are never printed. Nothing falls back to a default. The existing checks (port range, signing key length and placeholder, metrics username and password set together, CIDR parsing, bare hostnames, db_url not empty) apply to environment values through the same code, not a second copy.
Booleans parse with strconv.ParseBool. The three lists (allowlist_hosts, blocked_networks, trusted_proxies) are comma-separated; spaces around an entry are trimmed; an empty entry aborts, as it does in the file; an empty variable is an empty list, so PIXA_TRUSTED_PROXIES= trusts no one, like [] in the file.
The image bakes in no config file: config.docker.yml, its COPY in Dockerfile and the --config argument in ENTRYPOINT go. The defaults already give state_dir/var/lib/pixa and port 8080, and a file mounted at /etc/pixa/config.yml is still read because that path is on the loader's search list. With PIXA_SIGNING_KEY unset and no file, startup still aborts, and the error names PIXA_SIGNING_KEY.
Failing tests first, with t.Setenv: PORT=9090 beats a file saying 8080; PORT=banana aborts naming PORT; a list from the environment replaces the file's list; an invalid CIDR in PIXA_BLOCKED_NETWORKS aborts naming the variable; PIXA_DEBUG=maybe aborts; with no variables set, behavior is unchanged.
config.example.yml says once, near the top, how a key maps to its variable. README.md's Configuration section gets a table of every variable with a one-line meaning, and the container paragraph under Getting Started says settings come from environment variables and a mounted file is optional.
Keep it small: one list of key and variable names, consulted by the existing typed getters, not a second loader. Do not touch .golangci.yml.
Model: opus-5-5
Part of https://git.eeqj.de/sneak/pixa/issues/17 (definition of done: https://git.eeqj.de/sneak/pixa/issues/17#issuecomment-102965). It also covers https://git.eeqj.de/sneak/pixa/issues/99, which is closed when this lands.
Today only the signing key comes from the environment, and only inside the Docker image, through `signing_key: "${ENV:PIXA_SIGNING_KEY}"` in `config.docker.yml`. Every other setting needs a config file mounted over `/etc/pixa/config.yml`. upaas gives an app environment variables and bind-mounted directories; it mounts no config file. So pixa must take its whole configuration from the environment. smartconfig's `${ENV:...}` cannot do this for optional settings: it fails when the variable is unset.
## Definition of done
1. Every key the loader accepts (`isKnownConfigKey` in `internal/config/config.go`, except `env`) can be set by an environment variable named `PIXA_` plus the key in upper case, with `.` written as `_`: `PIXA_STATE_DIR`, `PIXA_ALLOWLIST_HOSTS`, `PIXA_METRICS_USERNAME`, and so on. The one exception is the port, read from `PORT`, the name `REPO_POLICIES.md` requires ("overridable with `PORT`"); there is no `PIXA_PORT`.
2. Precedence: environment variable, then config file, then default. A variable present in the environment is a set value, even when empty, and is parsed exactly as the same value in the config file would be. Only an absent variable falls through to the file, then the default.
3. A set value that cannot be parsed or fails validation aborts startup with an error naming the variable and the value. The values of `PIXA_SIGNING_KEY` and `PIXA_METRICS_PASSWORD` are never printed. Nothing falls back to a default. The existing checks (port range, signing key length and placeholder, metrics username and password set together, CIDR parsing, bare hostnames, `db_url` not empty) apply to environment values through the same code, not a second copy.
4. Booleans parse with `strconv.ParseBool`. The three lists (`allowlist_hosts`, `blocked_networks`, `trusted_proxies`) are comma-separated; spaces around an entry are trimmed; an empty entry aborts, as it does in the file; an empty variable is an empty list, so `PIXA_TRUSTED_PROXIES=` trusts no one, like `[]` in the file.
5. The image bakes in no config file: `config.docker.yml`, its `COPY` in `Dockerfile` and the `--config` argument in `ENTRYPOINT` go. The defaults already give `state_dir` `/var/lib/pixa` and port 8080, and a file mounted at `/etc/pixa/config.yml` is still read because that path is on the loader's search list. With `PIXA_SIGNING_KEY` unset and no file, startup still aborts, and the error names `PIXA_SIGNING_KEY`.
6. Failing tests first, with `t.Setenv`: `PORT=9090` beats a file saying 8080; `PORT=banana` aborts naming `PORT`; a list from the environment replaces the file's list; an invalid CIDR in `PIXA_BLOCKED_NETWORKS` aborts naming the variable; `PIXA_DEBUG=maybe` aborts; with no variables set, behavior is unchanged.
7. `config.example.yml` says once, near the top, how a key maps to its variable. `README.md`'s Configuration section gets a table of every variable with a one-line meaning, and the container paragraph under Getting Started says settings come from environment variables and a mounted file is optional.
Keep it small: one list of key and variable names, consulted by the existing typed getters, not a second loader. Do not touch `.golangci.yml`.
Model: opus-5-5
clawbot
added this to the 1.0.0 milestone 2026-09-28 11:09:51 +02:00
clawbot
self-assigned this 2026-09-28 11:09:51 +02:00
One list in internal/config/config.go pairs each key with its variable (PIXA_ plus the key in upper case, . written as _; the port is PORT). The existing getters check it first: a variable present in the environment, even empty, is read as the same text quoted in the file would be; otherwise the file, then the default. So every existing check covers environment values.
Errors a variable can reach name the key and the variable, for example config key "port" (environment variable PORT): value "banana" is not an integer. No check prints the signing key or the metrics password.
An empty string for blocked_networks or trusted_proxies becomes an empty list, as allowlist_hosts already does, so PIXA_TRUSTED_PROXIES= trusts no one. In the file, that text used to abort.
Point 5's premise does not hold: the search list is built from the daemon name, so it holds /etc/pixad/config.yml, not /etc/pixa/config.yml; the image read the latter only through --config. Reading taken: search under the project name pixa (/etc/pixa/, ~/.config/pixa/), matching /var/lib/pixa and every document that names the mount path. Then config.docker.yml, its COPY and --config go.
Failing tests first with t.Setenv: point 6's six, plus every variable with no file, an empty variable not falling back to the file, and a missing signing key naming PIXA_SIGNING_KEY. The config tests unset PORT and every PIXA_ variable first, so a developer's shell cannot sway them.
Model: opus-5-5
Plan:
- One list in `internal/config/config.go` pairs each key with its variable (`PIXA_` plus the key in upper case, `.` written as `_`; the port is `PORT`). The existing getters check it first: a variable present in the environment, even empty, is read as the same text quoted in the file would be; otherwise the file, then the default. So every existing check covers environment values.
- Errors a variable can reach name the key and the variable, for example `config key "port" (environment variable PORT): value "banana" is not an integer`. No check prints the signing key or the metrics password.
- An empty string for `blocked_networks` or `trusted_proxies` becomes an empty list, as `allowlist_hosts` already does, so `PIXA_TRUSTED_PROXIES=` trusts no one. In the file, that text used to abort.
- Point 5's premise does not hold: the search list is built from the daemon name, so it holds `/etc/pixad/config.yml`, not `/etc/pixa/config.yml`; the image read the latter only through `--config`. Reading taken: search under the project name `pixa` (`/etc/pixa/`, `~/.config/pixa/`), matching `/var/lib/pixa` and every document that names the mount path. Then `config.docker.yml`, its `COPY` and `--config` go.
- Failing tests first with `t.Setenv`: point 6's six, plus every variable with no file, an empty variable not falling back to the file, and a missing signing key naming `PIXA_SIGNING_KEY`. The config tests unset `PORT` and every `PIXA_` variable first, so a developer's shell cannot sway them.
Model: opus-5-5
Built as planned above, in #131. One addition to the plan: a //nolint:gosec (G101) on the key-to-variable list, which gosec reads as a hard-coded password because of PIXA_METRICS_PASSWORD.
Model: opus-5-5
Built as planned above, in https://git.eeqj.de/sneak/pixa/pulls/131. One addition to the plan: a `//nolint:gosec` (G101) on the key-to-variable list, which gosec reads as a hard-coded password because of `PIXA_METRICS_PASSWORD`.
Model: opus-5-5
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of #17 (definition of done: #17 (comment)). It also covers #99, which is closed when this lands.
Today only the signing key comes from the environment, and only inside the Docker image, through
signing_key: "${ENV:PIXA_SIGNING_KEY}"inconfig.docker.yml. Every other setting needs a config file mounted over/etc/pixa/config.yml. upaas gives an app environment variables and bind-mounted directories; it mounts no config file. So pixa must take its whole configuration from the environment. smartconfig's${ENV:...}cannot do this for optional settings: it fails when the variable is unset.Definition of done
isKnownConfigKeyininternal/config/config.go, exceptenv) can be set by an environment variable namedPIXA_plus the key in upper case, with.written as_:PIXA_STATE_DIR,PIXA_ALLOWLIST_HOSTS,PIXA_METRICS_USERNAME, and so on. The one exception is the port, read fromPORT, the nameREPO_POLICIES.mdrequires ("overridable withPORT"); there is noPIXA_PORT.PIXA_SIGNING_KEYandPIXA_METRICS_PASSWORDare never printed. Nothing falls back to a default. The existing checks (port range, signing key length and placeholder, metrics username and password set together, CIDR parsing, bare hostnames,db_urlnot empty) apply to environment values through the same code, not a second copy.strconv.ParseBool. The three lists (allowlist_hosts,blocked_networks,trusted_proxies) are comma-separated; spaces around an entry are trimmed; an empty entry aborts, as it does in the file; an empty variable is an empty list, soPIXA_TRUSTED_PROXIES=trusts no one, like[]in the file.config.docker.yml, itsCOPYinDockerfileand the--configargument inENTRYPOINTgo. The defaults already givestate_dir/var/lib/pixaand port 8080, and a file mounted at/etc/pixa/config.ymlis still read because that path is on the loader's search list. WithPIXA_SIGNING_KEYunset and no file, startup still aborts, and the error namesPIXA_SIGNING_KEY.t.Setenv:PORT=9090beats a file saying 8080;PORT=bananaaborts namingPORT; a list from the environment replaces the file's list; an invalid CIDR inPIXA_BLOCKED_NETWORKSaborts naming the variable;PIXA_DEBUG=maybeaborts; with no variables set, behavior is unchanged.config.example.ymlsays once, near the top, how a key maps to its variable.README.md's Configuration section gets a table of every variable with a one-line meaning, and the container paragraph under Getting Started says settings come from environment variables and a mounted file is optional.Keep it small: one list of key and variable names, consulted by the existing typed getters, not a second loader. Do not touch
.golangci.yml.Model: opus-5-5
Plan:
internal/config/config.gopairs each key with its variable (PIXA_plus the key in upper case,.written as_; the port isPORT). The existing getters check it first: a variable present in the environment, even empty, is read as the same text quoted in the file would be; otherwise the file, then the default. So every existing check covers environment values.config key "port" (environment variable PORT): value "banana" is not an integer. No check prints the signing key or the metrics password.blocked_networksortrusted_proxiesbecomes an empty list, asallowlist_hostsalready does, soPIXA_TRUSTED_PROXIES=trusts no one. In the file, that text used to abort./etc/pixad/config.yml, not/etc/pixa/config.yml; the image read the latter only through--config. Reading taken: search under the project namepixa(/etc/pixa/,~/.config/pixa/), matching/var/lib/pixaand every document that names the mount path. Thenconfig.docker.yml, itsCOPYand--configgo.t.Setenv: point 6's six, plus every variable with no file, an empty variable not falling back to the file, and a missing signing key namingPIXA_SIGNING_KEY. The config tests unsetPORTand everyPIXA_variable first, so a developer's shell cannot sway them.Model: opus-5-5
Built as planned above, in #131. One addition to the plan: a
//nolint:gosec(G101) on the key-to-variable list, which gosec reads as a hard-coded password because ofPIXA_METRICS_PASSWORD.Model: opus-5-5
clawbot referenced this issue2026-09-28 13:47:17 +02:00