Initial commit

This commit is contained in:
Exiles
2026-05-25 11:01:58 +00:00
commit b36de765c0
8 changed files with 471 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
* -text
+71
View File
@@ -0,0 +1,71 @@
name: release
on:
push:
tags:
- '*-coa.*' # Asc-1.1.6-coa.2, 9.1.40-coa.3, etc.
- 'v*' # v0.3.0 for repos without an upstream version
jobs:
release:
runs-on: linux-amd64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # build_zip uses git archive HEAD; full history is fine
- name: Build per-addon zip(s)
run: bash tools/build_zip.sh
- name: Publish release (Gitea API direct; no action dependency)
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
API: ${{ github.server_url }}/api/v1
# Gitea attachment ceiling is 200 MiB (see roles/gitea config).
# Skip anything larger so one oversized asset doesn't fail the job.
MAX_BYTES: 209715200
run: |
set -euo pipefail
# Create the release (or reuse if it already exists for this tag).
RID=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$API/repos/$REPO/releases/tags/$TAG" 2>/dev/null \
| jq -r '.id // empty')
if [ -z "$RID" ]; then
RID=$(curl -sf -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"$API/repos/$REPO/releases" \
-d "$(jq -nc --arg t "$TAG" '{tag_name:$t,name:$t,draft:false,prerelease:false}')" \
| jq -r '.id')
fi
echo "release id: $RID"
# Upload every dist/*.zip. Per-asset failures don't fail the job —
# we want partial releases to still publish rather than block the
# whole pipeline on one big file.
failed=0
uploaded=0
for f in dist/*.zip; do
name=$(basename "$f")
size=$(stat -c '%s' "$f")
if [ "$size" -gt "$MAX_BYTES" ]; then
echo "::warning::skip $name (${size} B > ${MAX_BYTES} B Gitea limit; host on CDN instead)"
failed=$((failed+1))
continue
fi
echo "uploading $name ($(numfmt --to=iec "$size"))"
if curl -sf -X POST -H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$f" \
"$API/repos/$REPO/releases/$RID/assets?name=$name" \
| jq -r '" -> " + .browser_download_url'; then
uploaded=$((uploaded+1))
else
echo "::warning::upload failed for $name"
failed=$((failed+1))
fi
done
echo "release published: $uploaded uploaded, $failed skipped/failed"
# Only fail the job if NO assets uploaded — a release with zero
# attachments isn't useful to anyone.
[ "$uploaded" -gt 0 ]
+8
View File
@@ -0,0 +1,8 @@
.env
.DS_Store
.release
.install
.lua/*
.vscode
.idea
dist/
+5
View File
@@ -0,0 +1,5 @@
Copyright (C) 2026 by Exiles
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+116
View File
@@ -0,0 +1,116 @@
# Porting addons to CoA / Project Ascension 3.3.5
The CoA Beta client is WoW 3.3.5 with a heavily reworked FrameXML /
SharedXML / StaticPopup / Settings system, 21 custom playable classes,
and selectively-modern globals (`C_Timer.After`, `C_ClassInfo`). Most
upstream retail / Classic / Wrath addons need a small bundle of fixes.
This document lists every recurring failure mode we've seen across the
existing 25 Exiles forks plus the matching one-paragraph fix.
Keep this file in your fork until you're done porting; once everything
loads cleanly you can leave it as documentation or delete it.
The canonical / live version of this checklist is at
[coa.exil.es/coa/dev/addons](https://coa.exil.es/coa/dev/addons) — if
you discover a new failure mode while porting, please add it there.
## Bug-pattern checklist
### Retail-only globals are nil
`InterfaceOptionsFrame*`, `InterfaceOptions_AddCategory`,
`InterfaceOptionsFramePanelContainer`, `Settings.*`, `Enum.ClassMask`
(only knows vanilla classes), retail `C_TooltipInfo`. Guard with
`if X then … end` or fall back to a sensible default (`UIParent` for
parent-frame fallbacks).
### FileDataIDs silently no-op
`Set*Texture(<integer>)` does nothing on 3.3.5 — textures render as red
placeholders. Use string paths. The comment next to an FDID call in
upstream code usually already names the path.
### Hardcoded class lists miss CoA's 21 custom classes
`SHAMAN/HERO` masks, `CLASS_SORT_ORDER` iteration, `RAID_CLASS_COLORS`
direct keys, `CLASS_ICON_TCOORDS`. Witchdoctor / Templar / Sun Cleric /
Primalist / Tinker / etc. won't appear. Extend via `GetNumClasses()`,
`LOCALIZED_CLASS_NAMES_MALE` fallback, or a class-keyed `or { … }`
guard.
### `C_ClassInfo.GetSpecInfo(class, spec)` arg mismatch
`GetAllSpecs(class)` returns non-string items on this client. Always
`pcall` the `GetSpecInfo` / `GetSpecInfoByID` call and skip iterations
that fail.
### `StaticPopup_Show` during `PLAYER_LOGIN` silently fails
Fires too early on this client. Defer with `C_Timer.After(0, …)`.
Failure mode is silent — the popup never appears and any code that
depends on user confirmation (e.g. WeakAuras' downgrade-repair flow)
never runs.
### `.tga` textures don't load
The engine only loads `.blp`. Ship BLP2 / DXT3 (`alphaEncoding = 1`) —
DXT5 (`ae=7`) does NOT decode on 3.3.5. Conversion:
```sh
blp-conv --blp-version blp2 --blp-format dxt3 --alpha-bits 8 in.png out.blp
```
### `Minimap:SetMaskTexture("Interface\\BUTTONS\\WHITE8X8")` silently rounds
The shared white mask used by retail SexyMap-style addons falls back to
the round minimap on CoA. Bundle an opaque BLP and point the mask at it
instead.
### `UnitClass("player")` at file-load time returns nil
Resolve class inside `OnEnable` / event handler, not as a file-level
`local`.
### CLEU argument positions changed
Verify against the 3.3.5 signature:
```
(event, timestamp, subevent, hideCaster,
srcGUID, srcName, srcFlags, srcRaidFlags,
destGUID, destName, destFlags, destRaidFlags,
spellId, spellName, spellSchool, …)
```
`destFlags` is arg 11, not arg 9. Many retail-era addons get this
wrong.
## Ace3 specifics
If the addon embeds Ace3 libs (`Libs/AceXxx-3.0/`), use the canonical
`Exiles/coa-ace3` bundle to overwrite them. The sweep script in that
repo handles every fork in one pass — see its README. Don't ship a
bespoke Ace3 vendor; mismatched MINOR versions across loaded addons
cause silent breakage at LibStub resolution time.
`coa-elvui` is the one exception — ElvUI ships its own customized
Ace3 stack and is excluded from the sweep.
## Standard layout
The Exiles fork-layout convention is `<repo>/<AddonFolder>/<AddonFolder>.toc`
at depth-1, with dev tooling (`tools/`, `.gitea/`, `README.md`, `LICENSE`,
`PORTING.md`) at the repo root. Multi-addon repos place each addon in its
own sibling folder.
`tools/build_zip.sh` discovers these `<Folder>/<Folder>.toc` pairs
automatically.
## Tagging convention
`<upstream>-coa.N` per repo. Examples:
| Repo | Tag |
|---|---|
| `coa-decursive` | `Asc-1.1.6-coa.1` |
| `coa-leatrix-plus` | `3.3.5-coa.1` |
| `coa-ace3` | `52e5f2c-coa.1` (upstream commit pin) |
| `coa-dbm` | `2026.05.25-coa.1` (no `## Version:` line; date-based) |
The trailing `-coa.N` counter bumps for each CoA-fork iteration. Pushing
the tag fires the release workflow and publishes the per-addon zips to
`https://git.sub-net.at/Exiles/<repo>/releases/tag/<tag>`.
See [coa.exil.es/coa/dev/releases](https://coa.exil.es/coa/dev/releases)
for the full release pipeline doc.
+100
View File
@@ -0,0 +1,100 @@
# coa-template
Template repo for new `Exiles/coa-*` addon forks. Click **Use This
Template** on the Gitea page to start a fresh repo with the canonical
release pipeline, layout convention, and CoA porting checklist
pre-wired.
## What you get
```
.
├── .gitea/workflows/release.yml # tag-triggered release pipeline
├── tools/
│ ├── build_zip.sh # git-archive per addon folder
│ └── init_from_upstream.sh # one-shot upstream import helper
├── .gitattributes # * -text (preserve upstream EOLs)
├── .gitignore # standard 7-line + dist/
├── LICENSE # 0BSD
├── PORTING.md # CoA-compat bug-pattern checklist
└── README.md # this file
```
## Quick start (backporting an existing addon)
```sh
# 1. Use this template to create Exiles/coa-myaddon, then:
git clone gitea@git.sub-net.at:Exiles/coa-myaddon.git
cd coa-myaddon
# 2. Pull the upstream source into MyAddon/ at the repo root.
tools/init_from_upstream.sh \
https://github.com/Ascension-Addons/MyAddon \
MyAddon
# 3. Make it run on CoA. Read PORTING.md — every recurring failure mode
# we've seen across the existing 25 Exiles forks is documented there
# with the matching fix.
# 4. Verify the build works.
bash tools/build_zip.sh
unzip -l dist/MyAddon.zip | head -5 # first entry: MyAddon/
# 5. Push your fork.
git push origin master
# 6. Tag the first release. Read MyAddon/MyAddon.toc for the upstream
# version, then:
git tag -a 1.2.3-coa.1 -m "first Exiles release"
git push origin 1.2.3-coa.1
# The runner picks it up within seconds and publishes
# https://git.sub-net.at/Exiles/coa-myaddon/releases/tag/1.2.3-coa.1
```
## Layout convention
Every addon source folder sits at the repo root with the same name as
its `.toc`:
```
coa-myaddon/
├── MyAddon/
│ ├── MyAddon.toc
│ └── ...
└── (dev tooling at root)
```
Multi-addon repos (DBM, Altoholic, Atlasloot, …) place each addon as a
sibling folder at the repo root. `tools/build_zip.sh` discovers them all
and emits one zip per addon plus a combined `coa-myaddon-all.zip`.
## Release pipeline
The workflow in `.gitea/workflows/release.yml` triggers on tags matching
`*-coa.*` or `v*`. It checks out, runs `tools/build_zip.sh`, and uploads
each `dist/*.zip` as a Gitea release attachment via plain `curl` — no
external action dependency, no org-level secret. Per-asset upload
failures are tolerated (the job still publishes whatever uploaded);
oversized assets (>200 MiB) get a `::warning` and skip.
Runner: `gitea-runner01.corp.sub-net.at` (host executor, label
`linux-amd64`). Configured by the `gitea_runner` Ansible role.
Full docs: [coa.exil.es/coa/dev/releases](https://coa.exil.es/coa/dev/releases).
## Maintenance
This template is updated by hand when the canonical files in
`Exiles/coa-decursive` (the pilot) change. To re-sync the canonical
release pipeline from the pilot:
```sh
cp /path/to/coa-decursive/tools/build_zip.sh tools/build_zip.sh
cp /path/to/coa-decursive/.gitea/workflows/release.yml .gitea/workflows/release.yml
```
The PORTING.md checklist is the lean snapshot of
[coa.exil.es/coa/dev/addons](https://coa.exil.es/coa/dev/addons) — when
you discover a new CoA failure mode, add it to the Tome page first, then
re-snapshot here.
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Build per-addon zip artefacts from HEAD via git-archive.
#
# - Discovers top-level addon folders (Foo/Foo.toc).
# - Re-creates dist/ each run.
# - Always archives HEAD, so the working tree state is irrelevant.
# - If more than one addon folder is present, also emits <repo>-all.zip
# with every addon folder side-by-side at the zip root.
# - When run inside Gitea Actions the working tree lives under a
# per-job dir like /var/lib/act_runner/work/.../hostexecutor, so the
# repo name comes from $GITHUB_REPOSITORY (set by the runner) and
# only falls back to the toplevel basename for local invocations.
set -euo pipefail
root=$(git rev-parse --show-toplevel)
cd "$root"
# Gitea Actions sets GITHUB_REPOSITORY=owner/repo. The basename of
# `git rev-parse --show-toplevel` inside the runner is the worker dir
# (e.g. `hostexecutor`), which would name the bundle wrong.
if [ -n "${GITHUB_REPOSITORY:-}" ]; then
repo_name="${GITHUB_REPOSITORY##*/}"
else
repo_name=$(basename "$root")
fi
dist="$root/dist"
# Find Foo/Foo.toc pairs at depth 1; ignore libs nested deeper.
addons=()
while IFS= read -r toc; do
dir=$(dirname "$toc")
folder=$(basename "$dir")
base=$(basename "$toc" .toc)
# Accept Foo.toc and Foo_Wrath.toc style flavour variants; folder must match
# at least one toc basename prefix (Foo).
case "$base" in
"$folder"|"$folder"_*) addons+=("$folder") ;;
esac
done < <(command find . -mindepth 2 -maxdepth 2 -type f -name '*.toc' | sed 's|^\./||' | sort)
# Dedupe (a folder with Foo.toc + Foo_Wrath.toc shows up twice).
if [ ${#addons[@]} -gt 0 ]; then
mapfile -t addons < <(printf '%s\n' "${addons[@]}" | awk '!seen[$0]++')
fi
if [ ${#addons[@]} -eq 0 ]; then
echo "no addon folders found (looking for */Foo.toc with matching folder name)" >&2
exit 1
fi
rm -rf "$dist"
mkdir -p "$dist"
for folder in "${addons[@]}"; do
out="$dist/$folder.zip"
# No --prefix: the folder already sits at the repo root, so git-archive
# emits entries as <folder>/... which is exactly what
# Interface/AddOns/ expects after extraction.
git archive HEAD --format=zip -o "$out" -- "$folder"
echo "built dist/$folder.zip"
done
# Combined bundle only makes sense when there are multiple addons.
if [ ${#addons[@]} -gt 1 ]; then
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
git archive HEAD --format=tar -- "${addons[@]}" | tar -x -C "$tmp"
out="$dist/$repo_name-all.zip"
( cd "$tmp" && zip -qr "$out" "${addons[@]}" )
echo "built dist/$repo_name-all.zip"
fi
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# init_from_upstream.sh — bootstrap a new Exiles addon fork from upstream.
#
# Usage:
# tools/init_from_upstream.sh <upstream-url> <AddonFolderName> [upstream-branch]
#
# Example:
# tools/init_from_upstream.sh https://github.com/Ascension-Addons/CallToArms CallToArms
#
# What it does:
# 1. Adds `upstream` remote and fetches it.
# 2. Squash-imports the upstream tree at the named branch (default master/main)
# into <AddonFolderName>/ at the repo root.
# 3. Leaves you with a single staged commit you review and finalise.
#
# What it does NOT do:
# - Push anything.
# - Tag anything.
# - Modify the upstream history (it's a squash import, not a merge).
#
# After running, verify with:
# bash tools/build_zip.sh
# unzip -l dist/<AddonFolderName>.zip | head -5
# then `git commit --amend` if you want to edit the message, push,
# and tag <upstream-version>-coa.1 to trigger the first release.
set -euo pipefail
if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then
echo "usage: $0 <upstream-url> <AddonFolderName> [upstream-branch]" >&2
exit 2
fi
upstream_url=$1
folder=$2
branch=${3:-}
if [ -d "$folder" ]; then
echo "refusing to overwrite existing folder $folder/" >&2
exit 1
fi
if ! git remote get-url upstream >/dev/null 2>&1; then
git remote add upstream "$upstream_url"
else
git remote set-url upstream "$upstream_url"
fi
echo "==> fetching upstream"
git fetch --tags upstream
if [ -z "$branch" ]; then
# Try master, then main.
if git rev-parse --verify "upstream/master" >/dev/null 2>&1; then
branch=master
elif git rev-parse --verify "upstream/main" >/dev/null 2>&1; then
branch=main
else
echo "couldn't find upstream/master or upstream/main; pass branch explicitly" >&2
exit 1
fi
fi
echo "==> importing tree from upstream/$branch into $folder/"
# Extract upstream tree into a temporary worktree-like dir, then move into folder.
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
git archive "upstream/$branch" | tar -x -C "$tmp"
mkdir -p "$folder"
# rsync preserves dotfiles; --exclude keeps obvious noise out.
rsync -a --exclude '.git/' --exclude '.github/' --exclude '.gitea/' \
--exclude '.idea/' --exclude '.vscode/' --exclude '.DS_Store' \
"$tmp"/ "$folder"/
git add "$folder"
upstream_sha=$(git rev-parse "upstream/$branch")
upstream_short=$(git rev-parse --short "upstream/$branch")
git commit -m "Import $folder from upstream@$upstream_short
upstream: $upstream_url
branch: $branch
commit: $upstream_sha"
cat <<EOF
==> done. next steps:
1. bash tools/build_zip.sh
2. unzip -l dist/$folder.zip | head -5
(expect first entry: $folder/)
3. git push origin master
4. Read $folder/$folder.toc — look for the version string.
Tag with <upstream-version>-coa.1, e.g.:
git tag -a 1.2.3-coa.1 -m "first Exiles release"
git push origin 1.2.3-coa.1
The release workflow picks it up and publishes
https://git.sub-net.at/Exiles/<this-repo>/releases/tag/<tag>.
The upstream remote stays configured so future rebases / re-imports
are one-line: \`git fetch upstream && ...\`.
EOF