Starter Template & Pro Showcase¶
Everything you need to go from zero to published addon. A production-ready template, real-world examples from top addons, modern tooling, and an AI-friendly workflow that lets you build faster than ever.
1. The Perfect Starter Template¶
No existing addon template targets Midnight 12.0+. Ours does. It ships with the correct Interface version, Secret Values awareness, CI/CD, linting, AI instructions, and every file a modern addon needs.
What's Included (and Why)¶
| File | Purpose |
|---|---|
MyAddon.toc | Addon manifest — Interface 120001, SavedVariables, load order |
Core.lua | Entry point — namespace setup, event dispatcher, initialization |
Config.lua | SavedVariables defaults, migration system, settings panel |
Events.lua | Event handler registrations, combat deferral utilities |
UI.lua | Frame creation, layout, skinning helpers |
Utils.lua | Shared utilities, secret value helpers |
Locales/enUS.lua | Localization strings |
Libs/embeds.xml | Library loader (LibStub, CallbackHandler, AceDB) |
.pkgmeta | BigWigsMods/packager config — library externals, packaging rules |
.luacheckrc | Luacheck config with WoW API globals |
.luarc.json | VS Code LuaLS config for IntelliSense |
.editorconfig | Consistent tabs, line endings, charset |
.github/workflows/release.yml | Tag push auto-publishes to CurseForge + Wago + GitHub |
.github/workflows/lint.yml | Luacheck runs on every push and PR |
CLAUDE.md | AI instructions — 12.0 API rules, deprecated functions, patterns |
.cursorrules | Cursor IDE instructions (mirrors CLAUDE.md) |
Full File Tree¶
MyAddon/
├── .github/
│ └── workflows/
│ ├── release.yml # Tag → package → upload everywhere
│ └── lint.yml # Luacheck on push/PR
├── .vscode/
│ ├── extensions.json # Recommends ketho.wow-api + StyLua
│ └── settings.json # LuaLS Lua 5.1 config
├── Libs/ # Populated by .pkgmeta at build time
│ └── embeds.xml # XML library loader
├── Locales/
│ └── enUS.lua # English strings
├── MyAddon.toc # Interface: 120001
├── Core.lua # Namespace + init + event dispatch
├── Config.lua # SavedVariables + defaults + migration
├── Events.lua # Event registrations + combat deferral
├── UI.lua # Frames + skinning helpers
├── Utils.lua # Utilities + secret value helpers
├── CLAUDE.md # AI coding instructions
├── .cursorrules # Cursor IDE instructions
├── .luacheckrc # Lua linter config
├── .luarc.json # VS Code language server config
├── .pkgmeta # Packager config + library externals
├── .editorconfig # Editor settings
├── .gitignore # Ignores .release/, Libs/, *.zip
├── CHANGELOG.md
├── LICENSE
└── README.md
File Walk-Through¶
MyAddon.toc¶
The manifest that tells WoW how to load your addon.
## Interface: 120001
## Title: MyAddon
## Notes: A brief description of what your addon does
## Author: YourName
## Version: @project-version@
## SavedVariables: MyAddonDB
## OptionalDeps: LibStub, CallbackHandler-1.0, Ace3
## IconTexture: Interface\AddOns\MyAddon\icon
## X-Curse-Project-ID: 000000
## X-Wago-ID: aBcDeFgH
#@no-lib-strip@
Libs\embeds.xml
#@end-no-lib-strip@
Locales\enUS.lua
Core.lua
Utils.lua
Config.lua
Events.lua
UI.lua
@project-version@
Replaced with your git tag at build time by BigWigsMods/packager. Tag v1.0.0 → Version shows as v1.0.0.
#@no-lib-strip@
Tells the packager to strip library loading lines from the -nolib zip variant. Users who have standalone library addons installed won't double-load.
Core.lua — The Entry Point¶
local addonName, ns = ...
-- ─── Version detection ───────────────────────────────────
ns.isRetail = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE
ns.isMidnight = ns.isRetail and (select(4, GetBuildInfo()) >= 120000)
-- ─── Addon version ───────────────────────────────────────
ns.version = C_AddOns.GetAddOnMetadata(addonName, "Version") or "dev"
-- ─── Slash command ───────────────────────────────────────
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function(msg)
local cmd = strlower(strtrim(msg))
if cmd == "config" or cmd == "options" then
Settings.OpenToCategory(addonName)
else
print("|cff00ccff" .. addonName .. "|r v" .. ns.version)
print(" /myaddon config — Open settings")
end
end
-- ─── Initialization via EventUtil (modern 12.0 pattern) ──
EventUtil.ContinueOnAddOnLoaded(addonName, function()
ns.InitDB() -- Config.lua
ns.InitEvents() -- Events.lua
ns.InitUI() -- UI.lua
print("|cff00ccff" .. addonName .. "|r v" .. ns.version .. " loaded.")
end)
Utils.lua — Secret Value Helpers¶
local addonName, ns = ...
-- ─── Secret Values (Midnight 12.0) ──────────────────────
ns.SECRETS_ENABLED = type(issecretvalue) == "function"
function ns.SafeValue(val, fallback)
if ns.SECRETS_ENABLED and issecretvalue(val) then
return fallback
end
return val
end
-- ─── Combat deferral ─────────────────────────────────────
ns.combatQueue = {}
function ns.AfterCombat(fn)
if InCombatLockdown() then
table.insert(ns.combatQueue, fn)
else
fn()
end
end
function ns.FlushCombatQueue()
for _, fn in ipairs(ns.combatQueue) do
fn()
end
wipe(ns.combatQueue)
end
-- ─── Debounce ────────────────────────────────────────────
function ns.Debounce(delay, fn)
local timer
return function(...)
if timer then timer:Cancel() end
local args = {...}
timer = C_Timer.NewTimer(delay, function()
fn(unpack(args))
end)
end
end
Config.lua — SavedVariables with Migration¶
local addonName, ns = ...
ns.DEFAULTS = {
enabled = true,
scale = 1.0,
position = { point = "CENTER", x = 0, y = 0 },
theme = "dark",
_version = 1,
}
function ns.InitDB()
-- Merge saved data with defaults
MyAddonDB = MyAddonDB or {}
for key, default in pairs(ns.DEFAULTS) do
if MyAddonDB[key] == nil then
if type(default) == "table" then
MyAddonDB[key] = CopyTable(default)
else
MyAddonDB[key] = default
end
end
end
ns.db = MyAddonDB
ns.MigrateDB()
end
function ns.MigrateDB()
local db = ns.db
-- v2: example migration
if (db._version or 1) < 2 then
-- Rename old keys, restructure data, etc.
db._version = 2
end
end
-- ─── Settings panel (Blizzard Settings API) ──────────────
function ns.RegisterSettings()
local category = Settings.RegisterCanvasLayoutCategory(
ns.CreateSettingsPanel(), addonName
)
Settings.RegisterAddOnCategory(category)
end
Events.lua — Event Handling + Combat Safety¶
local addonName, ns = ...
function ns.InitEvents()
-- Flush combat queue when combat ends
EventRegistry:RegisterFrameEventAndCallback(
"PLAYER_REGEN_ENABLED", ns.FlushCombatQueue
)
-- Example: react to bag updates with debounce
local debouncedRefresh = ns.Debounce(0.05, function()
if ns.RefreshUI then ns.RefreshUI() end
end)
EventRegistry:RegisterFrameEventAndCallback(
"BAG_UPDATE_DELAYED", debouncedRefresh
)
end
.pkgmeta — Library Externals¶
package-as: MyAddon
enable-nolib-creation: yes
externals:
Libs/LibStub:
url: https://repos.curseforge.com/wow/libstub/trunk
tag: latest
Libs/CallbackHandler-1.0:
url: https://repos.curseforge.com/wow/callbackhandler/trunk/CallbackHandler-1.0
tag: latest
ignore:
- .github
- .vscode
- .luacheckrc
- .luarc.json
- .editorconfig
- .cursorrules
- CLAUDE.md
- README.md
- CHANGELOG.md
.github/workflows/release.yml¶
name: Package and release
on:
push:
tags:
- "**"
jobs:
release:
runs-on: ubuntu-latest
env:
CF_API_KEY: ${{ secrets.CF_API_KEY }}
WOWI_API_TOKEN: ${{ secrets.WOWI_API_TOKEN }}
WAGO_API_TOKEN: ${{ secrets.WAGO_API_TOKEN }}
GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for changelog generation
- uses: BigWigsMods/packager@v2
.github/workflows/lint.yml¶
name: Lint
on:
push:
branches: [main]
pull_request:
jobs:
luacheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: nebularg/actions-luacheck@v1
with:
args: "--no-color -q"
annotate: warning
.luacheckrc¶
std = "lua51"
max_line_length = false
exclude_files = { "Libs/", ".release/" }
ignore = {
"11./SLASH_", -- Setting slash command globals
"212/self", -- Unused self in mixin methods
"212/event", -- Unused event arg
}
globals = {
"MyAddonDB",
"SLASH_MYADDON1",
"SlashCmdList",
}
read_globals = {
"LibStub",
"CreateFrame", "UIParent", "GameTooltip",
"C_Timer", "C_AddOns", "C_EditMode",
"Mixin", "CreateFromMixins", "BackdropTemplateMixin",
"EventUtil", "EventRegistry", "Settings",
"InCombatLockdown", "GetBuildInfo", "GetLocale",
"hooksecurefunc", "issecretvalue",
"WOW_PROJECT_ID", "WOW_PROJECT_MAINLINE",
"strlower", "strtrim", "wipe", "CopyTable",
"print", "format", "select", "type", "unpack",
}
Auto-generated luacheckrc
Jayrgo/wow-luacheckrc auto-generates a complete .luacheckrc from Blizzard's own interface resources, covering every known WoW global. Far more comprehensive than hand-maintained configs.
2. Showcase: How The Pros Do It¶
BetterBags — Modern Clean Architecture¶
Cidan/BetterBags | MIT License | Midnight Ready
BetterBags/
├── .github/workflows/ # release + alpha + luacheck + busted tests + claude
├── CLAUDE.md, AGENTS.md # AI context files
├── BetterBags.toc # Interface: 120000, 120001
├── BetterBags_Vanilla.toc, _TBC.toc, _Mists.toc
├── annotations.lua # LuaCATS type annotations throughout
├── core/
│ ├── boot.lua # FIRST loaded — AceAddon:NewAddon + SetDefaultModuleState(false)
│ ├── init.lua # LAST loaded — OnInitialize/OnEnable
│ ├── async.lua # Coroutine batch rendering (frame-rate aware)
│ ├── context.lua # Go-style context system
│ ├── database.lua # AceDB wrapper with migration
│ ├── events.lua # Custom event layer (bucketing, deferred send)
│ └── pool.lua # Object pooling
├── data/ # Data layer (items, categories, refresh)
├── frames/ # UI components (~25 files)
├── views/ # View layer (bag, grid)
├── themes/ # Pluggable themes (default, simpledark, gw2, elvui)
├── integrations/ # Masque, ConsolePort, Pawn
├── config/ # Custom form framework (NOT AceConfig)
├── spec/ # Unit tests with busted
└── examples/plugin/ # Plugin example for third-party devs
What makes it great:
- Modules start disabled —
SetDefaultModuleState(false), enabled selectively inOnEnable - Coroutine batch rendering — adapts batch size to framerate, yields between batches
- Go-style contexts — passed as first arg to every callback for cancellation/timeout
- AI-ready — ships CLAUDE.md and AGENTS.md, has a
claude.ymlworkflow - Theme registry — pluggable themes let users completely restyle the UI
- Unit tests — busted test framework runs in CI
Key patterns to steal: Boot/Init split, debounced refresh with combat deferral, hidden parent taint isolation, object pooling.
Cell — Gold Standard Raid Frames¶
enderneko/Cell | 186 stars | Interface 120001
Cell/
├── Cell.toc + 5 flavor TOCs
├── Core.lua # Single global: _G.Cell = select(2, ...)
├── Utils.lua, Revise.lua # Utilities + DB migration
├── HideBlizzard.lua # Hides native raid frames
├── Libs/
│ ├── CallbackHandler.lua # Cell's OWN lightweight callback (not Ace3)
│ ├── PixelPerfect.lua # Own pixel-perfect helper
│ └── [External libs via .pkgmeta]
├── Defaults/ # Settings defaults per category, per flavor
├── Indicators/ # Visual indicators (9 files)
├── RaidFrames/ # Core: MainFrame, UnitButton, Groups/
├── Modules/ # Options UI
├── RaidDebuffs/ # Per-expansion debuff data
├── Utilities/ # BattleRes, BuffTracker, DeathReport, Marks
├── Widgets/ # Animation, ColorPicker, Tooltip
├── Locales/ # 11 languages
└── .snippets/ # 30+ user code snippet examples
What makes it great:
- Zero framework dependency — custom namespace, custom callbacks, custom SavedVariables. No Ace3.
- Self-dispatching events —
eventFrame[event](self, ...)pattern for zero-allocation dispatch - Custom callback system —
Cell.Fire("UpdateLayout")pub/sub without library overhead - Per-expansion data — RaidDebuffs split by expansion for easy maintenance
- 6 flavor TOCs with flavor-specific Lua files
Key patterns to steal: Custom lightweight callback system, self-dispatching event frame, SavedVariables validation without AceDB.
OmniCC — Minimal & Modern¶
tullamods/OmniCC | Secret Values handling
OmniCC/
├── OmniCC/ # Main addon (monorepo)
│ ├── OmniCC.toc # Interface: 110207, 50503, 38000, 20505, 11508
│ ├── main.lua # Entry point — EventUtil, no ADDON_LOADED frame!
│ ├── core/
│ │ ├── cooldown.lua # Hooks Cooldown widget METATABLE globally
│ │ ├── timer.lua # Timer pool with subscriber pattern
│ │ ├── display.lua # FontString displays
│ │ └── types.lua # LuaCATS annotations
│ ├── effects/ # Finish effects (alert, flare, pulse, shine)
│ ├── settings/ # Themes + pattern-matching rules
│ └── localization/
├── OmniCC_Config/ # Separate LoadOnDemand config addon
│ ├── OmniCC_Config.toc # LoadOnDemand: 1
│ └── options.lua, preview.lua, rules.lua, themes.lua
└── .pkgmeta # move-folders separates addons at build time
What makes it great:
- Modern EventUtil init —
EventUtil.ContinueOnAddOnLoaded()instead of the traditional ADDON_LOADED frame - Metatable-level hooks — hooks ALL cooldown widgets by hooking the Cooldown widget metatable
- Secret Values handling — detects
SECRETS_ENABLED, uses proxy frames to reverse-engineer durations - LoadOnDemand config — settings UI only loads when opened, reducing memory
- Monorepo with
move-folders— two addons in one repo, separated at build time
Key patterns to steal: EventUtil initialization, metatable hooking, LoadOnDemand config addon, Secret Values detection.
EditModeExpanded — Edit Mode Library¶
teelolws/EditModeExpanded | Interface 120001
EditModeExpanded/
├── Source/ # Moved to root at build time
│ ├── EditModeExpanded.toc
│ ├── EditModeExpanded.lua # Main init — EventUtil throughout
│ ├── FrameHandlers/ # One file per frame (35 handlers!)
│ │ ├── ActionBars.lua, Buffs.lua, ChatFrame.lua, Housing.lua
│ │ ├── Minimap.lua, ObjectiveTracker.lua, Pet.lua, Tooltip.lua, ...
│ ├── RegisterFrame.lua # Core frame registration API
│ ├── CombatManager.lua # continueAfterCombatEnds() utility
│ ├── HookOnce.lua # One-time hook utilities
│ └── libs/
│ └── EditModeExpanded-1.0/ # Reusable library for other addons
What makes it great:
- One handler per frame — 35 clean files in
FrameHandlers/, easy to find and modify - Reusable library —
EditModeExpanded-1.0via LibStub, other addons can register frames - EventUtil everywhere — zero manual frame event registration
- Lazy Blizzard addon loading —
EventUtil.ContinueOnAddOnLoaded("Blizzard_AuctionHouseUI", ...) hookScriptOnce/hookFuncOnce— clean one-time hooks
Key patterns to steal: File-per-frame organization, reusable library pattern, one-time hook utilities.
AdiBags — Modular Bag Addon¶
AdiAddons/AdiBags | Predecessor to BetterBags
AdiBags/
├── AdiBags.toc + 4 flavor TOCs
├── core/
│ ├── EventHandlers.lua # Loaded FIRST — creates custom event libraries
│ ├── Boot.lua # AceAddon:NewAddon — starts DISABLED
│ ├── Core.lua, Hooks.lua, Layout.lua, Filters.lua
│ ├── ItemDatabase.lua, Theme.lua, OO.lua
├── modules/ # 12 feature modules (Masque, Junk, ItemLevel, ...)
├── widgets/ # UI: ItemButton, Section, ContainerFrame, grid/
├── config/ # Options
├── AdiBags_Config/ # Separate LoadOnDemand config addon
└── tests/
What makes it great:
- Custom event libraries —
ABEvent-1.0andABBucket-1.0via LibStub, reusable by any addon - Starts disabled —
SetEnabledState(false), enables onPLAYER_ENTERING_WORLD, disables onPLAYER_LEAVING_WORLD - Plugin system — filter plugins register categorization rules:
AdiBags:RegisterFilter("MyFilter", priority) - Event buckets —
self:RegisterBucketEvent("BAG_UPDATE", 0.01, "BagUpdated")for debouncing
Key patterns to steal: Custom event libraries via LibStub, delayed enablement, plugin/filter system.
Plater — Nameplate Powerhouse¶
Tercioo/Plater-Nameplates | Interface 120001
Plater/
├── .github/workflows/
│ ├── CurseForge-Deployment.yml
│ └── TOCBump.yml # Daily cron → auto-updates Interface versions!
├── Plater.lua # Main addon (~5000 lines, event dispatch table)
├── Plater_DefaultSettings.lua # Massive defaults table
├── Plater_API.lua # Public API
├── Plater_ScriptLibrary.lua # User script sandbox
├── Plater_LoadFinished.lua # LAST loaded — sets up load-on-demand subsystems
├── libs/
│ └── DF/ # Details Framework — massive UI library
├── options/ # Load-on-demand option panels
├── locales/, fonts/, images/, masks/, media/, sounds/
What makes it great:
- Auto TOC bump — daily cron workflow auto-updates Interface versions
- Event dispatch table —
handlers["NAME_PLATE_CREATED"] = function(...) endwith per-handler CPU profiling - User script sandbox — lets users write custom Lua scripts that run in a controlled environment
- Dual namespace —
platerInternal(private) +Plater(global via Details Framework)
Key patterns to steal: Auto TOC bump workflow, event dispatch table with profiling, user script system.
3. Template Comparison Matrix¶
| Feature | Ours | kapresoft | layday | Kkthnx | Ketho HelloWorld |
|---|---|---|---|---|---|
Interface 120001 | |||||
| Secret Values awareness | |||||
| EventUtil init pattern | |||||
| BigWigsMods/packager CI | |||||
| Luacheck linting | |||||
.luarc.json (VS Code) | |||||
.pkgmeta externals | |||||
| SavedVariables + migration | |||||
| Combat deferral utilities | |||||
| CLAUDE.md (AI instructions) | |||||
| .cursorrules | |||||
| Multi-flavor TOC | |||||
| Ace3 integration | Optional | ||||
| Last updated | 2026 | 2024 | 2020 | 2023 (archived) | 2025 |
Bottom line: Existing templates are outdated. None target Midnight, none handle Secret Values, and none include AI tooling. Our template fills every gap.
4. The AI-Friendly Addon Project¶
Why Structure Matters for AI¶
AI tools generate correct code when they have the right context upfront. Every file in this template serves double duty:
| For Humans | For AI |
|---|---|
| Documentation | Context that prevents hallucination |
| Code examples | Patterns to extrapolate from |
| Linting rules | Guardrails against deprecated APIs |
| File structure | Signals for where to put new code |
CLAUDE.md Best Practices¶
The CLAUDE.md file is your behavioral specification for AI. It's the difference between Claude hallucinating removed APIs and generating working 12.0 code on the first try.
# MyAddon
## Critical: WoW 12.0 Midnight API Rules
### DO NOT USE (Removed/Broken):
- COMBAT_LOG_EVENT_UNFILTERED — removed in 12.0
- CombatLogGetCurrentEventInfo() — removed
- UnitHealth() for math — returns secret values during combat
### Secret Values (CRITICAL):
- During M+/PvP/encounters, combat data returns as opaque tokens
- Always check: if issecretvalue(val) then return end
- Use CurveObject/DurationObject for visual display
### Interface Version:
- Current: 120001 (Patch 12.0.1)
- Addons below 120000 will NOT load
## Architecture
### File Load Order (defined in .toc):
1. Libs/embeds.xml — External libraries
2. Locales/enUS.lua — Strings
3. Core.lua — Namespace + init
4. Utils.lua — Shared utilities
5. Config.lua — SavedVariables + defaults
6. Events.lua — Event handlers
7. UI.lua — Frame creation
### Namespace Pattern:
local addonName, ns = ...
-- All code uses ns.* to share between files
-- NEVER create globals except slash commands
## Coding Conventions
### Always:
- Use `local` for everything
- Check issecretvalue() before comparing combat values
- Check InCombatLockdown() before modifying secure frames
- Use hooksecurefunc() for post-hooks (never wrap secure functions)
- Use BAG_UPDATE_DELAYED (not BAG_UPDATE)
### Never:
- Generate CLEU handlers
- Call functions listed as removed in 12.0
- Create globals without explicit declaration in .luacheckrc
- Call SetScript() on Blizzard frames (use HookScript)
Claude Agents for Addon Dev¶
Place these in .claude/agents/ for specialized AI assistance:
| Agent File | Role |
|---|---|
wow-addon-coder.md | Writes Lua code following verified 12.0 patterns. Knows Secret Values. Uses local everywhere. |
wow-addon-researcher.md | Searches warcraft.wiki.gg for API docs. Verifies function signatures exist in 12.0. |
The AI Workflow¶
You: "Add a minimap button that toggles the config panel"
Claude reads CLAUDE.md → knows the namespace pattern, load order, 12.0 APIs
↓
Claude reads Config.lua → sees existing settings panel registration
↓
Claude adds LibDBIcon to .pkgmeta externals
Claude updates embeds.xml to load LibDBIcon
Claude adds minimap button code to UI.lua using ns.db for toggle state
Claude updates .luacheckrc with new globals
↓
You: /reload → it works
The key insight: AI tools extrapolate from your existing patterns. One well-structured file teaches it how to write the next ten. A messy codebase produces messy AI output.
5. Modern Toolchain Setup¶
IDE Setup (VS Code)¶
Install three extensions:
ext install sumneko.lua # LuaLS language server
ext install ketho.wow-api # WoW API autocomplete (8,000+ functions)
ext install stanzilla.vscode-wow-toc # TOC file syntax highlighting
ketho.wow-api auto-activates when it detects a .toc file. Covers all 12.0.1 APIs with signatures, parameter types, and wiki links.
.vscode/settings.json:
{
"Lua.runtime.version": "Lua 5.1",
"Lua.runtime.builtin": {
"io": "disable",
"os": "disable",
"package": "disable",
"debug": "disable"
},
"Lua.workspace.ignoreDir": ["Libs", ".release", ".github"],
"Lua.diagnostics.disable": ["lowercase-global"],
"Lua.workspace.checkThirdParty": true
}
.vscode/extensions.json:
{
"recommendations": [
"sumneko.lua",
"ketho.wow-api",
"stanzilla.vscode-wow-toc",
"johnnymorganz.stylua"
]
}
Linting with Luacheck¶
# Install
brew install luacheck # macOS
luarocks install luacheck # via luarocks
# Run
luacheck .
# In CI: nebularg/actions-luacheck@v1 (see lint.yml above)
Auto-generated configs
Jayrgo/wow-luacheckrc parses Blizzard's own interface resources to generate a .luacheckrc covering every known WoW global. Alternatively, nebularg/wow-selene-parser generates configs for the Selene linter.
Packaging with BigWigsMods/packager¶
The industry standard. Used by every major addon — BetterBags, Cell, Plater, OmniCC, WeakAuras, ElvUI, Bartender4, BigWigs, DBM.
How it works:
- You push a git tag:
git tag v1.0.0 && git push --tags - GitHub Actions triggers
BigWigsMods/packager@v2 - Packager checks out library externals from
.pkgmeta - Replaces
@project-version@tokens in your code - Strips
@debug@blocks from release builds - Creates zip files (including
-nolibvariant) - Uploads to CurseForge, WoWInterface, Wago, and GitHub Releases
Keyword substitutions (replaced at build time):
| Keyword | Becomes |
|---|---|
@project-version@ | Your git tag (e.g., v1.0.0) |
@project-hash@ | Full git commit hash |
@project-date-iso@ | ISO date of last commit |
@debug@ ... @end-debug@ | Stripped from release builds |
@alpha@ ... @end-alpha@ | Active only in untagged builds |
CI/CD with GitHub Actions¶
Secrets to configure (Settings > Secrets and variables > Actions):
| Secret | Platform | Where to Generate |
|---|---|---|
CF_API_KEY | CurseForge | Author dashboard > API tokens |
WOWI_API_TOKEN | WoWInterface | File management > API tokens |
WAGO_API_TOKEN | Wago Addons | addons.wago.io/account/apikeys |
GITHUB_TOKEN | GitHub Releases | Auto-provided (enable write permissions) |
Enable workflow write permissions
Settings > Actions > General > Workflow permissions > Read and write permissions. Required for GitHub Releases.
Publishing to CurseForge / Wago¶
- Create the project on CurseForge and/or Wago
- Get your project IDs and add them to your TOC:
- Add API key secrets to your GitHub repo
- Push a tag — the workflow handles everything:
6. Quick Start Guide¶
Step 1: Fork & Clone¶
# Clone the template
git clone https://github.com/YourOrg/wow-addon-template.git MyAddon
cd MyAddon
# Remove template git history, start fresh
rm -rf .git
git init
git add -A
git commit -m "Initial commit from Midnight addon template"
Step 2: Rename Everything¶
Find and replace these strings across all files:
| Find | Replace With |
|---|---|
MyAddon | Your addon name (e.g., CoolBags) |
MYADDON | Uppercase version (e.g., COOLBAGS) |
MyAddonDB | Your SavedVariables name (e.g., CoolBagsDB) |
YourName | Your author name |
Files to rename:
MyAddon.toc→CoolBags.toc
Step 3: Customize¶
- Edit the TOC — update Title, Notes, Author
- Edit Core.lua — change the slash command, add your init logic
- Edit Config.lua — define your default settings
- Edit UI.lua — build your frames
- Update
.luacheckrc— add your global names
Step 4: Set Up Dev Environment¶
# macOS: symlink into WoW's AddOns folder
ln -s "$(pwd)" \
"/Applications/World of Warcraft/_retail_/Interface/AddOns/CoolBags"
# Windows (admin PowerShell):
New-Item -ItemType SymbolicLink `
-Path "D:\Games\World of Warcraft\_retail_\Interface\AddOns\CoolBags" `
-Value "D:\repos\CoolBags"
Step 5: Test In-Game¶
- Launch WoW
- Type
/reloadto reload the UI - Your addon loads — check for errors with
/console scriptErrors 1 - Install BugSack + BugGrabber for proper error capture with stack traces
Step 6: Iterate¶
No hot-reload exists. /reload restarts the full addon lifecycle.
Step 7: Release¶
# Create GitHub repo
gh repo create CoolBags --public --source=. --push
# Configure secrets (CF_API_KEY, WAGO_API_TOKEN, etc.)
# Then tag and push:
git tag v1.0.0
git push --tags
The CI pipeline packages your addon and uploads it to CurseForge, Wago, and GitHub Releases automatically.
You did it
Your addon is live. Users can install it from CurseForge or Wago. Push a new tag whenever you want to release an update.
Essential Resources¶
| Resource | URL |
|---|---|
| Warcraft Wiki API | warcraft.wiki.gg/wiki/World_of_Warcraft_API |
| 12.0 API Changes | warcraft.wiki.gg/wiki/Patch_12.0.0/API_changes |
| Blizzard FrameXML Source | github.com/Gethe/wow-ui-source |
| Ketho's API Resources | github.com/Ketho/BlizzardInterfaceResources |
| VS Code WoW API Extension | ketho.wow-api |
| BigWigsMods Packager | github.com/BigWigsMods/packager |
| Packager Wiki | github.com/BigWigsMods/packager/wiki |
| WoW Addon Dev Guide (AI) | github.com/Amadeus-/WoWAddonDevGuide |
| Copilot Agents for WoW | github.com/JBurlison/WoWAddonAPIAgents |
| TOC Format Spec | warcraft.wiki.gg/wiki/TOC_format |