feat: say what wikven does about Lua, and stop guessing at it - #473
Merged
Conversation
Scribunto is ordinary equipment on a MediaWiki wiki, and wikven's two products disagreed about it silently. Measured on a bake of one page invoking one module, every one of these exited 0:
| Scribunto | module file | rendered | module in the export |
|---|---|---|---|
| listed, engine present | `Module:Example` | `Lua says hello` | no |
| not listed | `Module:Example` | `{{#invoke:Greet|hello}}` | no, the file was ignored entirely |
| not listed | `Module:Example.wikitext` | `{{#invoke:Greet|hello}}` | **yes, as `Module%3AGreet.html`** |
So three of four ways to have a Lua module produced a published site with braces where the page meant to say something, and one of them exported the module's own Lua source as a page. Nothing said a word.
## The answer
Scribunto is supported under the Docker image and not in the standalone binary. That is not a preference: static-php-cli, which builds the binary's PHP, offers no Lua extension among the hundred and thirty it supports, and Scribunto's other engine shells out to a `lua` program that a single executable has no way to ship. Checked upstream rather than assumed.
## Two refusals
`Scribunto` holds the rule, and `build.php` puts it before `clearOutputDirectory` so a site that is refused still has its last bake on disk.
* a site with `Module:` files and no `Scribunto` in `extensions` is refused, naming the files;
* a site that lists Scribunto where no engine can run it is refused, naming the product that cannot.
Verified by baking, not by reading. Modules without Scribunto now exit 1 with no output written. Scribunto with no engine was tested by mounting an empty ini over the image's `docker-php-ext-luasandbox.ini`, which is as close to the binary as this machine gets, and it exits 1 with the message about the binary. A site with Scribunto and an engine bakes as before; a site with no Lua at all is told nothing, which matters because that is most sites and the binary is what they are pointed at.
Engine detection is `luasandbox`, then a configured `luaPath`, then `lua5.1` or `lua` on PATH, so a hand-rolled install with the standalone engine is not refused for no reason. The image has no `lua` on PATH, which the test above also proves: the check fired.
## The page
`Lua modules`, in the docs section between Extensions and JavaScript, in English and Korean. It says how to enable it, that a module file carries no `.wikitext` marker because its content model is Lua, what reaches the published site, and what the two refusals look like. `Standalone binary` gains a "What it cannot do" section, since it claimed its output was the same as the image's, and that is now known to be false in one way. `Extensions` points at the new page.
The page demonstrates itself: it shows `Module:Example` in full, invokes it, and prints the answer. The module returns the title of the page it ran on and no prose, because a module's return value is not a translation unit and would reach a reader of any language in whatever language it was written in.
## What holds it
`smoke.yml` asserts the module's answer is in the exported page in both languages, and that nothing named `Module*` was exported. It cannot assert the absence of `{{#invoke:` anywhere, because the new page documents the syntax inside `<nowiki>` and so contains it legitimately -- the positive assertion is the one that distinguishes rendering from leaking.
Ran locally against a real bake of this site: both languages carry the answer, `<code>Lua modules</code>` and `<code>Lua modules/ko</code>`, the module is not a page, the language-bar count still matches what the source predicts (414), and two bakes are byte-identical, so a module does not cost reproducibility. The prevnext chain reads Skins, Extensions, Lua modules, JavaScript, Searching in English, and shows `Lua 모듈` in Korean.
Unit suite: `OK (196 tests, 299 assertions)` against main, eleven of them new.
## A consequence worth stating
This documentation site now has a Lua module, so the standalone binary cannot bake it. Nothing in CI notices, because nothing bakes anything with the binary -- `binary.yml` builds it and runs `./wikven version`. That gap is real and is not this change's to close.
Closes #465.
---
_Generated by [Claude Code](https://claude.ai/code/session_935f02d1)_
Co-authored-by: Claude <noreply@anthropic.com>
A source tree with Module: files and no Scribunto in extensions ended the build. That is deciding for the site: what those files are is a guess read off a name, and they may be on their way in, on their way out, or kept for something else. The bake now goes on and says what it sees -- the invocations stay in the pages as their own source text, and a module named with the .wikitext marker is exported as a page. The other rule stays fatal, because it is not the same thing. A site that lists Scribunto where no engine can run it asked for Lua in so many words, and this build cannot give it; publishing braces instead is worse than being told no. So Scribunto::problem() narrows to the engine case and a new warning() carries the modules case, and build.php's check is no longer only an assertion.
This was referenced Aug 19, 2026
lens0021
added a commit
that referenced
this pull request
Aug 20, 2026
… that found (#481) Nothing had ever baked anything with the standalone binary. `binary.yml` built it and ran `./wikven version` — which proves the static link, since static linking fails at execution rather than compilation, but proves nothing about the product — and `smoke.yml` only ever uses the Docker image. So a site the image can bake and the binary cannot had never been caught by anything, which is how this repository's own documentation came to hold a Lua module the binary was documented as unable to render. This adds the missing job, fixes the three bugs it found on its first runs, and rewrites the documentation those runs proved wrong. Closes #465's remaining gap; the follow-up #473 flagged. ## The job A `bake` job hanging off `binary` with `needs:`, so it runs exactly when a binary was built. Not folded into `smoke.yml`: that job belongs to the image, it is a required check, and Lua is a niche of a niche. It runs on a plain runner with the binary downloaded as an artifact — the way somebody who fetched a release meets it, with no Docker and nothing of wikven's installed. Three assertions, in this order, because the order is load-bearing: | | asserts | where | |---|---|---| | a bake with no `src` | a refused bake exits non-zero | both | | Scribunto's bundled lua, nothing configured, **before anything is installed** | the out-of-the-box claim | x86_64 | | `luaPath` at an interpreter the host provides | the arrangement that works anywhere | both | Putting the bundled-lua bake after the install step made it pass for the wrong reason on the first run: `/usr/bin/lua5.1` was on `PATH`, which is one of the things `luaEngineAvailable()` looks for, so the bake was never asked whether it can manage without one. A pull request builds x86_64 only; the arm64 leg runs on the nightly and on releases, which is where an arm binary exists at all. It was exercised for this branch on the throwaway #482. ## What it found **1. A relative `WIKVEN_WORKDIR` broke the skin pass.** The first bake ever run got through the import and the job queue and died: ``` --- vector-2022 pass failed (exit 1) --- DBQueryError ... Error 1: no such table: page Query: SELECT MIN(page_id) FROM "page" #9 .../extensions/Wikven/maintenance/build.php(842): RebuildFileCache->execute() ``` A skin pass is spawned with the MediaWiki root as its working directory — `proc_open(..., $GLOBALS['IP'], ...)`, because the binary's php-cli needs script paths relative to the install root — and inherits `WIKVEN_WORKDIR`. Inside the binary that root is the temporary directory the embedded app is unpacked into, so a relative value means one place in the orchestrator and another in every pass: the pass opened an empty database beside the unpacked app instead of the one just filled. Not a corner case — it is what `binary.Dockerfile`'s own usage note documents, `WIKVEN_WORKDIR=. ./wikven build`. Under Docker the workdir is `/workspace`, absolute, which is why it never showed up. Resolved with `realpath()`. **2. The engine check refused sites it could have baked.** `luaEngineAvailable()` looked for `luasandbox`, a configured `luaPath`, and `lua` on `PATH`, and never for the interpreter Scribunto carries in its own source tree — which is what `luastandalone` runs when `luaPath` is null, and what `luaautodetect` falls back to when `luasandbox` is absent. It looks for it now, and **runs** it rather than trusting `is_executable()`: `LuaStandaloneInterpreter` picks among the bundled binaries by `PHP_OS` and `PHP_INT_SIZE` and never by architecture, so on arm the file it selects is an x86-64 ELF that exists, is executable, and cannot start. Executing it is what makes mirroring that choice safe — a wrong guess answers no rather than promising Lua that never arrives. **3. Every failed build exited 0.** Caught by the assertion added here, on both architectures: ``` wikven: no source directory at /home/runner/work/wikven/wikven/empty/src a bake with no source directory exited 0 ``` `reexec()` returned the child's exit code with a nil error, meaning to propagate the status without Caddy printing its own line over the child's diagnostic. Caddy 2.11.4, `cmd/cobra.go`: ```go status, err := f(Flags{cmd.Flags()}) if status > 1 { cmd.SilenceErrors = true return &exitError{ExitCode: status, Err: err} } return err ``` A status of exactly 1 is never turned into an exit status, and an ordinary failed build is 1. So a Makefile, a CI job or a deploy script running `wikven build` read every refusal as a success — the Lua refusal, a missing source directory, a failed skin pass, all of them. `reexec()` exits instead of returning; returning `(1, err)` would exit 1 but print Caddy's error line over the diagnostic the child already wrote, which is what the original nil was avoiding. ## The documentation `Lua modules` and `Standalone binary` both said the binary cannot render Lua. Written before anything had baked with it, and wrong in two cases of three: | Product | Engine | What you configure | |---|---|---| | Docker image | `luasandbox`, compiled into its PHP | nothing | | Standalone binary, x86-64 | the interpreter Scribunto ships | nothing | | Standalone binary, arm64 | an interpreter you install | `luaPath` | Every row is measured rather than reasoned. `lua5.1 5.1.5-9build2` installs from `universe` on the arm runner, no `luajit` fallback needed, and the bake with `luaPath` pointed at it finished with the module's answer in the page. arm64 with nothing installed refuses cleanly — no output written, and now a non-zero status. `Standalone binary`'s "What it cannot do" becomes "What it asks of the host", which is what the section now describes. Korean follows, restamped. ## Not done here `binary` is still 504s and still off the critical path of nothing, which is #479's problem, not this one's. --------- Co-authored-by: Claude <noreply@anthropic.com>
lens0021
added a commit
that referenced
this pull request
Aug 20, 2026
Closes #455. ## The duplication The order of the documentation was written down twice: as `MediaWiki:Sidebar`, and again as the arguments of the `{{prevnext}}` at the foot of each of twenty-one pages. Nothing checked that the two agreed, and twice they did not — a page reached the sidebar with no call naming it, and the last page of the chain had no row at all, because a lone argument read as the *next* step and no positional form could spell "nothing after this". They agree today, as it happens: twenty sidebar entries, and the chain covers all twenty in the sidebar's order with `index` at the head. That is the state to keep, not a state to rely on — both past divergences were repaired by hand, and the shape that produced them was untouched. ## The change `Module:Sequence` reads the sidebar, so `{{prevnext}}` takes no arguments. Adding a page is now two edits instead of three, and the third was the invisible one. The direction is forced, as #455 worked out: `MediaWiki:Sidebar` is read line by line by the skin rather than parsed as wikitext, so it can call nothing; but it is an ordinary page, so anything can read it. What the sidebar contributes is **order alone**. Labels stay in `Template:prevnext/label`, which reads each target's own translated title — so none of this reaches the translations. A prevnext sits outside the `<translate>` tags, and no `ko.wikitext` changed in this branch. ## The two decisions #455 left open * **The group boundary.** The sequence keeps crossing from the docs group into the references group, as it does today. A reader who reaches the end of the first group is better sent on than stopped. * **`index`.** It heads the sequence without being a sidebar entry, so the module prepends it. The alternative — a sidebar entry for it — would duplicate what every skin's logo already points at, which is why the sidebar's own comment says it has none. ## The condition #455 set > **check first that it runs in the image**: Scribunto needs either the LuaSandbox PHP extension or a standalone `lua` binary, and neither is obviously present in the Alpine-based image. If it is missing, this proposal costs a Dockerfile change, and that changes the trade. It runs. The base image does `pecl install LuaSandbox-4.1.2`, and this site has rendered `Module:Example` since #473. **No Dockerfile change**, which is what tips this over the alternative — #455's cheaper option was a check that asserts the two copies agree, and a check that watches a duplication is worth less than not having the duplication, once the module is free. ## What holds it `smoke` gets what the old shape got wrong, stated as three assertions: every page the sidebar names has a navigation row, `index` has no previous link, and `Licenses` has no next. The existing check that `Searching/ko`'s row carries Translating's *Korean* title still stands and now also proves the module renders at all. Draft because the module cannot be run here — no Docker on this machine, so the first real execution is CI's bake. --- _Generated by [Claude Code](https://claude.ai/code/session_01Uv1RzRurUH6wrgV5E9PESQ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scribunto is ordinary equipment on a MediaWiki wiki, and wikven's two products disagreed about it silently. Measured on a bake of one page invoking one module, every one of these exited 0:
Module:ExampleLua says helloModule:Example{{#invoke:Greet|hello}}Module:Example.wikitext{{#invoke:Greet|hello}}Module%3AGreet.htmlThree of those four ways to have a Lua module published a site with braces where the page meant to say something, and one exported the module's own Lua source as a page. Nothing said a word. #465 guessed this would show up as "red module errors"; raw wikitext is worse, because it looks like someone's typo rather than a missing engine.
The answer, per the decision on #465
Supported under the Docker image, not in the standalone binary. That is not a preference. static-php-cli, which builds the binary's PHP, offers no Lua extension among the ~130 it supports, and Scribunto's other engine shells out to a
luaprogram that a single executable has no way to ship. Checked against upstream's own list rather than assumed.One refusal and one warning
includes/Scribunto.phpholds both rules;build.phpruns them beforeclearOutputDirectory, so a site that is refused still has its last bake on disk.Scribuntowhere no engine can run it → refused, naming the product that cannot. The site asked for Lua in so many words and this build cannot give it; publishing braces instead is worse than being told no.Module:files with noScribuntoinextensions→ warned about, naming the files, and the bake goes on. The site never asked for Lua, and what those files are is a guess read off a name: they may be on their way in, on their way out, or kept for something else. Refusing there decides for the site, which is not this build's call. So it says what it sees — the invocations stay in the pages as their own source text, and a module named with the.wikitextmarker is exported as a page — and lets the bake finish.A site with no Lua at all is told nothing, which matters: that is most sites, and the binary is what they are pointed at.
Verified by baking, not by reading. The refusal:
The engine-missing case was tested by mounting an empty ini over the image's
docker-php-ext-luasandbox.ini, which is as close to the binary as this machine gets:That run also proves the image has no
luaonPATH: engine detection triesluasandbox, then a configuredluaPath, thenlua5.1/luaonPATH, and it still fired. A hand-rolled install with the standalone engine is therefore not refused for no reason.The modules-without-Scribunto case was baked while it was still a refusal (
bake exit: 1, noindex.html, message namingModule:Greet). Since it became a warning it is covered by the unit suite and not by a fresh bake — the bake host here has no Docker. What changed is which of the two functions produces the string and whetherbuild.phpcallsfatalError()oroutput()with it; the message and the file detection are the same code that bake exercised.The page
Lua modules, between Extensions and JavaScript, in English and Korean. How to enable it, why a module file carries no.wikitextmarker (its content model is Lua), what reaches the published site, and what each of the two mismatches does.It demonstrates itself: it shows
Module:Examplein full, invokes it, and prints the answer. The module returns the title of the page it ran on and no prose, because a module's return value is not a translation unit and would reach a reader of any language in whatever language it was written in — the sentence around it carries the words instead.Standalone binarygains a "What it cannot do" section: it claimed its output was the same as the image's, which is now known to be false in exactly one way.Extensionspoints at the new page.Developmentdocuments the new build step.What holds it
smoke.ymlasserts the module's answer is in the exported page in both languages, and that nothing namedModule*was exported.It deliberately does not assert the absence of
{{#invoke:in the output. The new page documents the syntax inside<syntaxhighlight>and so contains it legitimately — confirmed in the bake, three occurrences. The positive assertion is the one that tells rendering from leaking.Ran locally against a real bake of this site:
Two bakes byte-identical, so a module costs no reproducibility. The prevnext chain reads Skins → Extensions → Lua modules → JavaScript → Searching, and shows
Lua 모듈in Korean, which incidentally exercises #466 on a brand-new page.translate checkreports everything up to date.A consequence worth stating plainly
This documentation site now has a Lua module, so the standalone binary cannot bake it. Nothing in CI notices, because nothing bakes anything with the binary:
binary.ymlbuilds it and runs./wikven version. That gap is real, it is not this change's to close, and it is worth its own issue.Closes #465.