WikiWire is a GitHub Action that syncs changed files under modules/ and templates/ to a MediaWiki site via the Action API. Credentials are supplied only through the action inputs (or workflow secrets), never through the config file.
Wiki content lives under a path segment (second directory under modules/ or templates/):
- Modules:
modules/<path_segment>/<root_name>/… - Templates:
templates/<path_segment>/<root_name>/…
Ideally <path_segment> is the site’s host in wikiwire.toml, but can also be its id value, if no host is set. Using the host value instead removes any ambiguity. That keeps a stable id in config while the repo folder can stay a hostname.
Shared bucket (optional): If shared = true in wikiwire.toml, modules/shared/ and templates/shared/ are synced to every configured site. Wiki titles are the same as for a single site (the shared segment is not part of the title). When shared is false, paths under modules/shared/ or templates/shared/ cause the action to fail with a clear error. If you want to name a subfolder shared but don't want to use the first-party WikiWire support, name the folder _shared instead.
Example:
modules/obbywiki.com/GroupLink/GroupLink.module.lua
modules/obbywiki.com/GroupLink/doc.wikitext
modules/obbywiki.com/GroupLink/styles.css
modules/obbywiki.com/GroupLink/i18n/en.json
templates/obbywiki.com/Infobox/Infobox.template.wikitext
templates/obbywiki.com/MonthNav/MonthNav.template.wikitext
templates/obbywiki.com/MonthNav/styles.css
modules/shared/CommonUtil/CommonUtil.module.lua
You can see and use our live repository at https://github.com/obbywiki/modules.
<path_segment>must match a site’sidorhost, or be the literalshared(whenshared = true).<root_name>is the module or template root (e.g.GroupLink). For the main module file and template file, the basename in the filename must match<root_name>.
Any path under modules/ or templates/ that contains a path component starting with _ is skipped (not synced). Examples: modules/_legacy/..., modules/example.com/MyModule/_draft/example.wikitext, modules/example.com/shared/_imported/....
| Root | Repository path | Wiki title | Content model |
|---|---|---|---|
modules |
modules/<path_segment>/<root>/<root>.module.lua |
Module:<root> |
scribunto |
modules |
modules/<path_segment>/<root>/doc.wikitext |
Module:<root>/doc |
wikitext |
modules |
modules/<path_segment>/<root>/<any other path> |
Module:<root>/<any other path> |
See below |
templates |
templates/<path_segment>/<root>/<root>.template.wikitext |
Template:<root> |
wikitext |
templates |
templates/<path_segment>/<root>/doc.wikitext |
Template:<root>/doc |
wikitext |
templates |
templates/<path_segment>/<root>/<any other path> |
Template:<root>/<any other path> |
See below |
Any other file under modules/<path_segment>/<root>/ maps 1:1: the wiki subpage path is exactly the relative path under <root>/, including nested directories (for example i18n/en.json becomes Module:GroupLink/i18n/en.json).
The same 1:1 rule applies under templates/<path_segment>/<root>/ for every path except the main <root>.template.wikitext (which maps to the bare Template:<root>) and doc.wikitext (which maps to Template:<root>/doc). For example styles.css becomes Template:MonthNav/styles.css.
Templates synced to Template: must live under templates/, not modules/. You can still use regular wikitext files under a template root like any other subpath.
Suffix matching is ordered; the first match wins:
| Pattern | Content model |
|---|---|
*.template.wikitext |
(invalid under modules/; the action will fail) |
*.module.lua |
scribunto |
*.module.luau |
scribunto |
*.wikitext |
wikitext |
*.css |
Per-site css_content_model in wikiwire.toml (default sanitized-css) |
*.json |
json |
| Anything else | Error: unsupported extension |
(TODO: these should be ignored instead, such as README.md)
Suffix matching uses the same order as under modules/, with one restriction: *.module.lua and *.module.luau are invalid under templates/ (Scribunto modules must live under modules/). The main page must be <root>.template.wikitext at templates/<path_segment>/<root>/<root>.template.wikitext.
| Pattern | Content model |
|---|---|
*.module.lua |
(invalid under templates/; the action will fail) |
*.module.luau |
(invalid under templates/; the action will fail) |
*.wikitext |
wikitext |
*.css |
Per-site css_content_model in wikiwire.toml (default sanitized-css) |
*.json |
json |
| Anything else | Error: unsupported extension |
Some wikis may reject certain content models on Template: subpages; in that case the Action API returns an error, similar to unusual Module: subpages.
Place at the repository root unless you override with the config_path action input.
Did you know? The Better GitHub File Icons extension supports wikiwire files! Both wikiwire.toml and .wikiwireignore will use the wikiwire logo!
| Key | Type | Required | Description |
|---|---|---|---|
version |
integer | no | Config schema version; default 1. Reserved for future use. |
shared |
boolean | no | If true, enables modules/shared/ and templates/shared/, synced to every [[sites]] entry. Default false. |
| Key | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Stable site key (sessions, logs). Must be unique across rows. |
host |
string | no | Directory name under modules/ and templates/. If omitted, defaults to id. Must be unique across sites. Cannot be shared when shared = true (that name is reserved). |
api |
string | yes | Full MediaWiki API URL, e.g. https://example.org/w/api.php. |
dry_run |
boolean | no | If true, only log planned edits; no action=edit requests for this site. |
default_branch |
string | no | If set, the action skips syncing when the workflow ref is not this branch (e.g. refs/heads/main). |
css_content_model |
string | no | Content model for *.css files under modules/ and templates/. Default sanitized-css. Some wikis need css. |
Example:
# This is a global WikiWire configuration file, a CI action which automatically syncs and uploads modules and templates from a Git repo towards a production or upstream MediaWiki instance via bot passwords and the MediaWiki Action API.
# Learn more: https://github.com/obbywiki/wikiwire
version = 1
shared = true
[[sites]]
id = "obbywiki.com"
api = "https://obbywiki.com/w/api.php"
[[sites]]
id = "dev"
host = "dev.example.org"
api = "https://dev.example.org/w/api.php"
dry_run = true
default_branch = "main"
css_content_model = "css"Credentials are not stored in this file. Use action inputs backed by secrets.
Optional file at the repository root (override with ignore_path). Patterns are relative to the repo root and follow .gitignore semantics (comments #, blank lines ignored; ** and negation supported via the ignore package).
Ignored paths are skipped after change detection and never uploaded. Ignoring a path does not delete anything on the wiki.
Example:
# Legacy copies kept in git only
modules/obbywiki.com/ObbyGameInfobox/ObbyGameInfoboxLegacy.module.lua
modules/obbywiki.com/ObbyGameInfobox/ObbyGameInfoboxLegacy.template.wikitext
# It is recommended to include any file you don't want WikiWire to sync
**/*README.md
**/*requirements.txtPlease note that WikiWire is currently a BETA and this shouldn't be required in the future. Be advised that WikiWire doesn't support markdown or txt files, so syncing them will likely result in an error with-in your CI.
| Input | Required | Default | Description |
|---|---|---|---|
username |
no | "" |
Default bot username for sites not listed in site_credentials. With Bot passwords, use UserName@BotPasswordName. |
password |
no | "" |
Default bot password for sites not listed in site_credentials. |
site_credentials |
no | "" |
JSON object whose keys are site id values from wikiwire.toml (not host). Each value must be {"username":"…","password":"…"}. Overrides the global username / password for that site. Keys that do not match any configured site produce a workflow warning. |
config_path |
no | wikiwire.toml |
Path to the TOML config. |
ignore_path |
no | .wikiwireignore |
Path to the ignore file (may be missing). |
dry_run |
no | false |
If true, no edits are sent (site-level dry_run in TOML still applies per site). |
sync_all |
no | false |
If true, sync every file under modules/ and templates/ from the workspace instead of using commit diffs. Requires a prior checkout of the repo. Not recommended as this may potentially be destructive. |
dark_lua_compat |
no | "" |
Deprecated and ignored. Luau modules are always synced as Scribunto. Add **/*.module.luau to your .wikiwireignore file instead. |
Use a workflow permissions block with at least contents: read so the default GITHUB_TOKEN can call the compare API.
Every site that performs a real (non–dry-run) sync must resolve to a username and password: either the global inputs or a matching entry in site_credentials.
name: WikiWire
on:
push:
branches: [main]
paths:
- 'modules/**'
- 'modules/*'
- 'templates/**'
- 'templates/*'
jobs:
wikiwire:
runs-on: ubuntu-latest
name: Sync files to upstream MediaWiki
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: obbywiki/wikiwire@latest
with:
username: WikiWireBot@BotPasswordNameHere
password: ${{ secrets.WIKI_PASSWORD }}DarkLua support is currently experimental and may be a bit finnicky.
WikiWire uploads whatever is in the checked-out workspace under modules/ and templates/. If you generate or transform Lua/Luau in CI (for example with Darklua), run that step before WikiWire.
For push syncs (default sync_all: false), when the GitHub diff includes a path modules/**/*.module.luau, WikiWire also adds the sibling *.module.lua path (same basename, .module.lua instead of .module.luau) if that file exists in the workspace. That covers CI that runs Darklua on changed Luau and writes *.module.lua without committing it—the Lua file does not have to appear in the commit diff. Put **/*.module.luau in .wikiwireignore if you want only the generated Lua synced to the wiki (both map to the same Module: title).
Use sync_all: "true" when you need to upload from the whole tree (for example many generated files that are not tied to changed *.module.luau paths in the diff).
Example (outline):
steps:
- uses: actions/checkout@v4
# install your tooling (darklua, compiler, etc.)
# run darklua so modules/** contains the final output
- uses: obbywiki/wikiwire@latest
with:
sync_all: "true"
username: WikiWireBot@BotPasswordNameHere
password: ${{ secrets.WIKI_PASSWORD }}You can avoid sync_all in two ways:
-
Commit generated Lua: Keep
*.module.luau, run Darklua, commit sibling*.module.lua. The diff usually lists both; add**/*.module.luauto.wikiwireignoreif you want only the Lua file uploaded. -
CI-only Lua: In the same job, checkout → Darklua (so
*.module.luaexists on disk) → WikiWire. The push diff need only include the changed*.module.luau; WikiWire adds the sibling*.module.luapath when that file exists in the workspace. Still use.wikiwireignorefor*.module.luauso the wiki receives the Lua output, not the Luau source.
Example workflow (outline; first variant assumes *.module.lua outputs are committed alongside sources):
name: WikiWire (with Darklua)
on:
push:
branches: [main]
paths:
- 'modules/**'
- 'templates/**'
jobs:
darklua_check:
name: Verify Darklua outputs are up-to-date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Install Darklua # (pick your preferred installation method) and make sure it's up to date
run: |
wget https://github.com/seaofvoices/darklua/releases/download/v0.18.0/darklua-linux-x86_64.zip
unzip darklua-linux-x86_64.zip
chmod +x darklua
- name: Regenerate Lua outputs for changed Luau modules
shell: bash
run: |
set -euo pipefail
changed_files="$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}")"
while IFS= read -r path; do
[[ "$path" == modules/**/*.module.luau ]] || continue
out_path="${path%.module.luau}.module.lua"
mkdir -p "$(dirname "$out_path")"
# Example CLI shape (adjust flags to your darklua config)
darklua process --config .darklua.json "$path" "$out_path"
done <<< "$changed_files"
- name: Fail if outputs were not committed
run: git diff --exit-code
wikiwire:
name: Sync files to upstream MediaWiki
runs-on: ubuntu-latest
needs: [darklua_check]
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: obbywiki/wikiwire@latest
with:
# default: sync_all: "false"
username: WikiWireBot@BotPasswordNameHere
password: ${{ secrets.WIKI_PASSWORD }}Use site_credentials with one JSON object. Interpolate secrets per field, or store the entire JSON in a single secret and pass site_credentials: ${{ secrets.WIKIWIRE_SITE_CREDENTIALS_JSON }}.
- uses: obbywiki/wikiwire@latest
with:
site_credentials: |
{
"production.example": {
"username": "WikiWireBot@prod",
"password": "${{ secrets.WIKI_PASSWORD_PROD }}"
},
"dev": {
"username": "WikiWireBot@dev",
"password": "${{ secrets.WIKI_PASSWORD_DEV }}"
}
}You can combine global username / password with site_credentials: only sites with an entry in the JSON use the per-site pair; all others use the defaults.
- Store
passwordand per-site passwords in GitHub secrets, not in committed workflow YAML (except${{ secrets.* }}references). - Prefer Bot passwords with the minimum rights needed (
editpage,highvolume, etc.). - The config file must remain free of secrets so it can be committed safely.
- Deletes: Removing a file from git does not delete the wiki page.
- Renames: Appear as delete + add; see deletes.
- Initial push: When GitHub sends an all-zero
beforeSHA, the action uses the singlepushhead commit’s file list instead ofcompareCommits. - Branches: Use per-site
default_branchor workflowon.push.branchesto avoid syncing from unintended branches.
After changing src/, run pnpm install and pnpm build so dist/index.js is updated before tagging a release consumers pin to.