Environment registry, the kept test database on CI, and the vector mark #1241

Merged
tiagoagueda merged 7 commits from f-stack3 into main 2026-11-12 14:14:06 +00:00
Owner

Three changes stacked on one branch so they share one CI run; each keeps its own commits, and the catalogues are extracted once at the end. Merge fast-forward-only.

  • Refs #1186 (stage 1, the issue stays open for stages 2-4): every environment variable declared once in core/configuration.py; the settings read through it with no change for any deployment (every Django setting dumped under nine environments, before and after: 0 differences); sizes and durations accept 25MB/2h, booleans yes/no/on/off, secrets <NAME>_FILE; plugins declare their own variables (EnvironmentVariable, read_environment on the surface); checks W005 (unknown name), W006 (out of range), W007 (deprecated alias), E009 (a value that cannot be read stops the start, naming the variable and the syntax); manage.py settings; .env.example generated, and a fresh copy sets exactly what the old one did. The maintainer's decisions of 2026-11-12 are applied.
  • Refs #1033 (item 9, option (b) chosen by the maintainer): the unit job keeps its migrated test database between runs (#1144's cache under actions/cache, restore keys withheld from pull requests); the PostgreSQL and browser jobs still migrate from nothing on every push, and a rule test holds them to it.
  • Closes #1238: the mark is served as SVG, first, and every PNG is rendered from the SVG (the raster source is gone); the moving mark is a preference under Appearance, still by default and never moving under reduced motion; in June the mark wears the pride colours in the person's time zone. Migration accounts 0054_animated_mark, archive format 84.

Tested locally on the stack: lint, migrations, catalogues, manage.py check (default and production), the full unit suite (16,716 passed), and the browser files of the mark, every page and reflow. The one failure is tests/test_wiki_surface.py (CI skips it): the wiki's Writing-a-plugin page gains EnvironmentVariable, read_environment, sync_state and MAX_SYNC_STATE_BYTES after the merge.

🤖 Generated with Claude Code

Three changes stacked on one branch so they share one CI run; each keeps its own commits, and the catalogues are extracted once at the end. Merge fast-forward-only. - Refs #1186 (stage 1, the issue stays open for stages 2-4): every environment variable declared once in `core/configuration.py`; the settings read through it with no change for any deployment (every Django setting dumped under nine environments, before and after: 0 differences); sizes and durations accept `25MB`/`2h`, booleans yes/no/on/off, secrets `<NAME>_FILE`; plugins declare their own variables (`EnvironmentVariable`, `read_environment` on the surface); checks W005 (unknown name), W006 (out of range), W007 (deprecated alias), E009 (a value that cannot be read stops the start, naming the variable and the syntax); `manage.py settings`; `.env.example` generated, and a fresh copy sets exactly what the old one did. The maintainer's decisions of 2026-11-12 are applied. - Refs #1033 (item 9, option (b) chosen by the maintainer): the unit job keeps its migrated test database between runs (#1144's cache under `actions/cache`, restore keys withheld from pull requests); the PostgreSQL and browser jobs still migrate from nothing on every push, and a rule test holds them to it. - Closes #1238: the mark is served as SVG, first, and every PNG is rendered from the SVG (the raster source is gone); the moving mark is a preference under Appearance, still by default and never moving under reduced motion; in June the mark wears the pride colours in the person's time zone. Migration `accounts 0054_animated_mark`, archive format 84. Tested locally on the stack: lint, migrations, catalogues, `manage.py check` (default and production), the full unit suite (16,716 passed), and the browser files of the mark, every page and reflow. The one failure is `tests/test_wiki_surface.py` (CI skips it): the wiki's Writing-a-plugin page gains `EnvironmentVariable`, `read_environment`, `sync_state` and `MAX_SYNC_STATE_BYTES` after the merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Postulo read some 120 POSTULO_* variables through env(...) calls spread over the
settings and a few os.environ reads elsewhere, each with its own default and its own
idea of what a value may say. .env.example and the wiki were written by hand and had
drifted from the code, nothing said which spellings a boolean took, and a mistyped
name was a setting silently not made.

postulo.core.configuration now declares each variable once: type, default, unit,
range or choices, the area it belongs to, who reads it (the instance, the container's
start-up script, gunicorn, a build argument, or only the tests and scripts), the
SiteSettings field it pins and a one-line description. The settings modules, the ESCO
and GeoNames paths, fetch_geonames, seed_demo and the catalogue signing script read
through it; it imports no model or translation, so the settings can import it.
django-environ is still what reads a value (with its $NAME interpolation where it was
used), and a dump of every Django setting under nine environments and four settings
modules is identical before and after.

Values gain syntaxes without losing any: booleans take true/false/1/0/yes/no/on/off in
any case (and y, n, ok, a number, as before); sizes and durations take 25MB, 90s, 2h,
7d, converted to the unit the name implies; list items lose the spaces around them;
choices match in any case; a secret may be given as <NAME>_FILE. A value that cannot be
read no longer stops the instance at import: it falls back as it was read before and
the new check postulo.E009 names it with the syntax expected (postulo.W006 for a value
out of range). An unknown POSTULO_* name is postulo.W005, with the nearest known one.
Old names for a renamed variable are built (an Alias, read in its own unit, warned
about as postulo.W007, refused beside the new name), proven on a scratch entry; nothing
is renamed yet.

The naming rules are in CONTRIBUTING.md and a test holds every name to them except
the 21 that break them today, each listed with the name it takes in the next stage.
.env.example is written from the registry by scripts/configuration_docs.py, which also
prints the wiki's Configuration tables (--wiki), and manage.py settings prints each
variable's effective value and where it came from, secrets as set or unset.

Refs #1186

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three answers to the first stage, taken together.

A copy of the generated .env.example now sets what the hand-written one set,
with the same values: POSTULO_DEBUG=true, POSTULO_ALLOWED_HOSTS, the time zone,
the log level, closed registration and POSTULO_DEFAULT_FROM_EMAIL=postulo@example.org,
beside the secret key a deployment must fill in. A registry entry says so with
set_in_example; a dump of every setting under the new file's lines and the old
one's is identical.

A plugin declares the environment variables it reads, as EnvironmentVariable
entries in its `environment`, on the plugin surface with read_environment, which
reads them as Postulo reads its own (suffixed sizes, every boolean spelling,
_FILE secrets). Names are POSTULO_PLUGIN_<NAME>_... and follow the naming rules,
now one function in the registry. The start-up check knows a loaded plugin's
declared names and checks their values, so only a variable read without being
declared is still reported as unknown; manage.py settings lists them under the
plugin's name; and postulo.plugins.testing gains
assert_declares_usable_environment.

