Skip to content
WolfWave
Developers

Development

Build WolfWave from source and send a pull request. Prerequisites, Config.xcconfig, make targets, tests, lint gates, release builds, and CI.

TL;DR: clone the repo, copy Config.xcconfig, run make build and make test, then open a PR. This page is for contributors. Just want to use the app? See Installation.

The path

  1. Check the prerequisites.
  2. Clone the repo.
  3. Add your API keys in Config.xcconfig.
  4. Build and run the Debug app.
  5. Run the tests and the lint gates.
  6. Open a pull request.

Prerequisites

  • macOS 26.0+ (Tahoe) on Apple Silicon (M1 or later).
  • Xcode 26+, with Command Line Tools (xcode-select --install).
  • Docker Desktop, only for make lint and make lint-crash-safety (they run a pinned SwiftLint image).
  • bun 1.3.11, only if you touch the docs site, OBS widget, Stream Deck plugin, or design tokens.
    • Use exactly the version in the root package.json packageManager field.
    • Other bun releases minify the widget differently, so CI's drift check fails.

Only editing Swift? You don't need bun. Xcode ships the committed widget.html as-is.

Clone the repo

git clone https://github.com/MrDemonWolf/wolfwave.git
cd wolfwave

Touching web code too? Install the workspaces with the pinned bun:

bunx bun@1.3.11 install --frozen-lockfile

--frozen-lockfile keeps bun.lock unchanged. Don't commit lockfile churn.

Configure API keys

Copy the example config:

cp apps/native/WolfWave/Config.xcconfig.example apps/native/WolfWave/Config.xcconfig

Then fill in the two required keys:

TWITCH_CLIENT_ID = your_twitch_client_id_here
DISCORD_CLIENT_ID = your_discord_application_id_here

Config.xcconfig is gitignored. Every contributor uses their own. Never commit it.

Optional keys

KeyWhat it setsDefault when blank
GITHUB_REPO_OWNER / GITHUB_REPO_NAMERepo used for update checks and issue linksUpstream repo
DOCS_URLDocs site root (privacy, terms, changelog links derive from it)Upstream docs
COMMUNITY_DISCORD_URLTray menu Community Discord linkUpstream invite
COPYRIGHT_HOLDERLegal name in About and Monthly Wrap footersMrDemonWolf, Inc.

Escape // in URL values with $(), like DOCS_URL = https:/$()/example.com. xcconfig reads a bare // as a comment and silently cuts the value to https:.

Where these values come from, in priority order:

  1. Your Config.xcconfig. Local builds only.
  2. CI. Release and test workflows write the keys into a generated Config.xcconfig.
  3. Core/AppConstants. The fallback when a key is blank.

To rename the app itself, set PRODUCT_NAME and INFOPLIST_KEY_CFBundleDisplayName in the build configuration. Keep the Debug and Release names different.

Build and run

make open-xcode

Press ⌘R in Xcode. Or build from the terminal with make build.

  • Debug builds are a separate app: WolfWave Dev (com.mrdemonwolf.wolfwave.dev), with a DEV badge on its icon.
  • It lives beside a real WolfWave install without touching its settings or Keychain.
  • make clean-dev resets WolfWave Dev to a fresh first run. It never touches the production app.

Make targets

CommandWhat it does
make buildDebug build
make cleanClean build artifacts
make clean-devQuit WolfWave Dev, wipe its settings and Keychain entry, clean builds
make testUnit tests (XCTest + Swift Testing)
make test-verboseUnit tests with full xcodebuild output
make test-uiXCUITest suite (launches the real app)
make test-ciUnit tests the way CI runs them, writes TestResults.xcresult
make ciAlias for make test-ci
make lintSwiftLint against the tracked baseline (needs Docker)
make lint-baselineRegenerate the baseline. It may only shrink
make lint-crash-safetyNo new force unwrap, try!, or as! (needs Docker)
make lint-headersSwift file-header convention
make lint-syncSource-derived lists, component catalog, docs values (incl. ports), lint claims, release-notes and version script tests
make widgetRebuild the OBS widget into Resources/widget.html
make iconsRegenerate the Debug app icon from AppIcon.icon
make sponsor-configRegenerate SponsorConfig.generated.swift from FUNDING.yml
make check-driftRegenerate widget, tokens, SponsorConfig, and Debug icon; fail on drift
make update-depsResolve SwiftPM dependencies
make open-xcodeOpen the Xcode project
make prod-buildRelease build + DMG in builds/
make prod-installRelease build + install to /Applications
make notarizeSign, notarize, and staple the DMG (needs Developer ID)
make verify-notarizeCheck the DMG's stapled ticket and Gatekeeper acceptance

make build, every make test* target, and make prod-build run sponsor-config first, so you rarely call it yourself.

Testing

Run make test (or ⌘U in Xcode). It prints the file and pass count.

  • Unit tests live in apps/native/WolfWaveTests/.
  • New test file? Drop a .swift file in that folder and @testable import WolfWave. No Xcode project edit needed.
  • Your real data is safe. Tests use a throwaway settings suite and an in-memory Keychain. Never use UserDefaults.standard in a test.
  • Isolated output. make test* builds into the gitignored DerivedData/Tests, so the unsigned test host never replaces your signed Debug app.

The rules for shared test state (the SharedTestStateIsolation lock, WolfWaveTestCase) are in the repo's CLAUDE.md.

UI tests

make test-ui runs WolfWaveUITests. It launches the real app and drives it:

  • the onboarding wizard, end to end;
  • every settings pane, with a screenshot of each attached to the result.

