Automation and Releases
Spectrum Federation uses a beta-first workflow. Normal pull requests target beta; main is updated by the promotion workflow.
Pull-request validation
PRs to beta
.github/workflows/pr-beta-validation.yml runs when relevant addon, automation, workflow, or documentation files change. It:
- detects whether packaged addon files changed (via
classify_promotion_scope.py); - runs the unified Lua/YAML/Python linter;
- runs focused Python/Lua tests, including
tests/test_promotion_scope.py,tests/test_sync_protocol.py, andtests/test_linked_identity.py; - validates package structure;
- requires a TOC version bump and a non-duplicate beta release only when a packaged addon file changed;
- builds MkDocs in strict mode.
Documentation-only changes do not require an addon version bump. Packaged-addon detection uses .github/scripts/classify_promotion_scope.py, so zip-excluded files such as */AGENTS.md do not count as addon changes.
PRs to main
.github/workflows/pr-main-validation.yml runs lint, tests, packaging, stable-version-format, and documentation checks. Direct feature work should not normally target this branch.
.github/workflows/pr-template-validation.yml separately validates pull-request template completion. In-game testing must be either marked complete or explicitly marked not applicable. N/A is rejected when packaged addon files changed, or when a TOC change is runtime-affecting, unknown, or not inspectable. Zip-excluded repository files such as */AGENTS.md may still use N/A.
Both branch-validation workflows include README.md, tests/**, pkgmeta.yaml, and MkDocs inputs (docs/**, mkdocs.yml, overrides/**, requirements-docs.txt) in their path filters.
Post-merge beta release
.github/workflows/post-merge-beta.yml is triggered by pushes under the packaged addon trees or pkgmeta.yaml. Path filters start the workflow; they do not encode individual zip exclusions. The workflow classifies the immutable github.event.before...github.sha range with classify_promotion_scope.py before any release side effects. Zip-excluded addon-tree files, including */AGENTS.md, do not set release_required. A pkgmeta.yaml change does, because package validation and WowUp's git packaging path still read that file. CurseForge no longer rebuilds releases from it once automatic packaging is disabled.
When release_required is false, changelog, README badge, GitHub Release, Wago, and CurseForge side effects are skipped. Lint/packaging/docs validation and merged-branch cleanup still run; they do not depend on a successful publish. A guidance-only addon-tree push can therefore start the workflow, classify as release_required=false, skip every release/version job, and still run sanity checks plus merged-branch cleanup.
When release_required is true, the workflow:
- reruns lint, packaging, and documentation validation;
- verifies the TOC was bumped against the previous beta tip (
check_version_bump.py --base-commit) and is not a duplicate release; - queries Blizzard's beta product for Interface metadata;
- updates
CHANGELOG.md; - updates README badges;
- checks out the captured push SHA, overlays generated
CHANGELOG.md/README.mdfrom livebeta, refuses to publish if any packaged addon file has advanced, packages the addon zip, writes WowUprelease.json, creates a GitHub prerelease, and uploads the same zip to Wago asbeta; - deletes the merged source branch after a successful or skipped publish, never after a failed one.
Version extraction, duplicate-release checks, and the publisher all use the captured push SHA rather than live beta. publish_release.py also refuses to create a zip whose requested version does not match the packaged parent and child TOC versions. Concurrency serializes workflow runs, but it does not freeze the beta branch; the captured SHA plus packaged-tree verification is what keeps a later push from being published under an earlier version.
A packaged addon change with a forgotten or invalid version fails the workflow instead of silently skipping the beta release.
Docs-only merges do not trigger a beta addon release.
Parent/child addon membership and zip exclusions live in .github/scripts/validate_packaging.py. The validation zip, production zip, and release-scope classifier all consume those definitions. The classifier also treats pkgmeta.yaml as release-relevant even though that file is not a zip member.
Changelog automation
Beta and main changelog updates share .github/scripts/update_changelog.py. The workflows stay separate; only the generation logic is shared.
What updates each changelog
- Beta:
.github/workflows/post-merge-beta.ymlruns the script after an addon change is pushed tobeta. The new## [X.Y.Z-beta.N]section describes that incremental development update. - Main:
.github/workflows/promote-beta-to-main.ymlruns the same script after beta is merged tomainand the TOC version is stripped toX.Y.Z. That section describes the net user-facing result since the previous main release. - Compatibility:
.github/workflows/update-wow-game-version.ymlsetsCHANGELOG_MODE=compatibilityand writes a deterministic Interface-only note without inventing gameplay changes.
Deterministic vs AI work
The script determines the following without a model:
- whether this is a beta update or a promotion
- the git range to analyze
- changed addon files, commits, and pull-request metadata
- which existing changelog sections belong to the current release train
- whether the current version section already exists
- whether output is valid enough to write
AI is used only for semantic judgment: whether a change is user-facing, how related beta work should be grouped, and the wording of entries. The model must return JSON. Entries are rejected when they are ungrounded, low-confidence, placeholders, or malformed.
GitHub Models is retired. The script uses Copilot CLI when it is installed, or an optional OpenAI-compatible endpoint from CHANGELOG_AI_BASE_URL / CHANGELOG_AI_API_KEY. If no model is available, it falls back to pull-request titles and beta notes that can be grounded in the net file list. It does not write the old "Infrastructure and tooling updates" placeholder.
Promotion range
For Promote Beta to Main, the range is the previous stable vX.Y.Z tag (or the first parent of the previous promotion merge) through the commit being promoted. Beta changelog sections for the same X.Y.Z train are inputs to consolidate; they are not copied one-for-one onto main.
The dry-run promotion job fetches the captured promotion target SHA, uses the incoming update_changelog.py from that commit, and analyzes HEAD...<target SHA> with the upcoming stable version so the changelog path can be validated without pushing.
Safeguards
- Historical stable sections are not rewritten.
- Rerunning a job does not replace an existing non-placeholder section for the same version.
- Internal-only changes (CI, tests, docs, TOC metadata) do not create a user-facing entry.
- Reverted beta work that is absent from the net addon diff is omitted from the main entry, including when the remaining files do not match a known feature name. Pull-request titles and commit subjects are filtered the same way so they cannot restore a dropped feature.
- If the model is uncertain and no grounded fallback exists, the script does not invent a stable entry. On promotion it still removes leftover
-betasections somaindoes not keep prerelease headings.
Deterministic range, validation, and write-safety behavior is covered by tests/test_update_changelog.py.
Promote beta to main
.github/workflows/promote-beta-to-main.yml is manually dispatched with no inputs. Every run performs a complete local dry-run phase first. The actual phase starts automatically only if all required dry-run jobs succeed, including jobs that were skipped because they were not applicable.
Branch promotion and downstream publishing are separate decisions. Every valid dispatch still promotes the captured beta SHA into main when that SHA is not already contained in main, and fast-forwards beta onto current main when remote beta has not advanced. If the captured target is already an ancestor of main, the merge is a no-op: main is preserved and beta can catch up without an empty promotion commit. Changelog generation, README badge work, stable addon publishing, and MkDocs deployment run only when the promotion scope says they are applicable.
How promotion scope is determined
The first job captures an immutable range and classifies it with .github/scripts/classify_promotion_scope.py. Detect still checks out live beta so HEAD is the captured target, and it copies the helper from that live checkout first so packaged membership matches the tree being classified. If leftover beta predates the helper, it falls back to origin/main, then the dispatched workflow commit (github.sha). Those SHAs are invariants for the rest of the run, not informational outputs:
promotion_base_shaisorigin/mainat workflow start.promotion_target_shais thebetaHEAD at workflow start.- Changed files are
git diff --name-onlyfrommerge-base(base, target)totarget(origin/main...beta). That is the incoming promotion, not later workflow-generated commits.
Before the dry-run merge, the real merge, and the final beta sync, the workflow re-fetches origin/main and origin/beta and verifies them with classify_promotion_scope.py --verify-refs. Merge jobs check out main, so they copy that helper from a verified git object. The copy must support --validate-versions (and therefore --decide-merge): the workflow prefers the captured target SHA and falls back to current main when leftover beta predates the flag. Fast-forward jobs likewise copy a helper that supports --verify-refs, preferring the dispatched workflow commit, then origin/main, then the captured target. Git ancestry, via --decide-merge, then decides whether a merge commit is required. has_incoming_changes is a changed-file classification and is not used as the merge skip switch. When the captured target is not already contained in main, merge and file checkouts use that target SHA, not the live beta ref. When it is already contained, the workflow does not manufacture an empty merge commit and does not overlay CHANGELOG/README/TOC files from the older beta target. If either branch has moved unexpectedly, the job fails and tells the maintainer to rerun the promotion so scope and mutation stay aligned.
The script is the source of truth for path classification. Update it when packaged addon roots, zip excludes, pkgmeta.yaml release membership, or MkDocs inputs change. It does not use AI.
| Flag | Meaning |
|---|---|
addon_changed |
A file that ships in the release zip changed, or pkgmeta.yaml changed. Addon roots and zip exclusions come from validate_packaging.py; */AGENTS.md and *.git* are excluded. pkgmeta.yaml is not a zip member. Package validation and WowUp's git packaging path still read it. CurseForge direct publishing uploads the canonical zip instead of rebuilding from this file. |
docs_changed |
MkDocs sources changed: docs/**, mkdocs.yml, overrides/**, or requirements-docs.txt. |
readme_changed |
README.md is in the incoming diff. |
release_required |
Same as addon_changed. Incoming packaged addon or pkgmeta.yaml changes warrant a stable release. Generated TOC/version commits do not create this flag. |
changelog_required |
Same as release_required. AI may write the changelog text; it does not decide whether a changelog is needed. |
documentation_deploy_required |
Same as docs_changed. Docs-only promotions deploy MkDocs and do not publish an addon release. |
readme_work_required |
Incoming README change or a stable addon release (badge/version updates). |
These flags are independent. Addon plus documentation is addon_changed=true and docs_changed=true, which requires both a stable release and MkDocs deployment.
| Incoming changes | Promote branches | Changelog | Addon release | MkDocs deploy | README work |
|---|---|---|---|---|---|
| Addon only | Yes | Yes | Yes | No | Yes |
| Docs only | Yes | No | No | Yes | No |
| Addon + docs | Yes | Yes | Yes | Yes | Yes |
| README only | Yes | No | No | No | Yes |
| Workflow/dev only | Yes | No | No | No | No |
| No downstream-relevant changes | Yes | No | No | No | No |
The workflow:
- classifies the incoming
main...betarange and captures the base/target SHAs; - validates lint, packaging, docs, and TOC version format. Addon releases require
X.Y.Z-beta.Non the captured beta target. Non-addon merges require a stableX.Y.Zon that target so a prerelease TOC cannot be overlaid ontomain. When the captured target is already contained inmain, the captured main SHA is authoritative and must be stableX.Y.Z; the leftover beta checkout may still contain-beta.N; - dry-runs only the applicable merge, changelog, README, docs, release, and fast-forward steps without pushing, using the captured target SHA and the same ref-drift checks as the real merge;
- re-verifies that
origin/mainandorigin/betastill match the captured SHAs, then merges the captured target SHA intomainonly when that target is not already contained inmain; - when
release_required, removes-beta.N, fetches the live Interface value, updates the changelog, verifies themaincheckout still matches the captured packaged source (TOC rewrites allowed), and publishes a stable GitHub Release plus a Wagostableupload; - when
readme_work_required, updates README badges; - when
documentation_deploy_required, deploys MkDocs frommain; - re-verifies that
origin/betastill equals the captured target and that the target is an ancestor oforigin/main, then fast-forwardsbetawith a non-forcegit push origin origin/main:refs/heads/beta. Newer beta work is never overwritten.
Dry-run and real jobs consume the same scope outputs. The dry-run summary prints the detected flags and which operations would run. The final summary compares detected scope, required operations, and actual job results. If a required operation is skipped or fails, or a non-required operation runs, the summary fails the workflow instead of reporting success.
Older instructions that ask for a promotion dry_run input are obsolete; the workflow now always validates with its built-in dry-run phase.
When a dry-run README job runs, it uploads its simulated stable badge output to the dry-run docs job, which applies simulated stable TOC metadata before calling validate_docs.py. Docs-only dry runs skip that overlay and validate the incoming documentation as-is. The final main deployment also calls the validator after generated metadata is pushed.
GitHub, CurseForge, and Wago publishing
.github/scripts/publish_release.py is the shared publisher for beta and stable addon releases. GitHub Releases remain part of the pipeline. CurseForge and Wago are explicit downstream destinations. Do not replace GitHub Releases with either external publisher.
A live publish does the following:
- Build the existing release artifacts: the addon zip (parent plus packaged child addons from
validate_packaging.pyat the zip root) and WowUp Hubrelease.json. The publisher refuses to zip when the requested version does not match the packaged TOC versions. - Build release notes from
CHANGELOG.md. - Create or update the GitHub Release (prerelease for
-beta,-alpha, and-rcversions). - After GitHub succeeds, attempt CurseForge and Wago independently. Neither destination waits on the other, and neither deletes a destination that already succeeded.
If GitHub fails before a release exists, CurseForge and Wago are not attempted. If GitHub succeeds and one downstream destination fails, the workflow fails after the other destination has been attempted. The GitHub Release, and any downstream publish that succeeded, stay in place.
WowUp continues to use GitHub release.json and the canonical zip. Changelog text is reused for GitHub release notes, the CurseForge changelog, and the Wago changelog. There is no second changelog generator.
Version mapping
Classification comes from the version string, not from the current git branch:
| Version | GitHub | Wago stability | CurseForge release type |
|---|---|---|---|
1.4.0 |
Release | stable |
release |
1.5.0-beta.1 |
Prerelease | beta |
beta |
1.5.0-alpha.1 |
Prerelease | alpha |
alpha |
1.5.0-rc.1 |
Prerelease | beta |
beta |
Matching is case-insensitive (1.5.0-BETA.2 is a GitHub prerelease, Wago beta, and CurseForge beta).
Project IDs and Retail patch
The public Wago project ID is stored as ## X-Wago-ID: on the parent TOC and on every packaged child TOC. Child addons reuse that same ID; they do not get a second Wago project. WowUp and Wago use the shared ID to install the sibling folders from one listing. The publisher reads the parent TOC field instead of hard-coding the ID in workflows.
The public CurseForge project ID is ## X-Curse-Project-ID: on the parent TOC only. It is not a secret. Direct publishing reads that field and refuses to upload when it is missing or not a positive integer. The current project ID is 1445757.
pkgmeta.yaml still lifts each shipped addon to the zip root for git packagers and for package validation. A parent-only flatten leaves SpectrumFederation_CursedSurgeTracker and SpectrumFederation_RCLootCouncilIntegration nested inside SpectrumFederation/, which installers never load as optional AddOns. CurseForge direct publishing uploads the canonical zip produced by publish_release.py. It does not ask CurseForge to rebuild the addon from pkgmeta.yaml. Keep pkgmeta.yaml; WowUp's git packaging path and validate_packaging.py still depend on it.
The Retail patch sent to Wago and used to select the CurseForge game version is the human-readable form of the 6-digit Interface number already used for releases (120100 → 12.1.0).
- Wago requires that exact string in
https://addons.wago.io/api/data/game. The script does not claim an older patch. - CurseForge requires that exact Retail name from the documented Game Versions API at
https://wow.curseforge.com/api/game/versions. The numeric game-version ID is resolved from that catalog and is not hard-coded. The Retail type comes fromgame/version-types: the author catalog names itWorld of Warcraft/world-of-warcraft, and a label that containsretailis also accepted. Classic, PTR, public-test, and other non-Retail version types are ignored. A unique patch name is not treated as Retail when that type cannot be identified, including when only a PTR entry exists for an upcoming patch. If the exact Retail patch cannot be identified, CurseForge publishing fails. The catalog result is reused for the rest of that release process. - CurseForge game-version compatibility is read from
## Interfacein the parent TOC inside the release zip. If the--interfaceargument differs from that packaged value, CurseForge publishing fails instead of claiming the argument's newer patch. Wago still uses the--interfaceargument.
Release notes reuse CHANGELOG.md. Beta versions look for ## [X.Y.Z-beta.N] and then ## [Unreleased - Beta]. Stable, alpha, and RC versions use exact-heading lookup only; current changelog automation does not create alpha/RC sections, and the publisher does not invent them.
Credentials
WAGO_API_KEYis the Wago developer API credential. Direct publishing uses only this value, asAuthorization: Bearer …according to Wago's API docs.WAGO_API_SECRETis the signing secret from the old Wago GitHub Release webhook. The publisher does not read or send it.CURSEFORGE_API_TOKENis the CurseForge author Upload API token. Direct publishing sends it only as theX-Api-Tokenheader, per the CurseForge Upload API. It is not placed in the request body or query string.
Never log these secrets, Authorization headers, X-Api-Token values, or the legacy CurseForge webhook token. The publisher does not read the legacy webhook token.
Migration: CurseForge automatic packaging
Wago's old GitHub Release webhook should already be disabled. Leave WAGO_API_SECRET unused.
CurseForge automatic packaging may still be enabled when this publisher first ships, so beta releases are not lost before the replacement is in production. Do not disable it before the direct publisher is merged and the repository secret exists. After that, complete this one-time manual migration. Repository code does not change CurseForge account settings or revoke tokens.
- Confirm
CURSEFORGE_API_TOKENexists in repository Actions secrets. - Validate the direct publisher with dry-run and
tests/test_publish_release.py. - Merge and deploy the direct publisher.
- Disable CurseForge repository Automatic Packaging.
- Remove or disable the CurseForge GitHub webhook.
- Revoke the old CurseForge token labeled
Webhooksonce nothing uses it. - Keep
CURSEFORGE_API_TOKEN. - Verify the next beta release on GitHub, CurseForge as Beta, Wago as Beta, and WowUp.
The first release that includes this publisher can race the still-enabled automatic packager. Disable automatic packaging as soon as the direct publisher is deployed. If both paths upload the same version, remove the extra CurseForge file manually. A later rerun treats an exact existing file or an explicit duplicate response as success and does not upload another copy.
Dry-run
A dry-run must not create a GitHub Release, upload to CurseForge, upload to Wago, or mutate any external release service. It validates the CurseForge project ID, release type, and Retail patch locally, and it may resolve the public Wago catalog before printing the simulated actions. It does not send CURSEFORGE_API_TOKEN. The numeric CurseForge game-version ID is resolved during a live publish, when that token is available.
From the repository root, after substituting the version and Interface values you intend to publish:
The promotion workflow already runs this with --dry-run during its validation phase. Dry-run does not require WAGO_API_KEY, CURSEFORGE_API_TOKEN, or a GitHub token.
Retry when one destination fails
- If GitHub fails, CurseForge and Wago are not attempted.
- If GitHub succeeds and CurseForge fails, Wago is still attempted. Keep the GitHub Release and any Wago release that succeeded. Fix the CurseForge error and rerun
publish_release.pyfor the same version. - If GitHub succeeds and Wago fails, CurseForge is still attempted. Keep the GitHub Release and any CurseForge file that succeeded. Fix the Wago error and rerun the same version.
- Do not delete a GitHub Release, CurseForge file, or Wago version because a different destination failed.
- A Wago or CurseForge HTTP 409 is treated as success only when the response body clearly says this exact version or file already exists. An empty or generic 409 is a visible failure.
- Before uploading, the publisher also checks
GET /api/projects/{projectId}/fileson the CurseForge author API for an exact zip name or display name. A name match is an existing release only when its release type, game version, and usable state also match this publish. A same-named file that is Stable instead of Beta, targets a different game version, or is rejected or unavailable is a visible failure, and the publisher does not upload another copy. That list call is not part of the documented Upload API. A missing, forbidden, or otherwise unusable list response is treated as unavailable and does not block the upload. A listed name that omits those metadata fields is not treated as success; the documented upload endpoint then decides. Duplicate safety still uses the explicit upload response described above. A generic conflict is still a failure. - An unexpected error in the CurseForge attempt does not skip Wago, and an unexpected error in the Wago attempt does not discard a CurseForge result that already succeeded.
- If the GitHub Release already exists, the existing update/reuse path is preserved, and CurseForge and Wago are still attempted afterward.
Tests
Release classification, CurseForge and Wago metadata, credential handling, and mocked HTTP behavior are covered by tests/test_publish_release.py.
Roll back a release
.github/workflows/rollback-release.yml accepts:
release_tag, such asv1.0.0;dry_run, which defaults totrue.
It supports releases created by beta-to-main promotion. In live mode it identifies and reverts the promotion merge, deletes the GitHub release and tag, and restores the pre-promotion changelog.
Run and review the default dry run before setting dry_run to false.
Interface synchronization
.github/scripts/blizzard_api.py queries Blizzard's public version endpoint for current metadata and formats the README Interface badge from that 6-digit Interface number (120100 → 12.1.0).
Manual Update WoW Game Version workflow
.github/workflows/update-wow-game-version.yml is a workflow_dispatch-only workflow that updates Retail compatibility on main and beta without promoting unfinished beta features.
Typical use: Blizzard ships a Retail patch while beta still contains unfinished work. Promote Beta to Main would either wait or ship unfinished features; this workflow updates Interface/version metadata on each branch and republishes both channels through publish_release.py, while keeping beta's Git history synchronized with the completed main update.
Inputs:
game_version: human-readable Retail version such as12.1.5(converted to Interface120105);dry_run: defaults totrue; previews the plan, simulates main→beta Git integration, and validates packaging/publish dry-runs without mutating branches or publishing;retry_publication: defaults tofalse; whentrue, reuses the existing channel-matched TOC versions and runs the idempotent publisher again (CurseForge/Wago recovery after GitHub already succeeded). Does not bump versions, invent new releases, or perform history synchronization. If main→beta sync is still outstanding, fail and complete a normal run first.
Behavior:
- Must be dispatched from
main(the workflow file must already be on the default branch). - Validates the requested version against live Retail before any mutation.
- Plans independent
main(X.Y.Z) andbeta(X.Y.Z-beta.N) version bumps, keeping packaged child TOCs in lockstep. - Preflight simulates the main update, main→beta synchronization, and beta finalize against captured SHAs; unexpected generated-metadata conflicts or unrelated main-only edits fail before mutation.
- Updates
mainfirst (when needed), then synchronizes that exact main commit intobetabefore any beta push. Automatic conflict resolution is limited to generated TOC Interface/Version fields, README badges, and section-aware CHANGELOG reconciliation. - Finalizes beta metadata in one validated tree (stable metadata never remains on beta) and pushes once. History-only synchronization preserves existing beta release notes and does not invent a beta version bump.
- Writes deterministic compatibility changelog entries only when that channel's version update is planned (no invented gameplay notes).
- Updates README badges, then publishes GitHub/Wago/CurseForge via the existing publisher. After history-only sync, publish uses the post-sync beta commit rather than the pre-sync captured SHA.
- Always materializes automation helpers from the dispatched workflow commit via
git showinto a shared helpers directory (imports included), so an older beta tip can be updated without first promoting unfinished beta code that lacks those helpers. - Uses
PAT_TOKEN(repository Admin) for protected branch pushes because main/beta rulesets require pull requests and only Admin role bypasses them. Do not disable protection. The token must also be allowed to push workflow-file changes onbetawhen this workflow rewritespost-merge-beta.ymlconcurrency settings. - Skips Post-Merge Beta for
chore: bump Interface to …andchore: sync main game-version update into beta …commits; preflight also fails on active/queued Post-Merge Beta / Promote / Rollback runs by workflow name. - Distinguishes
none/update/publish: never publishes a stable TOC version from beta, avoids tag collisions after rollbacks, and recovers a missing GitHub release without bumping. An already-completed matching release is a no-op unlessretry_publicationis explicitly enabled for downstream CurseForge/Wago recovery. When a real Interface bump causes main to catch beta's prerelease train, beta advances even if its Interface already matches. Matching stable TOC on both branches after promotion is a no-op only when the relevant main commit is already in beta ancestry. - Shares the
release-channelsconcurrency group withqueue: maxso ordinary pending beta releases are not silently replaced while another release-channel workflow is queued. - Newly created GitHub release tags set
--targetto the captured publishing checkout SHA so stable and beta tags point at their release source rather than the repository default branch.
Planning, synchronization, and TOC helpers live in .github/scripts/wow_interface_sync.py.
Required secret / ruleset configuration
Before the first live run, confirm repository secret PAT_TOKEN is a token for a user with the repository Admin role (the Main/Beta rulesets bypass RepositoryRole Admin). The default GITHUB_TOKEN cannot push through those pull-request rulesets. Alternatively, an administrator may add an explicit ruleset bypass for the GitHub Actions app; do not remove required reviews or status checks.
When changing parser or version-plan behavior, run:
When changing Interface badge formatting, run:
Validation scripts
| Script | Purpose |
|---|---|
lint_all.py |
Lua, YAML, and Python validation. |
validate_packaging.py |
TOC references, required files, and package structure. |
validate_docs.py |
Repository-specific documentation guardrails plus mkdocs build --clean --strict. |
check_version_bump.py |
Compare TOC versions against a base branch. |
check_duplicate_release.py |
Reject an existing release version. |
publish_release.py |
Build release artifacts, create or update the GitHub Release, then publish the same zip to CurseForge and Wago. |
update_changelog.py |
Update the beta changelog after merge, or consolidate the main changelog during promotion. |
classify_promotion_scope.py |
Classify a git range or file list into addon/docs/README/infra flags used by promotion and PR addon detection. |
cleanup_merged_branch.py |
Remove the merged source branch after beta release. |
Use the scripts rather than reproducing their logic in ad hoc commands.
Documentation guardrails
validate_docs.py fails before the MkDocs build when it finds:
- a Markdown page missing from navigation or a navigation target missing on disk;
- a broken relative link or image;
- known stale commands, APIs, module paths, workflow names, or Blizzard Settings instructions;
- a referenced workflow, script, or asset that does not exist;
- missing automation-compatible README badges, or stable-branch badge values that disagree with
SpectrumFederation.toc(the Interface badge uses the human-readable form, so120100is shown as12.1.0); - a registered
/sfcommand or sync diagnostic alias missing from the command reference; - a visible unimplemented settings action whose limitation is no longer documented.
When behavior changes intentionally, update the implementation and its documentation together. Remove or revise a stale-pattern rule only when the old form has genuinely become valid again.
Version and manifest source of truth
Release zips contain sibling top-level folders SpectrumFederation/, SpectrumFederation_CursedSurgeTracker/, and SpectrumFederation_RCLootCouncilIntegration/. Extracting the archive into Interface/AddOns installs those addons. Packaged child TOCs must keep the same ## Interface and ## Version values as the parent.
README badges are generated release metadata, not the source of truth. The Interface badge is formatted from the 6-digit Interface number (the same MMmmpp value stored in the TOC) by adding decimals and stripping leading zeros (120100 → 12.1.0).