A value that cannot be read still stops the start (postulo.E009), and its message
now says enough to fix it from the log alone: the variable, the value (never a
secret's), why it cannot be read, that start-up stops until it is corrected, the
plugin that reads it if any, and in the hint the syntax expected with the default
taken when it is unset. The test database's variable is described as
postulo.plugins.testdb reads it.

Refs #1186

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every process of the unit suite began by migrating an empty in-memory
database: about 290 migrations, some 35 s, in each worker, five times a
push. #1144 taught the suite to keep the migrated database and restore
it, off on CI unless a workflow names the directory. The maintainer
chose to turn it on in CI (option b, 12 November 2026), as long as
something still migrates from nothing on every push.

The unit job, in ci.yml and its copy in every-python.yml, now names the
directory in POSTULO_TEST_DB_CACHE and keeps it with actions/cache,
continue-on-error as the other caches, restore keys withheld from pull
requests (#827). The key rolls with the migrations, the models and
settings they read, the classification one of them seeds from, the
lock, uv and the Python; testdb's own name for each file decides
whether it is used, so a stale restore costs a migration and nothing
else. The PostgreSQL and browser jobs keep migrating from nothing,
since only an in-memory SQLite database is ever kept.

tests/test_workflow_rules.py holds the cache step and the variable in
both copies of the job, the two jobs that still migrate, and, for every
cache step, restore keys withheld from pull requests.

Refs #1033

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mark was still made from a 1024-pixel raster although a vector drawing had sat beside
it since #1218, and the header showed a 64-pixel PNG scaled to 28, soft at any zoom and on
any dense screen. The raster also carried a light fringe that showed on the dark theme.

scripts/brand.py now takes the SVGs in assets/brand as the source. It writes minified,
safe copies framed on the mark (logo.svg, logo-pride.svg and the two drift animations: no
script, no external reference, no foreignObject, no editor's notes), and renders every PNG
from logo.svg or logo-pride.svg with Chromium, the browser the browser tests already
install. WeasyPrint draws no SVG blur, so none of the seven shadows; resvg drew the same
picture as Chromium but would have been one more package. brand.json records the SVGs as
the sources, and --check still compares digests and needs nothing but Python. The raster
source is gone; the pride raster stays as the record it was.

The tab offers the SVG icon first with the PNGs behind it; the header draws the vector in
a <picture> with the PNG as its image. Two additions decided with the issue:

- A moving mark, off by default, under Settings > Appearance: the drift loop (a twentieth
  of the swell's size, and at 28 pixels a bending edge is under a pixel). It sits behind
  prefers-reduced-motion: no-preference, so a system asking for less motion gets the still
  vector, and does the moment the setting changes. Kept in the archive (format 84).
- In June, in the person's own time zone (the instance's for a visitor), the tab and the
  header show the pride mark, moving if the person chose that. The icons a phone installs
  stay the usual mark.

Closes #1238

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Record #1186, #1033 and #1238 in the changelog and extract their strings
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 31m35s
CI / Browser tests (Chromium) (pull_request) Failing after 47m46s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m24s
CI / Security audit (pull_request) Successful in 30s
CI / Database tests on PostgreSQL (pull_request) Successful in 5m34s
63e6d8d148
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run CI again: the browser job ran out of time under host load
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 15m9s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m24s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m28s
CI / Security audit (pull_request) Successful in 21s
CI / Browser tests (Chromium) (pull_request) Failing after 36m12s
687457a31c
The browser job stopped at 77% with every test so far passing, when the
runner's context deadline expired while ouranos stood at a load average
of 55 with its swap full. Nothing in the change was found wanting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the header's mark to the 28 pixels the PNG took
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 15m0s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m6s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m22s
CI / Security audit (pull_request) Successful in 22s
CI / Browser tests (Chromium) (pull_request) Successful in 34m11s
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 2m6s
README / README language tables (push) Successful in 18s
Dev image / Dev image: build, scan, push (push) Failing after 41m3s
CI / Unit tests and coverage (Python 3.14) (push) Successful in 16m29s
CI / Database tests on PostgreSQL (push) Successful in 4m28s
CI / Security audit (push) Successful in 20s
CI / Browser tests (Chromium) (push) Successful in 35m36s
8445bae603
The <picture> that serves the vector mark was `display: contents`, so its children became
items of the wordmark's flex row. A <source> is not hidden by the browser's own stylesheet:
each one was an empty item, and the row's gap-2 put 8 pixels before the image for it. The
wordmark grew by as much, and on /listings/, /applications/ and /jobs/companies/ at 1280
pixels, under the text-spacing override and with no script to fit the row, that was enough
to put the search form on a second line.

The picture is now the row's item itself, a 28-pixel block that does not shrink, with the
image the same size inside it; the sources, their order, the reduced-motion rule and the
empty alt are as they were. A browser test pins the room the mark takes: it starts the
link, and the link is the mark, the gap and the name.

Refs #1238

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tiagoagueda deleted branch f-stack3 2026-11-12 14:14:09 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo!1241
No description provided.