Skip to content

Build, test and package on Windows, Linux and macOS in CI - #3

Merged
siegfriedpammer merged 2 commits into
mainfrom
ci-build-workflow
Aug 31, 2026
Merged

Build, test and package on Windows, Linux and macOS in CI#3
siegfriedpammer merged 2 commits into
mainfrom
ci-build-workflow

Conversation

@christophwille

Copy link
Copy Markdown
Member

Why

Nothing built the solution outside a developer machine, and nothing produced a runnable artifact. One workflow now builds Stampeded.slnx, runs the tests and publishes a Release build on each OS's native runner: a zip for win-x64 and linux-x64, an unsigned, unnotarized dmg for osx-arm64.

What

  • .github/workflows/build.yml: matrix over windows-latest / ubuntu-latest / macos-latest (arm64). Build, test, framework-dependent RID-specific publish, then package. Info-ZIP on Linux so the execute bit survives; hdiutil for the dmg.
  • src/Stampeded/Stampeded.csproj: BuildMacAppBundle target assembles Stampeded.app on any osx-* publish, so dotnet publish -r osx-arm64 reproduces what CI ships.
  • src/Stampeded/Assets/macos/Info.plist: minimal bundle manifest (no icon - there is no .icns yet).

Framework-dependent on purpose: the app needs an installed .NET SDK at runtime anyway (Roslyn MSBuild workspace, MSBuildLocator, dotnet build/test).

Modeled on icsharpcode/ILSpy#3768, minus deb/rpm, Homebrew, signing and pwsh packaging scripts, which wait for a release process.

Verified locally

dotnet publish -r osx-arm64 (cross-published from Windows) writes publish/osx-arm64/Stampeded.app/Contents/{Info.plist,MacOS/Stampeded} with all 116 publish files and is idempotent; -r win-x64 yields Stampeded.exe. CI on this PR is the first run of the workflow itself.

🤖 Generated with Claude Code

https://claude.ai/code/session_011EuGx5KH6zfiZQYHTcygyh

Nothing built the solution outside a developer machine, and nothing produced
a runnable artifact. One workflow now does both on each OS's native runner:
a zip for win-x64 and linux-x64, an unsigned, unnotarized dmg for osx-arm64.

The publish is framework-dependent on purpose. The app needs an installed
.NET SDK at runtime regardless - Roslyn's MSBuild workspace, MSBuildLocator,
dotnet build and test - so a bundled runtime would only grow the artifact.

The .app is assembled by an MSBuild target rather than in the workflow, so a
plain dotnet publish -r osx-arm64 reproduces what CI ships. deb/rpm, a
Homebrew cask, signing and versioned artifact names, which ILSpy's pipeline
carries, are left out until there is a release process to need them.

Assisted-by: Claude:claude-fable-5:Claude Code
Three things the suite assumed about its host turned out to be Linux-only.

git writes loose objects read-only, and on Windows Directory.Delete refuses
a read-only file, so every fixture's TearDown threw past the IOException it
caught. One helper clears the flag first and treats cleanup as best effort;
a leaked temp directory is not a verdict on what the test asserted.

git prints worktree paths in its own form - forward slashes on Windows, the
symlink-resolved /private/var on macOS - and the service passes that form
on unchanged. The three assertions that compared it with the path the test
built now ask git how it spells that path instead of assuming it matches.

A raw string literal carries the line endings of the checkout, which with
autocrlf is CRLF on Windows; the diff builder speaks LF. The literals are
normalised once instead of the repository dictating line endings for
everyone.

Assisted-by: Claude:claude-fable-5:Claude Code
@christophwille

Copy link
Copy Markdown
Member Author

What the first CI run revealed, and how it was fixed (96548e4)

The workflow's first run was green on Linux and red on Windows (37 failures) and macOS (3 failures). None of it was caused by the workflow; the suite had only ever run on Linux and carried three host assumptions. Fixed in the tests, not the product.

1. Windows: TearDown threw on git's read-only objects (33 tests)

git writes loose objects read-only, and on Windows Directory.Delete(dir, recursive: true) refuses a read-only file with UnauthorizedAccessException, which is not an IOException, so it sailed past the catch (IOException) every fixture had. The tests themselves passed; their cleanup failed them.

Fix: tests/Stampeded.Core.Tests/TempDirectory.cs clears the ReadOnly flag first (skipping reparse points, so a symlink a test planted is not followed out of the directory) and treats removal as best effort, since a leaked temp directory is not a verdict on what the test asserted. The 14 fixtures now call TempDirectory.Delete(dir); their try/catch blocks went with it.

2. Windows + macOS: worktree paths in git's spelling (3 tests)

DeleteBranchAsync(...).RemovedWorktree and RebaseBranchAsync(...).Checkout carry the path as git worktree list --porcelain prints it, and the service passes that on unchanged. That is C:/Users/... (forward slashes) on Windows and the symlink-resolved /private/var/... on macOS, where Path.GetTempPath() says /var/.... The three assertions compared it with the path the test had built and so failed on both OSes.

Fix: the tests ask git how it spells the directory (git rev-parse --show-toplevel, captured before the worktree is removed) and compare with that, in GitMergeStateTests.RemovesTheWorktreeThatHoldsTheBranch, RemovesAWorktreeWithSubmodulesThatGitRefusesToTouch and GitRebaseTests.RebasesABranchThatIsCheckedOutInAWorktree (including its RecoveryCommand string). Normalizing in the product was considered and rejected: forward slashes work as a working directory, nothing in the product compares these paths to user input, and Path.GetFullPath would not resolve the macOS symlink anyway.

3. Windows: a CRLF raw string literal (1 test)

SideBySideBuilderTests.ThreadRowCarriesNoBlobLine asserts that a thread row leaves the side text unchanged. With core.autocrlf=true the checkout is CRLF, so the raw string literal OldText contained \r\n while DiffDocumentBuilder emits \n. The other tests in the file split on \n and never noticed.

Fix: the two literals are static readonly and normalized once with .ReplaceLineEndings("\n"). A repository-wide .gitattributes dictating line endings would also have worked but is a policy decision beyond this PR.

Not fixed on purpose

BuildArtifactCleanupTests.DoesNotFollowASymlinkOutOfTheWorktree fails on a non-elevated Windows developer box (symlink creation needs a privilege or Developer Mode). It passes on windows-latest, which runs as admin, so it is local environment, not something the test should work around.

Second run: all three legs green; artifacts checked (exec bit preserved in the Linux zip, dmg mounts with Stampeded.app and the Applications link).

@christophwille
christophwille marked this pull request as ready for review August 22, 2026 09:18
@siegfriedpammer
siegfriedpammer merged commit 218068a into main Aug 31, 2026
3 checks passed
@christophwille
christophwille deleted the ci-build-workflow branch August 31, 2026 20:05
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