Skip to content

Add CI/CD pipeline, in-game changelog and version display [skip ci] - #7

Merged
DanderBot merged 1 commit into
DanderBot:mainfrom
riyuk:feature/github-actions
Feb 15, 2026
Merged

Add CI/CD pipeline, in-game changelog and version display [skip ci]#7
DanderBot merged 1 commit into
DanderBot:mainfrom
riyuk:feature/github-actions

Conversation

@riyuk

@riyuk riyuk commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Summary

This sets up the full CI/CD pipeline for DandersFrames — automated builds, CurseForge uploads, nightly betas, and an in-game changelog so players can actually see what changed.

What's new

CI/CD Pipeline (GitHub Actions)

Two workflows:

  • release.yml — Triggers on pushes to main and version tags (v*). Packages the addon via BigWigs Packager, uploads to CurseForge, creates a GitHub Release, and optionally pings Discord.
  • nightly-beta.yml — Runs every night at 3 AM UTC. If there are new commits since the last beta, it creates a beta tag and triggers the release workflow. Can also be triggered manually.

In-Game Changelog

  • New info button (ℹ) next to the close button in the settings window
  • Opens a changelog overlay showing the full changelog text
  • Title bar and footer now show the version number + release channel (e.g. v4.0.6 alpha or v4.0.6 beta in orange)

Build Infrastructure

  • Changelog.luaAuto-generated by CI with version, build date, release channel, and changelog text. Ships as a dev stub locally. Do not edit this file.
  • generate_changelog.sh — Reads CHANGELOG.md, determines version/channel from git tags, and writes Changelog.lua.
  • .pkgmeta — BigWigs Packager config for CurseForge.
  • CHANGELOG.md — Human-maintained changelog. This is what players see in-game.

How releases work

Automatic alpha builds

Every push to main automatically creates an alpha build on CurseForge. The version is derived from the last tag (e.g. v4.0.5-alpha.3 = 3 commits after v4.0.5).

Important: If a commit should NOT produce an alpha build and/or should NOT appear in the changelog, add [skip ci] to the commit message. This is useful for things like README edits, workflow changes, or anything that isn't an actual addon change.

Automatic beta builds (nightly)

Every night, the nightly workflow checks if there are new (non-[skip ci]) commits since the last beta. If yes, it automatically creates a beta tag (e.g. v4.0.5-beta.1) and triggers a build. You can also trigger it manually from the Actions tab whenever you want.

What alpha and beta players see

Alpha and beta builds auto-generate a changelog from commit messages — each commit becomes one line in the "Unreleased" section. Commits tagged with [skip ci] are excluded automatically.

This auto-generated section is prepended to whatever is already in CHANGELOG.md, so players always see both the latest unreleased changes and the last stable release notes.

Creating a stable release

When you're ready to ship a proper release:

  1. Edit CHANGELOG.md — Add a new section at the top with the version number and date. Write the changes however you like — this is what players will see in-game. Keep the previous versions below so there's always a full history.
## [4.0.6] - 2026-03-01

### Bug Fixes
* Fixed something that was broken
* Another fix

### New Features
* Cool new thing

## [4.0.5] - 2026-02-14
...older entries stay here...
  1. Commit and push to main
  2. Tag the commit: git tag v4.0.6 && git push --tags
  3. The release workflow picks up the tag, packages everything, and uploads it as a release to both CurseForge and GitHub

That's it. The tag name determines the version and release channel. No manual beta tagging needed — that's handled by the nightly workflow.

What happens to the changelog on major bumps?

You never need to manually clear CHANGELOG.md — keep the full history in the file. The build script is smart about it: on a minor or major version bump (e.g. v4.0.xv4.1.0 or v5.0.0), the in-game changelog automatically trims down to only the latest version's section. Players see a fresh start, but the full history stays in the repo for reference.

Required GitHub Secrets

Secret Required Purpose
CF_API_KEY Yes CurseForge API token for uploads
PAT_TOKEN Yes GitHub PAT with Contents: Read+Write (for nightly beta tag creation)
DISCORD_WEBHOOK_URL No Discord webhook for release notifications (skipped if not set)

How to create the PAT_TOKEN

