wowlua package builds a distributable zip. wowlua publish builds, packages
and uploads it to every configured publish target: CurseForge, Wago
Addons, WoWInterface, GitHub Releases, and (for libraries) the wowlua registry.
The upload protocols below were verified on 2026-10-11 against the sites'
documentation, their live endpoints, and the BigWigsMods packager, the de-facto
standard WoW release tool. Endpoints are data in the compiler and can be
overridden per target (api-url) if a site changes them.
11.1 Configuration
{ "publish": { "changelog": "CHANGELOG.md", "changelog-format": "markdown", "release-type": "auto", "archive-maps": true, "targets": { "curseforge": { "type": "curseforge", "project-id": 123456 }, "cf-nolib": { "type": "curseforge", "project-id": 123457, "nolib": true }, "wago": { "type": "wago", "project-id": "a1b2c3d4" }, "wowi": { "type": "wowinterface", "project-id": 5678 }, "github": { "type": "github", "repository": "owner/my-addon", "minify": "max" } } }}Targets are freely named. The type field selects the site, so a project
can have several targets for one site (another project, a nolib variant).
All target fields, with types and defaults, are listed in
Manifest §9.11.
11.1.1 Target builds
- Each target builds with its profile plus its own overrides (
minify,bundle,flavors,nolib). Addon sites (CurseForge, Wago, WoWInterface) build unminified unless the target setsminify, which W6010 warns about (Optimization §15.4.4). Targets with identical effective settings share one build and one zip. - Publish builds are written to
dist/targets/<target>/<Addon>/(a shared build lives under the first target's name, in manifest order), so targets with different settings never overwrite each other or the profile builds indist/<profile>/. wowlua publishimplies--locked: a publish never changeswowlua.lock.
11.1.2 Release type
With "auto" (BigWigs packager convention):
- the build is not on a VCS tag, or the working tree is dirty:
alpha; - the tag contains
alpha:alpha; the tag containsbeta:beta; - otherwise:
release.
11.1.3 Tokens
Tokens are never stored in wowlua.json. They come, in order of precedence,
from the target's environment variable, from the OS credential store (filled
by wowlua login <type>), or, where no credential store exists, from
config.json (Package manager §10.11).
A missing token is error E4201. Default variables:
| Target | Default env vars, in order |
|---|---|
| CurseForge | CF_API_TOKEN, CF_API_KEY |
| Wago | WAGO_API_TOKEN |
| WoWInterface | WOWI_API_TOKEN |
| GitHub | GITHUB_TOKEN, GH_API_TOKEN, GITHUB_API_TOKEN, GITHUB_OAUTH |
| wowlua registry | WOWLUA_REGISTRY_TOKEN |
Besides GITHUB_TOKEN, the names are the BigWigs packager's, so existing CI
secrets keep working. A converted packager project reads exactly the
packager's variables (not GITHUB_TOKEN), and also its .env file in the
repository root, whose values win over the environment like the packager's
(decision T158).
11.2 Packaging
wowlua package produces zips in dist/packages/, named
<Name>-<build.version>[-<target>][-<flavor>][-nolib].zip:
-<target>is added when publish targets produce different zips,-<flavor>when a target usessplit-flavors,-nolibfor nolib packages (§11.2.2).
MyAddon-1.4.2.zip└── MyAddon/ # the complete AddOn folder of the build ├── MyAddon.toc ├── MyAddon.standard.lua ├── … ├── LICENSES.txt └── LICENSE # copied from the package root if present- The zip's top-level folder is the AddOn folder name. With
publish.bundle-addonsin a workspace root, the root's targets get one zip with several AddOn folders (decision T160): all addon members, or the listed ones. The first listed member (withtrue: the first default member) is the main one: the zip is<Main>-<its build.version>…zip, and it gives the changelog and the release name; CurseForge relations are every bundled member's. The root's targets are then not shared with the members, whose own targets keep their own zips.packageandpublishat the root load the bundled members even when they are not default members; with-por inside a member, there is no bundle. - Source maps, tests, dev files and
dist/.cacheare never included. - Zip entries use
/, are sorted, and have timestamps normalized (toSOURCE_DATE_EPOCHor the commit time), so packages are reproducible. - Display names on the sites use
build.version(Manifest §9.9.2).
A converted packager project (compat.packager-keywords) packages like the
BigWigs packager instead (decision T157): one zip
{package-name}-{project-version}{nolib}{classic}.zip (or compat.archive-name)
holding every AddOn folder of the repository, one per released game for
compat.per-game projects, and with compat.nolib-package a nolib zip that
only the GitHub target receives.
11.2.1 Changelog
changelog |
Content |
|---|---|
A path (default CHANGELOG.md if it exists) |
If the file has version headings (## 1.4.2, ## [1.4.2], # v1.4.2, …), only the section for package.version is used. Otherwise the whole file. |
"git" |
Generated from the commit subjects since the previous tag: a heading <Name> <build.version> followed by one list item per commit (- <subject> (<short hash>)), merge commits omitted |
null |
No changelog |
Per site, the changelog is sent as:
| Site | Format |
|---|---|
| CurseForge | changelogType = markdown, text or html, matching changelog-format |
| Wago | Markdown. text is sent as-is; html is converted to Markdown. |
| WoWInterface | BBCode by default, converted from Markdown (headings → [size]/[b], lists → [list][*], links → [url], code → [code], emphasis → [b]/[i]), or Markdown with the target's changelog-output: "markdown" for projects where the site enabled it |
| GitHub | Release body as Markdown (text and html are passed through) |
CurseForge requires a changelog for every file: publishing to CurseForge with no changelog is error E4210.
11.2.2 Nolib packages
A target with "nolib": true packages the addon without its lua-library
dependencies, for users who install those libraries as standalone addons (a
packager convention). The library files and their TOC lines are omitted, and
the libraries' standalone addon names (the dependency field addon-name,
default: the install folder name) are added to ## OptionalDeps. wowlua
libraries and standard-library modules are always embedded, because they are
private copies. On GitHub, nolib zips get "nolib": true entries in
release.json.
11.3 Game version mapping
Every site identifies supported game versions in its own way. The compiler
maps each flavor's interface numbers (e.g. 120100 → 12.1.0) to the site's
identifiers. When the exact version is unknown to the site, it falls back to the
highest version below it, or the highest version of that flavor, with warning
W4328 naming the site, flavor and chosen version. A flavor the site does not
know at all is omitted from that site's upload, with warning W4220. This
matches the BigWigs packager's behavior. The mapping tables (flavor ↔
site-specific names and IDs) ship with the compiler and are refreshed from the
sites' version endpoints at publish time.
11.4 CurseForge
| Upload | POST https://wow.curseforge.com/api/projects/{projectId}/upload-file |
| Auth | Header X-Api-Token: <token> |
| Body | multipart/form-data with fields metadata (JSON string) and file (zip) |
| Success | HTTP 200, {"id": <fileId>} |
| Game versions | GET https://wow.curseforge.com/api/game/wow/versions (authenticated). Entries are {id, gameVersionTypeID, name, slug}. |
Metadata sent:
{ "displayName": "1.4.2", "gameVersions": [12345, 12399], "releaseType": "release", "changelog": "…", "changelogType": "markdown", "relations": { "projects": [ { "slug": "ace3", "type": "embeddedLibrary" }, { "slug": "details", "type": "optionalDependency" } ] }}gameVersionscontains the version IDs of all included flavors (one file for all flavors). The flavor is matched bygameVersionTypeID(for example 517 = Retail). The type-ID table is part of the flavor table.relationsare derived automatically: embeddeddependencieswith acurseforge-slugbecomeembeddedLibrary(in nolib packages:requiredDependency);addon-dependencies.required/.optionalentries with acurseforge-slugbecomerequiredDependency/optionalDependency. Extra relations can be listed in the target'srelationsfield as{ "slug": "…", "type": "tool" | "incompatible" | "embeddedLibrary" | "requiredDependency" | "optionalDependency" }.releaseTypeis the release type (alpha,beta,release).- CurseForge requires a changelog for every file, so publishing to CurseForge without one is error E4210.
11.5 Wago Addons
| Upload | POST https://addons.wago.io/api/projects/{projectId}/version |
| Auth | Header Authorization: Bearer <token>, Accept: application/json |
| Body | multipart/form-data with fields metadata (JSON string) and file (zip) |
| Success | HTTP 200 or 201 |
| Game versions | GET https://addons.wago.io/api/data/game (public). Contains patches per flavor (retail, classic, bc, wotlk, cata, mop, …). |
Metadata sent:
{ "label": "1.4.2", "stability": "stable", "changelog": "…", "supported_retail_patches": ["12.1.0"], "supported_classic_patches": ["1.15.9"]}stabilityisstable,betaoralpha, mapped from the release type (release→stable).- One
supported_<flavor>_patchesarray per included flavor. Wago's documentation shows singularsupported_<flavor>_patchstrings, but the plural array form is the one the BigWigs packager uses and the live API publishes. wowlua sends the plural form.
11.6 WoWInterface
| Upload | POST https://api.wowinterface.com/addons/update |
| Auth | Header x-api-token: <token> |
| Body | multipart/form-data: id, version, compatible (comma-separated version IDs), changelog, archive (Yes/No), updatefile (zip) |
| Success | HTTP 202. Errors: 401 (not author), 403 (token or addon disabled), 404 (no addon), 405 (missing fields), 415 (file type), 500 (failed) |
| Game versions | GET https://api.wowinterface.com/addons/compatible.json (public), entries {id, name, game, interface, default} |
- The changelog is converted from Markdown to BBCode by default
(
changelog-outputon the target:"bbcode"or"markdown"). WoWInterface accepts Markdown only for projects where the site has enabled it. archive-previous(defaulttrue) controls whether the previous file is archived.- WoWInterface's version list does not cover every flavor. Flavors without a
matching entry are omitted from
compatible, with warning W4220. - WoWInterface has no release-type field. Following packager convention, only
builds on a VCS tag are uploaded:
alphabuilds are skipped with a note (setupload-untagged: trueon the target to upload them anyway), andbetabuilds are uploaded with the release type visible inversion.
11.7 GitHub Releases
| Create release | POST https://api.github.com/repos/{owner}/{repo}/releases (tag_name, target_commitish, name, body = changelog, draft from the target, prerelease from the target, by default true unless the release type is release) |
| Update release | PATCH …/releases/{id} when a release for the tag already exists |
| Upload asset | POST https://uploads.github.com/repos/{owner}/{repo}/releases/{id}/assets?name={file}. Raw body, Content-Type: application/zip (application/json for release.json). GitHub rejects duplicate names (HTTP 422), so an existing asset with the same name is deleted first (DELETE …/releases/assets/{asset_id}). |
| Auth | Header Authorization: Bearer <token> |
With release-json: true (default), a release.json asset is uploaded as well.
It is read by addon managers such as WowUp:
{ "releases": [ { "name": "MyAddon", "version": "1.4.2", "filename": "MyAddon-1.4.2.zip", "nolib": false, "metadata": [ { "flavor": "mainline", "interface": 120100 }, { "flavor": "classic", "interface": 11509 } ] } ]}- One
metadataentry per interface number of every included flavor. The flavor names are the packager's (mainline,classic,bcc,wrath,cata,mists, …), taken from the flavor table. - When a release is re-run, existing
release.jsonentries are merged byfilename. - With
archive-maps: true, each zip's source maps are uploaded as<zip name>-maps.zip(for exampleMyAddon-1.4.2-maps.zip), sowowlua tracecan decode reports from that version later. Converted packager projects upload them only with an explicitarchive-maps: true. - The release is named after the tag, and the token is sent as
Authorization: Bearer(tokenfor converted packager projects). - GitHub requires a tag. Publishing an untagged build to GitHub creates no
release unless
--tagis given (warning W4329 otherwise).
11.8 wowlua registry
For library, lua-library and types packages. Uploads the package archive
described in Package manager §10.10. Addons
cannot be published to the registry (E4230).
11.9 The publish command
wowlua publish [--target <name>…] [--dry-run] [--tag <tag>] [--release-type <t>] [--allow-dirty] [--no-test] [--deny-warnings] [-p <package>]- Verify a clean working tree (E4336), unless
--allow-dirty(which makes the release typealphaunder"auto"). Resolution runs with--locked. Every target's token must be present (E4201) before anything is uploaded. - Build every distinct target configuration and validate the output.
- Run
wowlua test, unless--no-test. - Package the zips.
- Upload to each target in parallel, with retries using exponential backoff for 5xx and network errors. A target that fails does not stop the others.
- Archive the source maps under
$WOWLUA_HOME/maps/<build id>/(and upload them to GitHub witharchive-maps). - Print a summary with the resulting file IDs and URLs. The exit code is non-zero if any target failed.
--tag <tag>. If the tag does not exist, wowlua creates an annotated tag at
HEAD locally (it never pushes git refs). The tag must equal package.version
with an optional leading v (E4319). The build is then treated as a tagged
build, and the GitHub release is created with tag_name = <tag> and
target_commitish = the HEAD commit, so GitHub creates the remote tag
together with the release. If the tag exists but points at another commit, the
publish stops (E4330).
--dry-run performs steps 1–4, prints the exact metadata each target would
receive (tokens redacted), and validates tokens with a read-only request where
the site offers one. It does not create a --tag tag.
Converted packager projects publish like the BigWigs packager (decision T158) instead of steps 1, 5 and 7's parallel uploads:
- no clean-tree check (the packager does not look at the working tree);
- per package (one per game for
compat.per-game), the targets upload in the packager's order: CurseForge, WoWInterface, Wago, GitHub. The CurseForge upload is tried up to ten times; if it still fails, the remaining uploads are aborted; - a target without a token is skipped with a note;
- the changelog is the packager's: the
manual-changelogfile (markup type frompublish.changelog-format, defaulttext) or theCHANGELOG.mdit generates, read from the built package. WoWInterface gets the manual Markdown changelog converted withpandocwhen it is installed (the packager's HTML-to-BBCode rules), else as it is, or its generated BBCode changelog..pkgmetawowi-convert-changelog: nobecomes the target'schangelog-output: "markdown",wowi-create-changelog: noitschangelog: null,wowi-archive-previous: noitsarchive-previous: false; - the GitHub release gets the package, its nolib package, and
release.jsonentries named after the project title with the tag as version; - on GitHub Actions, a branch push whose commit is tagged publishes nothing (the tag's run does), like the packager.