The app runs with WOLFWAVE_UI_TEST=1, which swaps in throwaway settings and Keychain storage and keeps Apple Music, Twitch, Discord, and Sparkle off. UI tests run as their own CI job and never gate a release. Details: apps/native/docs/end-to-end-testing.md.

Web workspace tests

bun run --filter widget test       # OBS widget (bun test + happy-dom)
bun run --filter streamdeck test   # Stream Deck plugin
bun run ds:test                    # design-system lint rules

Lint gates

Run these before you push. CI runs the same ones.

make lint                # SwiftLint against the tracked baseline
make lint-crash-safety   # Blocking: no new force unwrap, try!, or as!
make lint-headers        # Blocking: Swift file headers match the Xcode template
make lint-sync           # Blocking: source, catalog, and docs stay in sync
bun run ds:lint          # Blocking: no literal font sizes, spacing, padding, or radius in views
bun run ds:schema        # Validate tokens.json against its schema

Generated files

Some committed files are generated. Never edit them by hand.

Generated fileSourceRegenerate with
Resources/widget.htmlapps/widget/make widget
Tokens.generated.swift, tokens.generated.css, widget-tokens.generated.js, widget-themes.generated.tsdesign-system/tokens.jsonbun run tokens
Resources/AppIcon-Dev.iconResources/AppIcon.iconmake icons
Core/SponsorConfig.generated.swift.github/FUNDING.ymlmake sponsor-config

Commit the regenerated output with your source change. make check-drift runs CI's drift check locally. The widget pipeline is covered on the OBS Widget page.

Open a pull request

  1. Branch off main. One concern per PR.
  2. Run make test and the lint gates.
  3. Fill in the PR template checklists.
  4. User-facing change? Add a one-line bullet to CHANGELOG.md under the top ## [x.y.z] heading, and to the top block of changelog.mdx.
  5. Contributor-only change? Put it under ### Developer in CHANGELOG.md. It skips the in-app notes and the docs changelog.
  6. Update the docs your change touches (features, settings, bot-commands, architecture).
  7. Attach a screenshot or recording for any UI change.

Don't bump CURRENT_PROJECT_VERSION. CI sets the build number from scripts/version.sh.

More detail: CONTRIBUTING.md and CLAUDE.md.

Rich Presence art assets (fork maintainers)

Rich Presence shows two images registered on the Discord app. They are not bundled with WolfWave.

Asset nameShown when
apple_musicFallback large image, and the small badge while playing
pauseSmall badge while paused

Shipping a fork with your own DISCORD_CLIENT_ID?

  1. Open your Discord app's Rich Presence → Art Assets page.
  2. Upload both PNGs from discord-assets/ with those exact lowercase names.
  3. Wait about 10 minutes for Discord's CDN.

Production build

make prod-build
  • The DMG lands at builds/WolfWave-{version}.dmg.
  • With a Developer ID Application certificate in your Keychain, the app is re-signed for distribution (inside-out, via scripts/codesign-app.sh, the same script CI uses).
  • make prod-install builds and copies the app to /Applications.
  • The DMG background comes from scripts/generate-dmg-background.swift. Regenerate with swift scripts/generate-dmg-background.swift assets and commit the PNGs.

Notarization

APPLE_ID=you@example.com \
APPLE_TEAM_ID=YOUR_TEAM_ID \
APPLE_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx \
make notarize
  • make notarize signs, submits, and staples the DMG.
  • It then runs stapler validate and spctl --assess, so a bad ticket fails here, not on a user's Mac.
  • make verify-notarize reruns those two checks on an existing DMG.

Create an app-specific password at appleid.apple.com under Sign-In and Security → App-Specific Passwords.

CI/CD

WorkflowWhen it runsWhat it does
test.yml (CI)Every push and PR to mainUnit tests, drift checks, widget tests, UI tests, docs build, Stream Deck plugin, SwiftLint, crash-safety, headers, sync, design-system lint. Each job runs only when its files change
build_release.ymlv* tag pushBuild, sign, notarize, embed release notes in the Sparkle appcast, upload the DMG to a draft GitHub Release
update_homebrew.ymlAfter a release is publishedOpens a PR bumping the Homebrew cask
docs.ymlDocs changes, or a published stable releaseDeploys this site to GitHub Pages
nightly.ymlDaily at 05:00 UTCSigned build off main for the Nightly channel. Skipped when nothing that ships changed
dependabot.ymlWeeklyGrouped updates for GitHub Actions, SwiftPM, and bun

Required GitHub secrets

SecretWhat it is
TWITCH_CLIENT_IDTwitch application Client ID
DISCORD_CLIENT_IDDiscord application ID
DEVELOPER_ID_CERT_P12Base64 Developer ID certificate
DEVELOPER_ID_CERT_PASSWORDPassword for that certificate
APPLE_IDApple ID for notarization
APPLE_TEAM_IDApple Developer Team ID
APPLE_APP_PASSWORDApp-specific password for notarization
SPARKLE_PRIVATE_KEYEdDSA key that signs the appcast
PROVISIONING_PROFILE_B64Optional. Developer ID profile with iCloud, for "Sync with iCloud"

Cutting a release

Push a tag that matches MARKETING_VERSION:

git tag vX.Y.Z
git push origin vX.Y.Z
  1. The workflow uploads to a draft release. Rerunning updates the draft; it never replaces a published release.
  2. Review the draft and publish it.
  3. The Homebrew cask and the docs site update on their own.

The full list is the Release Checklist.

Next steps

On this page