Skip to main content

Continuous integration

GitHub Actions builds the solution and runs the tests for every pull request and every push to main, and publishes this site. This page lists the workflows in .github/workflows, when each one runs and what it checks, how to read a failed run, how Renovate proposes dependency updates, and the repository settings the workflows rely on.

Workflows​

WorkflowFileRunsChecks
CIci.ymlEvery pull request and push to main, and on demandBuilds with warnings as errors, then runs every test except the slow ones on Linux.
Checkschecks.ymlEvery pull request and push to main, and on demandChecks formatting against .editorconfig, generated files against their sources, and on pull requests that every commit follows Conventional Commits. See Formatting, generated files and commit messages.
Slow testsslow-tests.ymlEvery push to main, nightly, on demand, and pull requests that are labelled run-slow or change the build, the player, the asset pipeline or pluginsThe tests marked Category=Slow.
Windows and macOScross-platform.ymlMondays, on demand, and pull requests labelled run-cross-platformBuilds and runs every test except the slow ones on Windows and macOS.
Nightlynightly.ymlNightly, on demand, and pull requests that change these checksDrives the editor under Xvfb, and runs an exported game in a container without fontconfig. See Nightly checks.
Releaserelease.ymlVersion tags such as v0.2.0, on demand, and pull requests that change itPackages the editor for every platform and publishes a GitHub release. See Releases.
Benchmarksbenchmarks.ymlNightly, on demand, and pull requests that change itRuns the micro-benchmarks and compares them with their recent history. See Benchmarks.
Screenshotsscreenshots.ymlOn demand, and pull requests that change itRenders the documentation screenshots and opens a pull request with the changed ones. See Screenshot refresh.
Labelerlabeler.ymlEvery pull requestAdds area labels by the paths a pull request changes. See Labels.
Labelslabels.ymlChanges to it on main, and on demandCreates and updates the repository's labels.
Renovate configrenovate-config.ymlPull requests and pushes to main that change the Renovate configurationChecks .github/renovate.json5 with Renovate's own validator. See Dependency updates.
CodeQLcodeql.ymlEvery pull request and push to main, weekly, and on demand, once turned onScans the C# code, this site's TypeScript and the workflows for security problems.
Dependency reviewdependency-review.ymlEvery pull request, once turned onFails a pull request that adds or updates a package with a known vulnerability.
Websitewebsite.ymlPull requests and pushes to main that change website/, and on demandType-checks and builds this site. On main, publishes it to Fly.io. See This site.

To run a workflow on demand, open it on the repository's Actions tab, choose Run workflow and pick a branch.

The SDK version​

global.json pins the .NET SDK, so CI and your machine build with the same compiler:

global.json
{
"sdk": {
"version": "10.0.400",
"rollForward": "latestFeature"
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}

Any SDK from 10.0.400 on works, and CI installs the newest one this allows. The floor comes from the script compiler: its analyzers build against Microsoft.CodeAnalysis.CSharp 5.9 from Directory.Packages.props, and the C# compiler only loads analyzers built for its own version or older. SDK 10.0.400 is the first with compiler 5.9. An older SDK skips the analyzers with warning CS9057 when it builds a game's script project, and the test that builds one fails. Raise the SDK whenever you raise Microsoft.CodeAnalysis.CSharp. Renovate proposes the two in one pull request.

Build and test​

The Build and test job in ci.yml runs on Ubuntu 24.04:

  1. Installs the SDK that global.json selects and restores NuGet packages from a cache that pushes to main refresh.
  2. Installs mesa-vulkan-drivers. Its lavapipe driver runs Vulkan on the CPU, so the rendering and lighting tests check the Vulkan renderer as well as Skia. Without a Vulkan driver they check Skia only.
  3. Builds Talesmith.slnx with -warnaserror. The solution builds without warnings, and NuGet's audit warnings about packages with known vulnerabilities count too. The build hard-links the files that projects copy into their output folders instead of copying them, with CreateHardLinksForCopyLocalIfPossible and the two related properties. Every test project carries the native libraries of every platform, about 550 MB, so copies would take about 12 GB, more than a hosted runner has free.
  4. Runs the tests without the slow ones, writes a TRX report per test project and summarizes them.

The same build and tests run locally with:

dotnet build Talesmith.slnx -warnaserror
dotnet test --solution Talesmith.slnx --no-build -- --filter-not-trait "Category=Slow"

Read a failed run​

  • Summary. The run's summary page has a table with the passed, failed and skipped tests of each project, and the message and stack trace of each failed test.
  • Annotations. Each failed test is also marked on the line where it failed. When the pull request changes that file, the mark shows in its Files changed tab too.
  • Artifacts. test-results holds the TRX reports. When a test fails, render-captures holds the PNG files that the rendering and lighting tests write to captures/lighting next to their binaries.

.github/scripts/TestSummary.cs writes the summary and the annotations. It is a file-based C# program, so dotnet run .github/scripts/TestSummary.cs -- TestResults runs it locally on a results folder.

Hangs​

Every test project references Microsoft.Testing.Extensions.HangDump through Directory.Build.props, and CI passes --hangdump --hangdump-timeout 10m --hangdump-type Mini. When no test of a project makes progress for 10 minutes, the test platform saves a dump of the test process and a log that names the tests still running, stops the process and fails the run. Both files are in test-results. Open the dump with dotnet-dump analyze and run clrstack -all to see where each thread waits. The job stops after 45 minutes in any case.

Slow tests​

The tests marked [Trait("Category", "Slow")] publish players, export Lantern Grove and build a plugin with dotnet build, which takes a few minutes. They run on every push to main, every night, and, for a pull request, when it changes one of these:

  • src/Talesmith.Build, src/Talesmith.Player, src/Talesmith.Assets, src/Talesmith.Plugins or src/Talesmith.Editor/Plugins
  • Lantern Grove (samples/LanternGrove and samples/Talesmith.Samples.LanternGrove)
  • tests/Talesmith.Build.Tests, tests/Talesmith.EndToEnd.Tests or tests/Talesmith.Editor.Tests/Plugins
  • Directory.Build.props, Directory.Packages.props or global.json

For any other pull request, add the run-slow label to run them. A small Decide job checks the label and the changed files first and skips the slow tests otherwise. The test run filters the whole solution with --filter-trait "Category=Slow" and passes --ignore-exit-code 8, because projects without slow tests run no tests, which the test platform reports with exit code 8.

Windows and macOS​

cross-platform.yml builds and tests on windows-2025 and macos-15. GitHub bills their minutes at two and ten times the Linux rate, so this workflow runs every Monday and when you ask: run it from the Actions tab, or add the run-cross-platform label to a pull request. With the label, every push to the pull request runs it again until you remove the label. These runners have no Vulkan driver, so the Vulkan tests skip themselves. They also have only three or four cores, so the workflow runs the test projects one at a time with --max-parallel-test-modules 1: run in parallel, the editor tests starve the tests that count frames against the clock.

Add the label when a change touches file paths, the case of file names, line endings, timing or the platforms a build targets. Those are where Windows and macOS behave differently from Linux.

Nightly checks​

nightly.yml runs every night, on demand, and on pull requests that change it, the smoke script or the editor's command line:

  • Editor smoke test. tools/smoke/editor-smoke.sh starts the real editor under Xvfb, creates a platformer project, selects and drags an entity, undoes, plays and stops, searches the command palette and quits, with a screenshot after each step. The editor-smoke artifact holds the screenshots and the editor's log.
  • Exported game without fontconfig. Exports Lantern Grove for Linux with talesmith --export and runs it for 120 frames in an ubuntu:24.04 container, which has no fontconfig, FreeType, X11 or Vulkan. Only libicu74 is added, because .NET needs ICU. This keeps the promise that exported games start without fontconfig. The exported-game artifact holds the game's log and its benchmark report.

The slow tests also run every night and the Windows and macOS tests every Monday, so problems from outside the repository show up within a day or a week even when nothing is merged: a new SDK, a new runner image, or a new advisory for a NuGet package, which fails the build. GitHub emails the failures of scheduled runs to whoever last changed the schedule.

Releases​

release.yml runs when a version tag is pushed:

git tag v0.2.0
git push origin v0.2.0
  1. Four jobs, one per target, publish the editor for win-x64, linux-x64, osx-arm64 and osx-x64. .NET publishes for every platform from Linux, so a release uses no Windows or macOS minutes.
  2. Each job adds the target's development and release players to the editor's players folder with talesmith --publish-player, so the installed editor exports games for its own platform without the .NET SDK or the engine's sources.
  3. Each job also exports Lantern Grove with the Distribution profile, as a game to download.
  4. The last job writes SHA256SUMS.txt and publishes a GitHub release with notes generated from the merged pull requests. A tag with a pre-release part, such as v0.3.0-rc.1, makes a pre-release.

The version comes from the tag. The workflow sets the TalesmithVersion environment variable, which Directory.Build.props uses as every project's version and which reaches the players' builds too. Without it, builds are version 0.1.0.

ArchiveContents
talesmith-<version>-<target>.zip (Windows), .tar.gz (Linux, macOS)The editor with .NET inside it, and the target's players in players/. Run talesmith, or talesmith.exe on Windows.
lantern-grove-<version>-<target>.zip, .tar.gzThe Lantern Grove sample as an exported game.

The executables are not signed. Windows SmartScreen asks before the first start, and on macOS run xattr -dr com.apple.quarantine on the unpacked folder before starting it. To export for another platform from an installed editor, copy that platform's folder from the players folder of its archive into your editor's players folder.

Pull requests that change release.yml run the four packaging jobs without publishing, and so does Run workflow on the Actions tab. Their archives are the run's artifacts.

Benchmarks​

benchmarks.yml runs the micro-benchmarks every night with BenchmarkDotNet's short job, then .github/scripts/BenchmarkHistory.cs compares each mean with the median of the same benchmark's last five runs on main. The run's summary page shows the table, and a benchmark more than 15% slower gets a warning. Shared runners vary from run to run, so a single warning is a hint to look, and a slowdown that stays for several nights is a regression; the workflow never fails because of one.

The history of the last 90 runs lives in the Actions cache: each run on main saves it, and the next run restores the newest copy. Runs on other branches compare without adding to it. The benchmarks artifact holds the BenchmarkDotNet reports and the history file.

Screenshot refresh​

screenshots.yml renders the documentation screenshots, as npm run screenshots does, and opens a pull request with the images that changed. Start it with Run workflow on the Actions tab; the optional filter renders only the screenshots whose name contains it, such as guide/. Rendering every screenshot takes about half an hour, and the job stops after an hour. The run's summary lists the images that changed. Review them before merging: shots that depend on time, such as the project hub's dates or the hot-reload toast, can differ on every run. GitHub starts no workflows for a pull request that a workflow opened with its own token, so close and reopen it to run CI.

Opening the pull request needs a repository setting. Without it, the run fails with a link for opening the pull request from the pushed branch yourself. Pull requests that change screenshots.yml render every screenshot and list the changes without opening a pull request.

Formatting, generated files and commit messages​

checks.yml runs three jobs next to Build and test:

  • Formatting runs dotnet format whitespace and dotnet format style with --verify-no-changes. They check the rules in .editorconfig: indentation, line breaks and spacing, System directives first, and file-scoped namespaces. Each finding names its file and line. The same commands without --verify-no-changes fix them:

    dotnet format whitespace Talesmith.slnx
    dotnet format style Talesmith.slnx
  • Generated files recompiles the Vulkan shaders to SPIR-V with tools/Talesmith.ShaderCompiler, exports the editor's shortcuts for this site as npm run shortcuts does, and fails if either result differs from what is committed. A change to a shader or to an editor command carries the regenerated .spv file or website/src/data/shortcuts.json.

  • Commit messages runs on pull requests and fails when a commit's subject does not start with a type and an optional scope, such as fix(editor):, as the commit rules describe. It names each such commit. Merge commits are left out.

This site​

website.yml checks this site on pull requests that change website/ and publishes it to Fly.io from main:

  1. Installs the packages with npm ci and type-checks the TypeScript.
  2. Builds the site. The build runs the prose lint first and fails on broken links and anchors.
  3. On main, runs flyctl deploy in website/. Fly.io builds the image from website/Dockerfile on its own builder and replaces the app's machines with it.

The image builds the site and serves it with Caddy on port 8080, behind Fly.io's proxy, which handles HTTPS. Pages are served without .html, the way the site links to them, and an address that ends in a slash redirects to the one without. Files under /assets/ have a hash of their content in their names, so browsers keep them for a year, while pages and images are checked for changes on every visit, so a new deployment shows at once. website/fly.toml keeps one machine running in Amsterdam, lets Fly.io start a second one under load, and checks / every 30 seconds.

The site builds for https://talesmith.dev/; SITE_URL and BASE_URL build it for another address. Until the FLY_API_TOKEN secret is set, the workflow builds the site, skips the deployment and leaves a warning on the run.

To publish the site:

  1. Install flyctl and sign in with fly auth login.
  2. In website/, create the app with fly apps create talesmith-docs. App names are unique across Fly.io, so if the name is taken, pick another and change app in fly.toml to match.
  3. Still in website/, create a deploy token for the app with fly tokens create deploy, and add it as the secret FLY_API_TOKEN under Settings › Secrets and variables › Actions.
  4. Run the Website workflow from the Actions tab, or push a change to website/. The site appears at https://talesmith-docs.fly.dev.
  5. To serve it from talesmith.dev, run fly certs add talesmith.dev, add the DNS records it prints, and wait until fly certs check talesmith.dev reports the certificate as issued.

Dependency updates​

Renovate opens a pull request for each new version of the NuGet packages in Directory.Packages.props, the SDK in global.json, this site's npm packages and the actions in the workflows. Its configuration is .github/renovate.json5, and renovate-config.yml checks it with Renovate's own validator whenever it changes.

  • Nothing merges on its own. Every update waits for CI and a review.
  • Commit messages follow the commit rules: chore(deps): for packages, the SDK and lock files, and ci(deps): for the actions in the workflows. Pull request titles match.
  • Schedule. Renovate opens pull requests on Mondays before 9:00, Amsterdam time: at most 10 open at once and 5 new ones an hour. Fixes for known vulnerabilities open at any time and carry the security label. This site's package-lock.json is refreshed on the first day of each month.
  • Dependency Dashboard. An issue lists every pending update, including those waiting for their schedule or an approval. Tick an update's box to get its pull request now.
  • New npm releases wait three days, the time in which a broken or compromised release is usually pulled.
  • Actions are pinned to a commit, with the version in a comment next to it. Renovate's first pull request pins them, and later updates change both.
  • The SDK in global.json only moves to a new feature band, such as 10.0.500. CI installs the newest SDK anyway, and each raise makes every contributor update theirs.

Groups​

Packages that must move together arrive in one pull request:

GroupPackagesWhy
AvaloniaAvalonia, Avalonia.*They share AvaloniaVersion.
SkiaSharpSkiaSharp, SkiaSharp.NativeAssets.*They share SkiaSharpVersion, because the native libraries must match the managed version.
Microsoft.ExtensionsMicrosoft.Extensions.*They share MicrosoftExtensionsVersion.
Roslyn and the .NET SDKMicrosoft.CodeAnalysis.CSharp and the SDK in global.jsonThe SDK's compiler must be at least as new as Roslyn. See The SDK version.
xUnit and the test platformxunit.v3, xunit.runner.visualstudio, Microsoft.NET.Test.Sdk, Microsoft.Testing.*They run the tests together.
Docusaurus@docusaurus/*They must share one version.
Reactreact, react-dom, @types/reactThe types follow the version they describe.

Updates that wait for approval​

A new major version of these opens no pull request until you tick it on the Dependency Dashboard:

  • The .NET SDK, Microsoft.CodeAnalysis.CSharp and Microsoft.Extensions.*, which belong to the next .NET release.
  • Avalonia and SkiaSharp. Avalonia builds on one SkiaSharp major, so the two move together.

Two kinds of pull request carry a note about extra work. A Roslyn update that the SDK in global.json cannot load yet waits for an SDK update: until then TheGeneratedProjectBuildsWithDotnetBuild fails with CS9057. A Vortice.ShaderCompiler update asks you to recompile the shaders and commit any .spv file that changes.

Security​

CheckRunsWhat it does
NuGet auditEvery buildWhile restoring, NuGet checks every package, transitive ones included, against GitHub's advisory database. CI builds with -warnaserror, so a package with a known vulnerability of any severity fails Build and test. On your machine it is a warning, NU1901 to NU1904.
CodeQLcodeql.yml: pull requests, pushes to main, MondaysAnalyzes the C# code, this site's TypeScript and the workflows without building, and lists findings under Security › Code scanning.
Dependency reviewdependency-review.yml: pull requestsFails a pull request that adds or updates a package with a known vulnerability of moderate severity or higher.
Secret scanningA repository settingFinds credentials in the repository. Push protection rejects a push that contains one.

Renovate also opens a pull request for a vulnerable package as soon as a fix is out; see Dependency updates.

In a private repository, CodeQL and dependency review need GitHub Code Security, and secret scanning needs GitHub Secret Protection, both paid. The two workflows skip their jobs until the repository variable CODE_SECURITY is true, so they do not fail every pull request before then. To turn them on:

  1. Under Settings › Advanced Security, turn on GitHub Code Security, and Secret Protection with push protection if you want secret scanning. An organization owner may have to make the licenses available first.
  2. Under Settings › Secrets and variables › Actions › Variables, add the variable CODE_SECURITY with the value true.
  3. Leave CodeQL's default setup off: GitHub rejects the results of codeql.yml while it is on.

Labels​

labels.yml creates the repository's labels with their colors and descriptions, and updates them when the list in it changes. labeler.yml gives each pull request area labels, such as editor, rendering, runtime or docs, for the paths it changes, as .github/labeler.yml maps them, and removes those that stop applying. A test project counts toward the area it tests. Two labels start workflows: run-slow runs the slow tests and run-cross-platform the Windows and macOS tests. Renovate adds dependencies and security to its pull requests.

Repository settings​

The workflows need these settings, which a repository admin makes once:

SettingWhereValue
Required checksSettings › Rules › Rulesets, a branch ruleset for mainRequire pull requests, and the Build and test, Formatting and Generated files status checks. Leave the other workflows out: they run only when their paths change, and a required check that never runs blocks the pull request.
MergingSettings › General › Pull RequestsAllow merge commits, the way the history is kept, and turn on Automatically delete head branches.
LabelsActions › Labels › Run workflowRun it once after merging to create every label; see Labels.
Code owners.github/CODEOWNERSAsks the maintainer to review every pull request. Leave Require review from Code Owners off while there is one maintainer, since GitHub does not let authors approve their own pull requests.
Fly.ioSettings › Secrets and variables › ActionsThe secret FLY_API_TOKEN, a deploy token for the site's Fly.io app. See This site.
Pull requests from workflowsSettings › Actions › General › Workflow permissionsTick Allow GitHub Actions to create and approve pull requests, so the Screenshots workflow can open its pull request. In an organization, an owner allows it first under the organization's Settings › Actions › General.
Code SecuritySettings › Advanced Security, then Settings › Secrets and variables › ActionsGitHub Code Security on, and the variable CODE_SECURITY set to true. See Security.
Secret scanningSettings › Advanced SecuritySecret Protection with push protection.
Renovategithub.com/apps/renovateInstall the app for this repository. It finds .github/renovate.json5, so it skips its onboarding pull request. Keep Issues turned on for the Dependency Dashboard.