diff --git a/.github/workflows/nightly-beta.yml b/.github/workflows/nightly-beta.yml new file mode 100644 index 000000000..4861133f0 --- /dev/null +++ b/.github/workflows/nightly-beta.yml @@ -0,0 +1,59 @@ +name: Nightly Beta + +on: + schedule: + - cron: '0 3 * * *' + workflow_dispatch: + +jobs: + nightly-beta: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for changes since last beta + id: check + run: | + LAST_BETA=$(git tag -l 'v*-beta*' --sort=-v:refname | head -n1) + LAST_RELEASE=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude '*-*' HEAD 2>/dev/null || echo "") + REF="${LAST_BETA:-${LAST_RELEASE}}" + + if [ -n "$REF" ]; then + CHANGES=$(git log "${REF}..HEAD" --oneline --no-merges | grep -cv '\[skip ci\]' || true) + else + CHANGES=$(git log HEAD --oneline --no-merges | grep -cv '\[skip ci\]' || true) + fi + + echo "changes=$CHANGES" >> $GITHUB_OUTPUT + echo "Changes since ${REF:-inception}: $CHANGES" + + - name: Create beta tag and trigger release + if: steps.check.outputs.changes != '0' + run: | + BASE=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude '*-*' HEAD 2>/dev/null || echo "v0.1.0") + SHA=$(git rev-parse HEAD) + + # Find next available beta number (API check handles ghost refs from deleted tags) + NUM=1 + while gh api "repos/${{ github.repository }}/git/ref/tags/${BASE}-beta.${NUM}" --silent 2>/dev/null; do + NUM=$((NUM + 1)) + done + + # Create tag, retry with next number on conflict (ghost ref not visible via GET) + for i in $(seq 0 4); do + TAG="${BASE}-beta.$((NUM + i))" + if gh api "repos/${{ github.repository }}/git/refs" \ + -f ref="refs/tags/${TAG}" -f sha="${SHA}" 2>/dev/null; then + echo "Created tag: ${TAG}" + gh workflow run release.yml -f "tag=${TAG}" + echo "Triggered release build" + exit 0 + fi + done + echo "::error::Could not create beta tag" + exit 1 + env: + GH_TOKEN: ${{ secrets.PAT_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..27d8ac8fe --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,124 @@ +name: Package and Release + +on: + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + inputs: + tag: + description: 'Tag to build (e.g. v1.0.0-beta.1). Leave empty for alpha build.' + required: false + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag || github.ref }} + fetch-depth: 0 + + - name: Check if build is needed + id: guard + run: | + # Tags and workflow_dispatch always build + if [[ "$GITHUB_REF" == refs/tags/* ]] || [[ -n "${{ inputs.tag }}" ]] || [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + echo "skip=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Branch pushes: skip if only docs/CI files changed + DOMINATED=true + for f in $(git diff --name-only HEAD~1..HEAD 2>/dev/null); do + case "$f" in + *.md|*.sh|.github/*|.gitignore|.pkgmeta) ;; + *) DOMINATED=false; break ;; + esac + done + echo "skip=$DOMINATED" >> $GITHUB_OUTPUT + + - name: Generate Changelog.lua + if: steps.guard.outputs.skip != 'true' + id: changelog + run: | + chmod +x generate_changelog.sh + if [[ "$GITHUB_REF" == refs/heads/* ]] && [[ -z "${{ inputs.tag }}" ]]; then + export FORCE_ALPHA=true + fi + ./generate_changelog.sh + + VERSION=$(grep 'DF.ADDON_VERSION' Changelog.lua | sed 's/.*= "\(.*\)"/\1/') + CHANNEL=$(grep 'DF.RELEASE_CHANNEL' Changelog.lua | sed 's/.*= "\(.*\)"/\1/') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "channel=$CHANNEL" >> $GITHUB_OUTPUT + + NOTES=$(awk '/^## / { count++; if (count > 1) exit } count >= 1 { print }' CHANGELOG.md | head -c 1800 | sed -e :a -e '/^[[:space:]-]*$/{ $d; N; ba; }') + { + echo "notes<> $GITHUB_OUTPUT + + - name: Package and Release + if: steps.guard.outputs.skip != 'true' + uses: BigWigsMods/packager@v2 + env: + CF_API_KEY: ${{ secrets.CF_API_KEY }} + GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }} + + - name: Notify Discord + if: steps.guard.outputs.skip != 'true' && success() + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + VERSION: ${{ steps.changelog.outputs.version }} + CHANNEL: ${{ steps.changelog.outputs.channel }} + NOTES: ${{ steps.changelog.outputs.notes }} + run: | + if [ -z "$DISCORD_WEBHOOK_URL" ]; then + echo "No Discord webhook configured, skipping." + exit 0 + fi + + case "$CHANNEL" in + release) + COLOR=3066993 + TITLE="DandersFrames ${VERSION} — New Release" + FOOTER="Available on CurseForge" + ;; + beta) + COLOR=16755200 + TITLE="DandersFrames ${VERSION} — Beta" + FOOTER="Available on CurseForge (Beta channel)" + ;; + *) + COLOR=9474192 + TITLE="DandersFrames ${VERSION} — Alpha" + FOOTER="Available on CurseForge (Alpha channel)" + ;; + esac + + PAYLOAD=$(jq -n \ + --arg title "$TITLE" \ + --arg desc "$NOTES" \ + --arg color "$COLOR" \ + --arg footer "$FOOTER" \ + --arg url "https://www.curseforge.com/wow/addons/danders-frames" \ + '{ + embeds: [{ + title: $title, + description: $desc, + color: ($color | tonumber), + url: $url, + footer: { text: $footer } + }] + }') + + curl -s -o /dev/null -w "%{http_code}" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" \ + "$DISCORD_WEBHOOK_URL" diff --git a/.pkgmeta b/.pkgmeta new file mode 100644 index 000000000..9b02fc5e8 --- /dev/null +++ b/.pkgmeta @@ -0,0 +1,13 @@ +package-as: DandersFrames +enable-nolib-creation: no + +ignore: + - CHANGELOG.md + - README.md + - generate_changelog.sh + - .github + - .gitignore + +manual-changelog: + filename: CHANGELOG.md + markup-type: markdown diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..d12d7e66d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# DandersFrames Changelog + +## [4.0.5] - 2026-02-14 + +### Bug Fixes +* Raid frames misaligned / anchoring broken +* Groups per row setting not working in live raids +* Arena/BG frames showing wrong layout after reload +* Arena health bars not updating after reload +* Leader change causes frames to disappear or misalign +* Menu bind ignores out-of-combat setting +* Boss aura font size defaulting to 200% instead of 100% +* Click casting profiles don't switch on spec change +* Clique not working on pet frames +* Absorb overlay doesn't fade when out of range +* Heal absorb and heal prediction bars don't fade when out of range +* Defensive icon flashes at wrong opacity when appearing +* Name text stays full opacity on out-of-range players +* Health text and status text stay full opacity on out-of-range players +* Name alpha resets after exiting test mode +* Glowy hand cursor after failed click cast spells +* Macro editing window gets stuck open when reopened +* Flat raid unlock mover sized incorrectly +* Fonts broken on non-English client languages + +### New Features +* Click casting spec default profile option +* Group visibility options now available in flat raid mode +* Slider edit boxes accept precise decimal values for fine-tuned positioning and scaling \ No newline at end of file diff --git a/Changelog.lua b/Changelog.lua new file mode 100644 index 000000000..8ac1e5c9f --- /dev/null +++ b/Changelog.lua @@ -0,0 +1,5 @@ +local addonName, DF = ... +DF.ADDON_VERSION = "dev" +DF.BUILD_DATE = "0000-00-00T00:00:00Z" +DF.RELEASE_CHANNEL = "alpha" +DF.CHANGELOG_TEXT = "Development version" diff --git a/DandersFrames.toc b/DandersFrames.toc index 450148503..99e437e09 100644 --- a/DandersFrames.toc +++ b/DandersFrames.toc @@ -3,7 +3,8 @@ ## Notes: Custom party/raid frames for WoW Midnight 12.0 ## X-Credits: Some optimization patterns informed by studying Grid2 and other community addons. ## Author: Danders -## Version: 4.0.5 +## Version: @project-version@ +## X-Curse-Project-ID: 1389690 ## IconTexture: Interface\AddOns\DandersFrames\Media\DF_Icon ## SavedVariables: DandersFramesDB_v2, DandersFramesClickCastingDB ## SavedVariablesPerCharacter: DandersFramesCharDB @@ -25,6 +26,7 @@ DandersFrames.xml # Core Config.lua +Changelog.lua Profile.lua ExportCategories.lua Core.lua diff --git a/GUI/GUI.lua b/GUI/GUI.lua index 9a7a369af..c61da886a 100644 --- a/GUI/GUI.lua +++ b/GUI/GUI.lua @@ -4328,7 +4328,10 @@ function DF:CreateGUI() local title = titleBar:CreateFontString(nil, "OVERLAY", "GameFontNormal") title:SetPoint("LEFT", 12, 0) - title:SetText("DandersFrames v" .. (DF.VERSION or "?")) + local versionStr = DF.ADDON_VERSION or DF.VERSION or "?" + local channelTags = { alpha = " |cffff8800alpha|r", beta = " |cffff8800beta|r" } + local channelTag = channelTags[DF.RELEASE_CHANNEL] or "" + title:SetText("DandersFrames " .. versionStr .. channelTag) local c = GetThemeColor() title:SetTextColor(c.r, c.g, c.b) title.UpdateTheme = function() @@ -4363,7 +4366,92 @@ function DF:CreateGUI() closeIcon:SetVertexColor(C_TEXT_DIM.r, C_TEXT_DIM.g, C_TEXT_DIM.b) end) closeBtn:SetScript("OnClick", function() frame:Hide() end) - + + -- Info button (changelog) + local infoBtn = CreateFrame("Button", nil, frame, "BackdropTemplate") + infoBtn:SetSize(20, 20) + infoBtn:SetPoint("TOPRIGHT", -32, -5) + infoBtn:SetFrameStrata("FULLSCREEN_DIALOG") + infoBtn:SetFrameLevel(210) + infoBtn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + edgeSize = 1, + }) + infoBtn:SetBackdropColor(C_ELEMENT.r, C_ELEMENT.g, C_ELEMENT.b, 1) + infoBtn:SetBackdropBorderColor(C_BORDER.r, C_BORDER.g, C_BORDER.b, 0.5) + local infoIcon = infoBtn:CreateTexture(nil, "OVERLAY") + infoIcon:SetPoint("CENTER") + infoIcon:SetSize(12, 12) + infoIcon:SetTexture("Interface\\AddOns\\DandersFrames\\Media\\Icons\\info") + infoIcon:SetVertexColor(C_TEXT_DIM.r, C_TEXT_DIM.g, C_TEXT_DIM.b) + infoBtn:SetScript("OnEnter", function(self) + local tc = GetThemeColor() + self:SetBackdropBorderColor(tc.r, tc.g, tc.b, 1) + infoIcon:SetVertexColor(tc.r, tc.g, tc.b) + end) + infoBtn:SetScript("OnLeave", function(self) + self:SetBackdropBorderColor(C_BORDER.r, C_BORDER.g, C_BORDER.b, 0.5) + infoIcon:SetVertexColor(C_TEXT_DIM.r, C_TEXT_DIM.g, C_TEXT_DIM.b) + end) + + -- Changelog overlay + local changelogOverlay = CreateFrame("Frame", nil, frame, "BackdropTemplate") + changelogOverlay:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -34) + changelogOverlay:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 36) + changelogOverlay:SetFrameStrata("FULLSCREEN_DIALOG") + changelogOverlay:SetFrameLevel(300) + CreatePanelBackdrop(changelogOverlay) + changelogOverlay:Hide() + + local changelogTitle = changelogOverlay:CreateFontString(nil, "OVERLAY", "GameFontNormal") + changelogTitle:SetPoint("TOPLEFT", 12, -10) + changelogTitle:SetText("Changelog") + changelogTitle:SetTextColor(C_TEXT.r, C_TEXT.g, C_TEXT.b) + + local backBtn = CreateFrame("Button", nil, changelogOverlay, "BackdropTemplate") + backBtn:SetSize(60, 22) + backBtn:SetPoint("TOPRIGHT", -8, -6) + CreateElementBackdrop(backBtn) + local backText = backBtn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + backText:SetPoint("CENTER") + backText:SetText("Back") + backText:SetTextColor(C_TEXT.r, C_TEXT.g, C_TEXT.b) + backBtn:SetScript("OnEnter", function(self) + local tc = GetThemeColor() + self:SetBackdropBorderColor(tc.r, tc.g, tc.b, 1) + end) + backBtn:SetScript("OnLeave", function(self) + self:SetBackdropBorderColor(C_BORDER.r, C_BORDER.g, C_BORDER.b, 0.5) + end) + backBtn:SetScript("OnClick", function() changelogOverlay:Hide() end) + + local changelogScroll = CreateFrame("ScrollFrame", nil, changelogOverlay, "UIPanelScrollFrameTemplate") + changelogScroll:SetPoint("TOPLEFT", 8, -34) + changelogScroll:SetPoint("BOTTOMRIGHT", -26, 8) + + local changelogEdit = CreateFrame("EditBox", nil, changelogScroll) + changelogEdit:SetMultiLine(true) + changelogEdit:SetAutoFocus(false) + changelogEdit:SetFontObject(GameFontHighlightSmall) + changelogEdit:SetTextColor(C_TEXT_DIM.r, C_TEXT_DIM.g, C_TEXT_DIM.b) + changelogEdit:SetWidth(changelogScroll:GetWidth() or 500) + changelogEdit:SetText(DF.CHANGELOG_TEXT or "No changelog available.") + changelogEdit:SetCursorPosition(0) + changelogEdit:EnableMouse(true) + changelogEdit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end) + changelogEdit:SetScript("OnEditFocusGained", function(self) self:HighlightText(0, 0) end) + changelogScroll:SetScrollChild(changelogEdit) + + infoBtn:SetScript("OnClick", function() + if changelogOverlay:IsShown() then + changelogOverlay:Hide() + else + changelogEdit:SetWidth(changelogScroll:GetWidth()) + changelogOverlay:Show() + end + end) + -- ========================================================================= -- RESIZE HANDLE (bottom-right corner) -- ========================================================================= @@ -5051,7 +5139,7 @@ function DF:CreateGUI() -- Version on the right local versionText = footer:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") versionText:SetPoint("RIGHT", footer, "RIGHT", -2, 0) - versionText:SetText("v" .. (DF.VERSION or "?")) + versionText:SetText(versionStr .. channelTag) versionText:SetTextColor(C_TEXT_DIM.r, C_TEXT_DIM.g, C_TEXT_DIM.b, 0.5) -- Create the click casting UI content diff --git a/generate_changelog.sh b/generate_changelog.sh new file mode 100755 index 000000000..d2be5dc29 --- /dev/null +++ b/generate_changelog.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# generate_changelog.sh — Reads CHANGELOG.md and generates Changelog.lua +# Called by GitHub Actions before packaging. + +set -euo pipefail + +CHANGELOG_FILE="CHANGELOG.md" +OUTPUT_FILE="Changelog.lua" + +if [ ! -f "$CHANGELOG_FILE" ]; then + echo "Error: $CHANGELOG_FILE not found." + exit 1 +fi + +# Build date +BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +# ============================================================ +# SEMVER HELPERS +# ============================================================ + +# Extract major.minor.patch from a version string (v1.2.3, v1.2.3-beta.1, etc.) +parse_major() { echo "${1#v}" | cut -d. -f1; } +parse_minor() { echo "${1#v}" | cut -d. -f2 | cut -d- -f1; } + +# Check if the version bump is minor or major +is_minor_or_major_bump() { + local cur_major; cur_major=$(parse_major "$1") + local cur_minor; cur_minor=$(parse_minor "$1") + local prev_major; prev_major=$(parse_major "$2") + local prev_minor; prev_minor=$(parse_minor "$2") + + [ "$cur_major" != "$prev_major" ] || [ "$cur_minor" != "$prev_minor" ] +} + +# Extract only the header + first ## [x.y.z] section from CHANGELOG.md +trim_to_first_section() { + echo "$1" | awk '/^## \[/ { count++; if (count > 1) exit } { print }' +} + +# ============================================================ +# DETERMINE VERSION AND CHANGELOG CONTENT +# ============================================================ +CURRENT_TAG=$(git describe --exact-match --tags HEAD 2>/dev/null || echo "") +LAST_TAG=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || echo "v0.0.0") + +# Branch pushes must always produce alpha builds, even if a tag exists on HEAD +if [ "${FORCE_ALPHA:-}" = "true" ]; then + CURRENT_TAG="" +fi + +if [ -n "$CURRENT_TAG" ]; then + # Tagged commit — determine channel from tag name + if echo "$CURRENT_TAG" | grep -qi "beta"; then + RELEASE_CHANNEL="beta" + elif echo "$CURRENT_TAG" | grep -qi "alpha"; then + RELEASE_CHANNEL="alpha" + else + RELEASE_CHANNEL="release" + fi + VERSION="$CURRENT_TAG" + + # Find previous release tag (excluding current and pre-release tags) + PREV_RELEASE=$(git tag -l 'v[0-9]*' --sort=-v:refname | grep -v '-' | grep -v "^${CURRENT_TAG}$" | head -n1 || echo "") + + if [ -n "$PREV_RELEASE" ] && is_minor_or_major_bump "$CURRENT_TAG" "$PREV_RELEASE"; then + # Minor/major bump: only include the latest version section + CHANGELOG_CONTENT=$(trim_to_first_section "$(cat "$CHANGELOG_FILE")") + echo "Minor/major bump detected (${PREV_RELEASE} -> ${CURRENT_TAG}): trimmed changelog" + else + # Patch or first release: keep full changelog + CHANGELOG_CONTENT=$(cat "$CHANGELOG_FILE") + fi + + # Beta builds: prepend unreleased commits since the base release + if [ "$RELEASE_CHANNEL" = "beta" ] && [ -n "$PREV_RELEASE" ]; then + UNRELEASED_COMMITS=$(git log "${PREV_RELEASE}..HEAD" --oneline --no-merges 2>/dev/null | grep -v '\[skip ci\]' || echo "") + if [ -n "$UNRELEASED_COMMITS" ]; then + UNRELEASED_SECTION="## Unreleased (${VERSION}) + +$(echo "$UNRELEASED_COMMITS" | sed 's/^[a-f0-9]* /- /') + +--- +" + HEADER=$(echo "$CHANGELOG_CONTENT" | head -n 1) + BODY=$(echo "$CHANGELOG_CONTENT" | tail -n +2) + CHANGELOG_CONTENT="${HEADER} + +${UNRELEASED_SECTION} +${BODY}" + fi + fi +else + # Untagged commit — alpha build + RELEASE_CHANNEL="alpha" + COMMIT_COUNT=$(git rev-list "${LAST_TAG}..HEAD" --count 2>/dev/null || echo "0") + # Strip pre-release suffix (v1.0.0-beta.3 → v1.0.0) for clean alpha version + BASE_VERSION=$(echo "$LAST_TAG" | sed 's/-[a-zA-Z].*$//') + VERSION="${BASE_VERSION}-alpha.${COMMIT_COUNT}" + + UNRELEASED_COMMITS=$(git log "${LAST_TAG}..HEAD" --oneline --no-merges 2>/dev/null | grep -v '\[skip ci\]' || echo "") + + if [ -n "$UNRELEASED_COMMITS" ]; then + UNRELEASED_SECTION="## Unreleased (${VERSION}) + +$(echo "$UNRELEASED_COMMITS" | sed 's/^[a-f0-9]* /- /') + +--- +" + HEADER=$(head -n 1 "$CHANGELOG_FILE") + BODY=$(tail -n +2 "$CHANGELOG_FILE") + CHANGELOG_CONTENT="${HEADER} + +${UNRELEASED_SECTION} +${BODY}" + else + CHANGELOG_CONTENT=$(cat "$CHANGELOG_FILE") + fi +fi + +# Update CHANGELOG.md in working directory so the packager picks up changes +echo "$CHANGELOG_CONTENT" > "$CHANGELOG_FILE" + +# Write Changelog.lua +cat > "$OUTPUT_FILE" << LUAEOF +local addonName, DF = ... +DF.ADDON_VERSION = "${VERSION}" +DF.BUILD_DATE = "${BUILD_DATE}" +DF.RELEASE_CHANNEL = "${RELEASE_CHANNEL}" +DF.CHANGELOG_TEXT = [===[ +${CHANGELOG_CONTENT} +]===] +LUAEOF + +# Pre-set TOC version for alpha builds so the packager uses our clean version +# For tagged builds, the packager replaces @project-version@ with the tag name +if [ "$RELEASE_CHANNEL" = "alpha" ]; then + sed -i "s/@project-version@/${VERSION}/" DandersFrames.toc + echo "Pre-set TOC version to ${VERSION}" +fi + +echo "Generated ${OUTPUT_FILE}: version=${VERSION} channel=${RELEASE_CHANNEL} date=${BUILD_DATE}"