The nightly beta workflow needs a Personal Access Token to create tags and trigger the release workflow (the default GITHUB_TOKEN can't trigger other workflows).

  1. Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
  2. Click "Generate new token"
  3. Set a name like DandersFrames CI and pick an expiration (or no expiration)
  4. Under Repository access, select "Only select repositories" and pick the DandersFrames repo
  5. Under Permissions → Repository permissions, set Contents to Read and write — that's the only permission needed
  6. Click Generate token and copy it
  7. Go to the DandersFrames repo → Settings → Secrets and variables → Actions → New repository secret
  8. Name: PAT_TOKEN, Value: paste the token

Same process for CF_API_KEY (get it from CurseForge → Account → API Tokens) and DISCORD_WEBHOOK_URL (Discord → Server Settings → Integrations → Webhooks).

Testing

Please pull this branch and test it locally before merging — check that the addon loads, the info button works, and the changelog overlay shows up correctly in-game. Better safe than sorry! :D

Files changed

  • New: .github/workflows/release.yml, .github/workflows/nightly-beta.yml, .pkgmeta, CHANGELOG.md, Changelog.lua, generate_changelog.sh
  • Modified: DandersFrames.toc (version placeholder + CurseForge project ID + Changelog.lua in load order), GUI/GUI.lua (version display + info button + changelog overlay)

@DanderBot
DanderBot merged commit 7a5960d into DanderBot:main Feb 15, 2026
DanderBot added a commit that referenced this pull request Apr 10, 2026
Replaces the scaffolded stubs with the full cast-lifecycle
implementation. Debug output via DF:Debug("TARGETEDLIST", ...) is
used as a stand-in for visual bars until commit #5 lands the render
pipeline — this lets in-game verification happen against the debug
console without requiring the full rendering work first.

All 13 correctness gotchas from the TS3 cross-reference in
_Reference/targeted-spells-findings.md are handled and tagged inline:

  #1  0.2s delay before reading cast data (start events fire before
      UnitSpellTargetName / duration are populated)
  #2  Cast-ID matching at both pickup (via UnitCastingInfo) and stop
      (via event castGuid) — prevents rapid-restart flicker
  #3  UNIT_SPELLCAST_SUCCEEDED during an active channel is treated
      as a no-op (channel tick, not a stop)
  #4  INTERRUPTED with nil interrupter or a dead caster is treated
      as a normal stop (no interrupter flash)
  #6  Empower spellId/castGuid offset handled (same shape as regular
      start on current retail — documented inline)
  #7  Interrupter lookup via UnitNameFromGUID / UnitClassFromGUID
  #8  Uninterruptible flag from UnitCastingInfo/UnitChannelInfo is
      treated as secret-tainted and only ever fed to
      SetVertexColorFromBoolean (applied in commit #5)
  #9  UNIT_SPELLCAST_INTERRUPTIBLE / NOT_INTERRUPTIBLE overwrite the
      stored uninterruptible field with a clean boolean
  #11 LOADING_SCREEN_DISABLED triggers a full release sweep

Also adds the cast-targeting filter pipeline:

  * TargetedList_IsRelevantCaster filters out non-nameplate units,
    friendly units, and party-member nameplates
  * TargetedList_ReadCastData unifies UnitCastingInfo / UnitChannelInfo
    read paths, handling the positional offset difference for
    castID vs notInterruptible between the two APIs
  * Important-spells filter via C_Spell.IsSpellImportant
  * Hide-own-casts filter
  * Roster name cache used for O(1) target lookup

Event dispatcher extended to route NAME_PLATE_UNIT_ADDED and
UNIT_TARGET through the start handler so new nameplates and mid-cast
target swaps are picked up. UNIT_TARGET also fires an immediate stop
to drop the old tracking — the 0.2s delayed re-pickup recreates it
if the new target is still a party member.

Content-type filter (gotcha #13) is intentionally deferred to
commit #5 since the existing TargetedSpells content-type detection
can be reused rather than duplicated.
DanderBot added a commit that referenced this pull request Apr 10, 2026
Live testing surfaced more secret-tainting: the spellId from the
UNIT_SPELLCAST_START event payload is itself secret-tainted on
nameplates, not just the values from UnitCastingInfo. So:

  C_Spell.IsSpellImportant(secretSpellId) returns a SECRET boolean.
  not <secretBool> raises 'attempt to perform boolean test on a
  secret boolean value'.

Combined with earlier findings, the rule is: essentially everything
derived from a nameplate context is secret. The only safe pattern is
to read at render time and pipe directly into secret-safe sinks
(SetText, SetTexture, SetAlphaFromBoolean, SetVertexColorFromBoolean,
SetShownFromBoolean, SetTimerDuration).

Refactor of TargetedList_DelayedPickup:

  * REMOVED: important-spells filter at pickup. Will be re-added in
    commit #5 via SetShownFromBoolean(IsSpellImportant(spellId), true,
    false) at render time, so the filter never inspects the secret
    bool in Lua.

  * REMOVED: spellName / spellTexture reads at pickup. The render
    pipeline will fetch them via C_Spell.GetSpellName /
    C_Spell.GetSpellTexture and feed them straight into SetText /
    SetTexture sinks.

  * Active-cast record now stores spellId opaquely as a secret token
    — never inspected, only passed into C_Spell.* + sinks.

  * Debug log simplified to clean values only: casterUnit + channel
    flag. The previous tostring(spellName) call would have crashed on
    a secret string.

Refactor of TargetedList_OnCastStop:

  * REMOVED: interrupter source extraction (gotcha #7). The
    interrupter GUID arrives in the event payload as a secret string;
    truth-testing it ('not interrupterGuid') is not allowed and
    UnitNameFromGUID(secretGuid) returns a secret string that can't
    be formatted in Lua. The render pipeline will display the
    interrupter name (if at all) by piping UnitNameFromGUID's result
    directly into a SetText sink at render time.

  * Stop event still records the wasInterrupted flag for the render
    pipeline to play the interrupted-flash animation.
Krathe82 pushed a commit to Krathe82/DandersFrames that referenced this pull request Jun 6, 2026
Add CI/CD pipeline, in-game changelog and version display [skip ci]
Krathe82 pushed a commit to Krathe82/DandersFrames that referenced this pull request Jun 6, 2026
Replaces the scaffolded stubs with the full cast-lifecycle
implementation. Debug output via DF:Debug("TARGETEDLIST", ...) is
used as a stand-in for visual bars until commit DanderBot#5 lands the render
pipeline — this lets in-game verification happen against the debug
console without requiring the full rendering work first.

All 13 correctness gotchas from the TS3 cross-reference in
_Reference/targeted-spells-findings.md are handled and tagged inline:

  #1  0.2s delay before reading cast data (start events fire before
      UnitSpellTargetName / duration are populated)
  DanderBot#2  Cast-ID matching at both pickup (via UnitCastingInfo) and stop
      (via event castGuid) — prevents rapid-restart flicker
  DanderBot#3  UNIT_SPELLCAST_SUCCEEDED during an active channel is treated
      as a no-op (channel tick, not a stop)
  DanderBot#4  INTERRUPTED with nil interrupter or a dead caster is treated
      as a normal stop (no interrupter flash)
  DanderBot#6  Empower spellId/castGuid offset handled (same shape as regular
      start on current retail — documented inline)
  DanderBot#7  Interrupter lookup via UnitNameFromGUID / UnitClassFromGUID
  DanderBot#8  Uninterruptible flag from UnitCastingInfo/UnitChannelInfo is
      treated as secret-tainted and only ever fed to
      SetVertexColorFromBoolean (applied in commit DanderBot#5)
  DanderBot#9  UNIT_SPELLCAST_INTERRUPTIBLE / NOT_INTERRUPTIBLE overwrite the
      stored uninterruptible field with a clean boolean
  DanderBot#11 LOADING_SCREEN_DISABLED triggers a full release sweep

Also adds the cast-targeting filter pipeline:

  * TargetedList_IsRelevantCaster filters out non-nameplate units,
    friendly units, and party-member nameplates
  * TargetedList_ReadCastData unifies UnitCastingInfo / UnitChannelInfo
    read paths, handling the positional offset difference for
    castID vs notInterruptible between the two APIs
  * Important-spells filter via C_Spell.IsSpellImportant
  * Hide-own-casts filter
  * Roster name cache used for O(1) target lookup

Event dispatcher extended to route NAME_PLATE_UNIT_ADDED and
UNIT_TARGET through the start handler so new nameplates and mid-cast
target swaps are picked up. UNIT_TARGET also fires an immediate stop
to drop the old tracking — the 0.2s delayed re-pickup recreates it
if the new target is still a party member.

Content-type filter (gotcha DanderBot#13) is intentionally deferred to
commit DanderBot#5 since the existing TargetedSpells content-type detection
can be reused rather than duplicated.
Krathe82 pushed a commit to Krathe82/DandersFrames that referenced this pull request Jun 6, 2026
Live testing surfaced more secret-tainting: the spellId from the
UNIT_SPELLCAST_START event payload is itself secret-tainted on
nameplates, not just the values from UnitCastingInfo. So:

  C_Spell.IsSpellImportant(secretSpellId) returns a SECRET boolean.
  not <secretBool> raises 'attempt to perform boolean test on a
  secret boolean value'.

Combined with earlier findings, the rule is: essentially everything
derived from a nameplate context is secret. The only safe pattern is
to read at render time and pipe directly into secret-safe sinks
(SetText, SetTexture, SetAlphaFromBoolean, SetVertexColorFromBoolean,
SetShownFromBoolean, SetTimerDuration).

Refactor of TargetedList_DelayedPickup:

  * REMOVED: important-spells filter at pickup. Will be re-added in
    commit DanderBot#5 via SetShownFromBoolean(IsSpellImportant(spellId), true,
    false) at render time, so the filter never inspects the secret
    bool in Lua.

  * REMOVED: spellName / spellTexture reads at pickup. The render
    pipeline will fetch them via C_Spell.GetSpellName /
    C_Spell.GetSpellTexture and feed them straight into SetText /
    SetTexture sinks.

  * Active-cast record now stores spellId opaquely as a secret token
    — never inspected, only passed into C_Spell.* + sinks.

  * Debug log simplified to clean values only: casterUnit + channel
    flag. The previous tostring(spellName) call would have crashed on
    a secret string.

Refactor of TargetedList_OnCastStop:

  * REMOVED: interrupter source extraction (gotcha DanderBot#7). The
    interrupter GUID arrives in the event payload as a secret string;
    truth-testing it ('not interrupterGuid') is not allowed and
    UnitNameFromGUID(secretGuid) returns a secret string that can't
    be formatted in Lua. The render pipeline will display the
    interrupter name (if at all) by piping UnitNameFromGUID's result
    directly into a SetText sink at render time.

  * Stop event still records the wasInterrupted flag for the render
    pipeline to play the interrupted-flash animation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants