From 5ac82e0134794c006a586e7aa569ffe8cf88f2d1 Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Mon, 25 May 2026 12:35:12 +0200 Subject: [PATCH] Initial template: release pipeline + PORTING.md + init helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What lands in any repo created from this template: - .gitea/workflows/release.yml — canonical Gitea-Actions release pipeline (tag-triggered, curl-based publish, per-asset failure tolerance) - tools/build_zip.sh — git-archive per addon folder, multi-addon bundle - tools/init_from_upstream.sh — one-shot squash-import of upstream tree into / at the repo root - PORTING.md — full CoA-compat bug-pattern checklist (Retail-only globals, FileDataIDs, hardcoded class lists, StaticPopup race, .tga textures, Minimap mask, CLEU arg positions, Ace3 sweep, etc.) - README.md — quick-start backport guide - standard .gitignore (incl dist/) + .gitattributes (* -text) --- .gitattributes | 1 + .gitea/workflows/release.yml | 71 +++++++++++++++++++++ .gitignore | 8 +++ PORTING.md | 116 +++++++++++++++++++++++++++++++++++ README.md | 99 +++++++++++++++++++++++++++++- tools/build_zip.sh | 71 +++++++++++++++++++++ tools/init_from_upstream.sh | 99 ++++++++++++++++++++++++++++++ 7 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 .gitea/workflows/release.yml create mode 100644 .gitignore create mode 100644 PORTING.md create mode 100755 tools/build_zip.sh create mode 100755 tools/init_from_upstream.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fa1385d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* -text diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..709a7cd --- /dev/null +++ b/.gitea/workflows/release.yml @@ -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 ] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec92999 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.env +.DS_Store +.release +.install +.lua/* +.vscode +.idea +dist/ diff --git a/PORTING.md b/PORTING.md new file mode 100644 index 0000000..362c7f9 --- /dev/null +++ b/PORTING.md @@ -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()` 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 `//.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 `/.toc` pairs +automatically. + +## Tagging convention + +`-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//releases/tag/`. + +See [coa.exil.es/coa/dev/releases](https://coa.exil.es/coa/dev/releases) +for the full release pipeline doc. diff --git a/README.md b/README.md index a173d1f..69be261 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,100 @@ # coa-template -Template repo for new Exiles addon forks. Click Use This Template to start a coa-* repo with the canonical release pipeline pre-wired. \ No newline at end of file +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. diff --git a/tools/build_zip.sh b/tools/build_zip.sh new file mode 100755 index 0000000..45d1400 --- /dev/null +++ b/tools/build_zip.sh @@ -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 -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 /... 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 diff --git a/tools/init_from_upstream.sh b/tools/init_from_upstream.sh new file mode 100755 index 0000000..93c4c3f --- /dev/null +++ b/tools/init_from_upstream.sh @@ -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-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 / 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/.zip | head -5 +# then `git commit --amend` if you want to edit the message, push, +# and tag -coa.1 to trigger the first release. +set -euo pipefail + +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + echo "usage: $0 [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 < 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 -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//releases/tag/. + + The upstream remote stays configured so future rebases / re-imports + are one-line: \`git fetch upstream && ...\`. +EOF