Workflows in .github/workflows/ cover tests, OTA game updates,
installers, the multiplayer service and native command parity. Test results are evaluated against
explicit coverage and known-failure inventories.
- What CI is for here
tests.yml- The skip gate
- The self-hosted runner
- Native command campaign
release.yml- Debugging a red run
- Re-baselining the skip counts
This suite does not fail when its inputs are missing. Tests that need the
1995 Warcraft II data, an asset pack or the Opus vectors call
Assumptions.assumeTrue(...) and skip, and Maven reports BUILD SUCCESS
either way. With nothing configured, 1,419 of 3,085 tests skip.
So the exit code certifies almost nothing on its own, and a CI job that trusts it converts "nobody is checking" into "something is checking" without either being true. Everything below exists to make the exit code mean something.
The hosted lane cannot exercise tests requiring original game media. The self-hosted authenticated lane exists so those tests run continuously instead of being silently counted as successful skips.
Runs on push, pull request and manual dispatch. Two jobs, deliberately different, and neither is redundant.
ubuntu-latest. Checks out only this repository, verifies the native source
boundary, and runs without proprietary game data. It asserts the data-free
profile.
This is the newcomer's path. It proves the suite behaves correctly for somebody who has cloned the repository and owns no copy of the game, which is most people who will ever look at it.
Two things about it are deliberate:
It asserts the runner's filesystem is case-sensitive before anything else. The reason this job is on Linux rather than macOS is to exercise asset path resolution where case matters. On a case-insensitive filesystem one wrong letter cannot fail. If GitHub ever moved the Ubuntu runners onto a case-insensitive filesystem, that coverage would vanish without a symptom, so the job fails loudly instead.
-Dwc2.install.dir points at a path that does not exist, so the run is
unambiguous: the data-gated tests skip because there is no data, not because of
anything subtler.
Asserts the full profile: 31 expected skips with the runner's
authenticated classic retail installation, derived pack and Opus fixtures.
An exact Battle.net Edition source
has a different per-module inventory because its data lives in TOMEs and it
does not expose several classic loose-file fixtures. That layout is covered by
the separate full-bne-with-playtest-saves profile instead of being folded
into the classic count.
scripts/ci/check-test-skips.py explains each release-dependent assertion.
About nine minutes.
It builds an asset pack from the mounted installation and runs the whole suite against the installation, pack and Opus vectors together.
The summary separates passed tests, known failures that still executed, unexpected failures, and skipped tests that did not execute. Both gates run even when one fails, so a skip mismatch cannot hide a regression. Green means both inventories match, not that every test passes.
The September 7 audit of run 34136321688 found two independent problems:
- The data-free engine inventory expected 989 skips but observed 1,008. Nineteen newly added authenticated tests were missing from the inventory; they ran in the full job. The new walking regression adds one more expected data-free skip. That full data-free run discovered 2,992 tests, skipped 1,345, and executed 1,647, including 88 recorded failures.
- The authenticated job discovered 2,990 tests, skipped the expected 27, and reported 110 failures against 109 recorded ones. Its extra failure asserted an internal cold-loop latch changed by the September 1 moving-quarry fix. The regression now checks the constructor that owns the retry; its existing route, position, timing, damage, and random-sequence assertions remain.
No expected-failure entries were added or tests disabled to resolve this audit. The canonical full-profile baseline then recorded 106 known failures. Three combat failures were reproduced in the pinned BNE executable and fixed at the swing-to-chase boundary; their original assertions now also run in the required 26-test control-liveness gate. The remaining entries are technical debt and still execute. A citation label alone does not establish retail verification.
publish-game-update.yml waits for tests.yml on the exact master commit.
Both Native Java, no game data and Authenticated game data must finish
successfully in a trusted push or manual run. A missing, skipped, cancelled or
failed job prevents publication; an older green revision cannot qualify a new
one. If master advances while the workflow waits, the superseded revision
cannot proceed. These jobs enforce the coverage and known-failure inventories,
so publication still does not imply that every test passed.
command-parity.yml runs the fixed 121-case native command campaign on the
private runner. It only executes trusted master code. Manual dispatch is
available; push runs require BNE_COMMAND_PARITY_ENABLED=true in repository
variables. Public pull requests only run the media-free campaign inventory
and regression-gate tests in tests.yml.
Before enabling the private workflow, mount both inputs read-only in the runner and configure these repository variables with their runner-visible absolute paths:
BNE_COMMAND_PARITY_STORE: directory containing the 121 pinned captures underobjects/.BNE_COMMAND_PARITY_PACK: the exact authenticated BNE pack named by the command campaign definition. The classic pack built by the ordinary authenticated test job is not interchangeable.
The existing runner's /opt/wc2/install and Opus mounts alone do not satisfy
these inputs. The native workflow is prepared but automatic push execution
stays disabled until its private mounts and variables are configured. Missing
inputs fail a requested run; they never produce a passing parity result.
The workflow retains only the regression summary as a GitHub artifact. Raw native fixtures, per-cycle observations and game media remain private. Local setup, baseline advancement and the exact metric boundaries are documented in the command campaign guide.
scripts/ci/check-test-results.py evaluates coverage and failure identity
for both jobs and writes their results to the GitHub Actions summary. The test step is
continue-on-error: true; the gate runs after it and reads the Surefire XML
rather than Maven's console output, because the XML records what each module
actually ran. Grepping a log cannot tell "the engine module skipped 226 tests"
from "the engine module never ran", and those must not look alike.
Per module and in total:
- skipped must be exactly the recorded number
- tests run must be at least the recorded number
The asymmetry is deliberate. Adding a test that always runs raises the run count and needs no change here. Adding a test that skips without game data raises the skip count and turns CI red until somebody writes the new number down. That second case is the one worth catching: it is how hundreds of tests came to be skippable in the first place, one at a time, with nothing objecting.
| Profile | Inputs | Skips |
|---|---|---|
data-free |
none | 1,419 |
full |
classic installation, pack, Opus vectors | 31 |
full-with-playtest-saves |
full inputs plus three private save referees | 28 |
full-bne-with-playtest-saves |
exact BNE source, matching pack, Opus references, and three private save referees | 30 |
The thirty-one that skip even in full want nothing anyone should have to
provide: seven need a display, five need a directory of 16-bit WAVs
(-Dopus.music) that this project does not ask anybody for, four are
fixture-sensitive, five need custom maps absent from the retail pack, three
need private playtest saves, one three-map recording matrix needs a BNE pack,
and the production service smoke runs only in the deploy workflow. One video
assertion depends on which retail release is mounted. Four soundtrack and
screen checks require BNE recordings. Session lifecycle checks launch real
windows in child JVMs; CI installs Xvfb so these execute despite Surefire's
headless setting.
The full-with-playtest-saves profile is the exact local counterpart for a
development machine that has the three private save referees installed. It
runs those checks instead of weakening the hosted full profile when stronger
local coverage legitimately removes three skips.
The full-bne-with-playtest-saves profile is the exact counterpart for the
sealed Battle.net Edition source archive and its matching signed pack. Its 30
skips are not weaker authentication: they record BNE's TOME layout, data-only
CUE, absent classic loose-map fixtures, display-only checks, optional music
fixture, release-sensitive unit declarations, and opt-in production smoke.
scripts/ci/check-test-failures.py, against the authenticated-data baseline
in scripts/ci/expected-failures.txt or the data-free baseline in
scripts/ci/expected-failures-data-free.txt.
The engine has a documented baseline failure set with all three inputs
configured, and both are stable. That is not a broken build. This is a parity
port whose suite is partly a specification written ahead of the code, so a
test asserting a behaviour nobody has ported yet does its job by failing.
parity-clean.txt treats LegacyEngine map traces this way -- as a list that only
grows on purpose, though it gates nothing and is a sanity check rather than a
statement about correctness -- and the skip gate treats
skips this way; failures had no equivalent, and the cost was that no run could
report a regression -- attributing a failure meant reverting a change and
re-running, and comparing two runs meant diffing test-method names by hand.
It compares the set of failing tests, not the count, because a count cannot tell one test being fixed and another breaking from nothing happening. Two things fail it, and they are different problems:
- a failure not in the file -- a regression, and the usual red;
- a file entry that now passes -- good news that still goes red, because a
baseline nobody prunes drifts into fiction. Re-baseline with
--writeand say in the commit which test was fixed.
Each line carries a verdict and a cites. cites is mechanical and
rewritten on every --write. verdict is a human judgement, preserved across
--write, and follows CONTRIBUTING.md's rule that parity is with the retail
Battle.net Edition: retail-verified asserts something read off the binary and
is the count worth watching, legacyEngine-default asserts a LegacyEngine rule with
no retail evidence, port-bug is an ordinary defect, unsorted means nobody
has looked. Today that is 0, 42, 4 and 126.
Both jobs install ffmpeg and flac, and the self-hosted one also installs
fontconfig and fonts-dejavu-core. These are not "inputs" in the sense the
other four are -- nothing points at them -- but the baselines assume them:
- Without
ffmpegandflac,FlacInteropTest's seven tests andOggTest's two skip. Those nine are the only checks in the tree that compare this project's FLAC and Ogg writers against the reference decoders, and their absence is what had CI red for nine pushes. - Without fonts, Java2D rasterises nothing, every piece of HUD lettering comes
out blank, and
HudInkTestfails. The runner container has no fonts at all.
The authenticated job uses a maintainer-managed Actions Runner Controller scale set. Network addresses, host layout, runner registration, and recovery procedures intentionally live in private infrastructure configuration rather than this public repository.
Public pull requests and non-default branch pushes can never select this job.
It runs only after a commit reaches master, or when a maintainer explicitly
dispatches it. The hosted data-free job remains the required public path.
It is not downloaded, not stored in an Actions secret, and not present in this
repository. The private runner mounts the installation, authenticated
movie/speech media, and official Opus vectors read-only at /opt/wc2.
The workflow fetches RFC 6716 from the RFC Editor into its temporary directory and passes that exact file to the tests; it does not silently depend on a stale host copy.
Private Maven and runtime caches persist between runs. They contain no source credentials or original game media.
The job asserts all seven required data files are readable before it builds anything, so a broken mount says "the data is not there" rather than surfacing forty minutes later as an arithmetic mismatch in the skip gate.
Not cached. About 95 seconds including verification. The point is that
PackParityTest compares what this commit's extractor produces against the
installation, rather than a stale artifact: a cached pack would make that test
report a difference belonging to neither.
Manual on purpose: the artefacts are large and slow. macOS .dmg, Linux .AppImage
and Windows .msi, each stamped with a version passed as a workflow input. The
optional Linux job also retains the same signed game-update assets used by the
automatic publisher. See packaging and
game updates.
The macOS job deliberately uses the same Apple-silicon host as ChonkBlocker:
[self-hosted, macOS, ARM64, chonk-mac-arm64]
Because iconidentify is a personal account, GitHub runner registrations are
repository-scoped. The host therefore runs a separate service named
m1-chonkcraft-arm64 from ~/actions-runners/chonkcraft-arm64; it shares the
physical Mac and Homebrew/JBR caches without sharing another repository's
credentials or work directory.
It imports the Developer ID certificate into a job-scoped keychain, signs the
JBR's nested native code from the inside out, signs the application and DMG with
the hardened runtime, submits the DMG to Apple's notary service, staples the
ticket, and verifies both Gatekeeper and a real launcher render. The keychain is
removed in an always() step and the shared runner's search list is restored to
the System keychain, matching the concurrency-safe ChonkBlocker pattern.
The signed application is copied into the DMG with hdiutil. Do not replace
that step with jpackage --type dmg --app-image: the latter silently re-signed
the already-valid nested application ad-hoc in the first production run. The
macOS JNA dispatcher is likewise extracted from the shaded game JAR, signed as
a standalone bundle library, and supplied to both launcher and child JVM; Apple
notarization inspects native code inside archives and rejects an otherwise valid
top-level signature when an unsigned .jnilib remains in a JAR.
The lane is release-strict: an ad-hoc or unnotarized artifact is a failure, not an upload with a misleading name. It accepts the same repository or organization secrets as the sister projects:
| Purpose | Secrets |
|---|---|
| Developer ID | MAC_CERTIFICATE_P12_BASE64, MAC_CERTIFICATE_PASSWORD, MAC_SIGNING_IDENTITY |
| Preferred notarization | APP_STORE_CONNECT_API_KEY_BASE64, APP_STORE_CONNECT_KEY_ID, APP_STORE_CONNECT_ISSUER_ID |
| Notarization fallback | APPLE_ID, APPLE_APP_PASSWORD, APPLE_TEAM_ID |
The Developer ID identity may instead already be installed on a self-hosted runner. Notarization still requires one complete credential set. A release always builds the notarized Apple-silicon DMG, signed Windows x64 MSI, and Linux x64 AppImage. The Linux lane runs on the private ARC fleet; the other native lanes can move back to repository-scoped home runners without changing their proof steps. Windows signs both the packaged launcher and the final MSI with Azure Trusted Signing before either is accepted.
Only after all three lanes pass does the publish job create or update
v<version>, upload every installer and checksum, and copy the exact artifacts
to the retained release volume. Immutable versioned files land first, stable
download names follow, and downloads/latest.json moves last. The ChonkCraft
page reads that no-cache catalog, so a release updates the website without a
frontend redeploy and can never advertise a partial platform set. The workflow
never downloads or packages Warcraft II data.
A push to master that changes a game input first requires both test jobs
on that exact revision, then builds the self-contained desktop JAR and publishes it through updates.chonkbase.net. The workflow signs a
single catalog envelope, proves the production launcher installs it locally,
uploads the content-addressed JAR to the retained Linode volume, replaces the
catalog last, and finally repeats the install through the public HTTPS endpoint.
Its Kubernetes credential is restricted to listing the update-server pod and
executing the upload there; it cannot read Secrets or mutate workloads. Full
protocol, rollback, and operating instructions are in
game-updates.md.
Read which step failed first. The order matters:
| Step | What a failure means |
|---|---|
| Assert the game data is mounted | the host mount is broken; nothing else is worth reading |
| Check the documentation | check-docs.py -- a dead link, missing document, or stale skip-count claim |
| Build | a real compilation failure |
| Run the test suite | continue-on-error, so this never fails the job by itself |
| Assert the run exercised what it should | the skip gate; see below |
| Fail if any test failed | a genuine test failure -- download the Surefire artifact |
A skip-gate failure is not a test failure. It says a different set of tests ran than expected. Read the per-module table it prints:
- A count went UP: either an input was not configured for that run, or a new
test skips without it. The first is a CI setup fault. The second is a decision
that belongs in
check-test-skips.py, written down with a reason. - A count went DOWN: usually good news -- more ran than expected. Record the new floor rather than leaving the gate loose.
Do not raise a number to make the build green. Raise it because the new skip is one the project accepts.
Getting the actual failing test names:
gh run view --log-failed --job=<job-id>
gh run download <run-id> -n surefire-reports-full # or -data-freeThen read the XML rather than the log:
python3 -c "
import glob, xml.etree.ElementTree as ET
for f in glob.glob('*/target/surefire-reports/TEST-*.xml'):
r = ET.parse(f).getroot()
for tc in r.iter('testcase'):
for bad in tc.findall('failure') + tc.findall('error'):
print(tc.get('classname'), tc.get('name'), (bad.get('message') or '')[:200])
"This has happened twice and both times the test was at fault rather than the port, so suspect the measurement first:
HudInkTestcounted pixels of one exact ARGB value.GameFontdraws with antialiasing on, so only fully covered pixels land on the ink exactly, and how many of those a 13-point glyph produces is a question about the platform's hinting. It now counts within a tolerance that cannot confuse the two inks it distinguishes.- The runner container had no fonts at all, so nothing rendered.
The general form: a test that measures a rendered pixel, a filesystem ordering, or anything the JDK delegates to the host is measuring the host until proven otherwise.
When the counts legitimately change, re-measure all three profiles rather than patching the one that failed:
scripts/run-tests.sh -Dwc2.install.dir=/nonexistent
scripts/ci/check-test-skips.py --profile data-free --write
scripts/run-tests.sh -Dwc2.install.dir="$WC2_INSTALL_DIR" \
-Dchonkcraft.pack="$CHONKCRAFT_ASSET_PACK" -Dopus.testvectors="$OPUS_TESTVECTORS"
scripts/ci/check-test-skips.py --profile full --writeThen update the totals quoted in prose -- README.md,
development-setup.md, scripts/check-setup.sh and the
workflow's own comments. scripts/check-docs.py fails the build if a quoted
total disagrees with the gate's baselines, so this is checked rather than
remembered.