2. Build and test .NET and Java
Sections
Reading settings
Natural voice recorded for this course, the same in every browser.
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
The project
Section titled “The project”code/github-actions holds the same small function twice — turn a title into a URL slug — with four tests on each side:
| .NET | Java | |
|---|---|---|
| Code | dotnet/Slugs/Slug.cs |
java/src/main/java/com/example/slugs/Slug.java |
| Tests | xUnit v3 [Theory] + [InlineData] |
JUnit 6 @ParameterizedTest + @CsvSource |
| Command | dotnet test |
mvn -B verify |
| SDK pinned by | global.json |
maven.compiler.release in pom.xml |
From dotnet/Slugs.Tests/SlugTests.cs, lines 8-14:
[Theory][InlineData("Hello, Wörld!", "hello-world")][InlineData(" GitHub Actions ", "github-actions")][InlineData("C# 14 & .NET 10", "c-14-net-10")][InlineData("", "")]public void From_builds_a_lowercase_ascii_slug(string text, string expected) => Assert.Equal(expected, Slug.From(text));Before any YAML: it must pass locally, with the same commands CI will run. That’s where the first two traps of this lesson appeared.
Trap 1: xUnit v3 and dotnet test on the .NET 10 SDK
Section titled “Trap 1: xUnit v3 and dotnet test on the .NET 10 SDK”With xunit.v3 4.0.1, the first dotnet test failed before running anything:
error : Testing with VSTest target is no longer supported by Microsoft.Testing.Platform on .NET 10 SDK and later. If you use dotnet test, you should opt-in to the new dotnet test experience. For more information, see https://aka.ms/dotnet-test-mtp-errorxUnit v3 runs on Microsoft.Testing.Platform, and the .NET 10 SDK wants you to opt in to the new dotnet test explicitly, in global.json (dotnet/global.json):
{ "sdk": { "version": "10.0.100", "rollForward": "latestFeature" }, "test": { "runner": "Microsoft.Testing.Platform" }}Trap 2: no implicit using Xunit
Section titled “Trap 2: no implicit using Xunit”Next error:
CS0246: The type or namespace name 'Theory' could not be found (are you missing a using directive or an assembly reference?)CS0246: The type or namespace name 'InlineData' could not be found (are you missing a using directive or an assembly reference?)With ImplicitUsings enabled, this 4.0.1 package didn’t add Xunit to the global usings: the test file needs using Xunit;. Then:
Test run summary: Passed! total: 4 failed: 0 succeeded: 4 skipped: 0The workflow
Section titled “The workflow”.github/workflows/gha-02-build.yml:
# GitHub Actions course, lesson 2: build and test .NET and Java on three OSesname: "GHA 02: build and test"
on: workflow_dispatch: push: branches: [main] paths: ['code/github-actions/**', '.github/workflows/gha-02-build.yml'] pull_request: paths: ['code/github-actions/**', '.github/workflows/gha-02-build.yml']
jobs: dotnet: name: dotnet (${{ matrix.os }}) strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} defaults: run: working-directory: code/github-actions/dotnet steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 with: global-json-file: code/github-actions/dotnet/global.json - run: dotnet --version - run: dotnet test
java: name: java (${{ matrix.os }}) strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} defaults: run: working-directory: code/github-actions/java steps: - uses: actions/checkout@v7 - uses: actions/setup-java@v6 with: distribution: temurin java-version: '25' cache: maven - run: java --version - run: mvn -B verify| Element | Effect |
|---|---|
strategy.matrix.os |
one job per value: 3 dotnet jobs and 3 java jobs, all in parallel |
fail-fast: false |
a failure on Windows doesn’t cancel the macOS job still running (the default, true, does) |
name: dotnet (${{ matrix.os }}) |
the job name shown in the run; without name, GitHub uses the job id and all matrix values, e.g. java (ubuntu-latest, 22) |
defaults.run.working-directory |
every run: starts in that folder (uses: steps aren’t affected) |
actions/setup-dotnet with global-json-file |
installs the SDK that global.json asks for |
actions/setup-java with cache: maven |
installs Temurin 25 and caches ~/.m2/repository, keyed on the hash of pom.xml |
mvn -B |
batch mode: no colors, no download progress bars in the log |
What the run shows
Section titled “What the run shows”All six jobs passed. Excerpts:
dotnet (ubuntu-latest) | dotnet-install: Installed version is 10.0.401dotnet (ubuntu-latest) | 10.0.401java (ubuntu-latest) | Resolved Java 25.0.4+1 from tool-cachejava (ubuntu-latest) | openjdk 25.0.4.1 2026-08-18 LTSjava (ubuntu-latest) | [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0java (ubuntu-latest) | [INFO] BUILD SUCCESSglobal.jsonsays10.0.100withrollForward: latestFeature:setup-dotnetinstalled the latest 10.0 feature band, 10.0.401. Pin"rollForward": "disable"if you need exactly one SDK.- Temurin 25 was already in the Ubuntu image (
from tool-cache): no download.
The shell isn’t the same everywhere. The same run: dotnet test step logged:
dotnet (ubuntu-latest) | shell: /usr/bin/bash -e {0}dotnet (macos-latest) | shell: /bin/bash -e {0}dotnet (windows-latest) | shell: C:\Program Files\PowerShell\7\pwsh.EXE -command ". '{0}'"On Windows, run: defaults to PowerShell 7. dotnet test and mvn -B verify behave the same in both, but a script with $(pwd), export or && chains won’t. Set defaults.run.shell: bash for the job (Git Bash is installed on Windows runners) when a script must be identical on the three OSes.
The cache, measured
Section titled “The cache, measured”The first run found no Maven cache and saved one; a second run (started with gh workflow run gha-02-build.yml) restored it (setup-java/src/cache.ts):
maven cache is not foundCache saved with the key: setup-java-Linux-x64-maven-4b450ab7781caaed20d3b55ef20c4500aaacb6aad3f54b145929988a7515a1ebCache restored from key: setup-java-Linux-x64-maven-4b450ab7781caaed20d3b55ef20c4500aaacb6aad3f54b145929988a7515a1ebCache hit occurred on the primary key setup-java-Linux-x64-maven-4b450ab7781caaed20d3b55ef20c4500aaacb6aad3f54b145929988a7515a1eb, not saving cache.| Job | 1st run (no cache) | 2nd run (cache hit) |
|---|---|---|
| java (ubuntu-latest) | 16 s | 10 s |
| java (windows-latest) | 43 s | 28 s |
| java (macos-latest) | 20 s | 9 s |
| dotnet (ubuntu-latest) | 21 s | 22 s |
| dotnet (windows-latest) | 60 s | 57 s |
| dotnet (macos-latest) | 20 s | 22 s |
The .NET jobs didn’t get faster: nothing caches the NuGet packages yet. setup-dotnet has a cache: true option based on packages.lock.json files, measured in lesson 5. The key contains the OS (Linux-x64, Windows-x64, macOS-arm64): each OS has its own cache. And macos-latest is an arm64 machine.
Key takeaways
Section titled “Key takeaways”- Make it pass locally first, with exactly the commands CI runs.
- A
matrixmultiplies a job;fail-fast: falselets every combination finish. setup-dotnetreadsglobal.json;setup-javainstalls a JDK and caches Maven or Gradle.run:is bash on Linux/macOS and PowerShell 7 on Windows unless you setshell.- Measure: a Maven cache hit saved 6 to 15 seconds per job here; Windows jobs are the slowest.
Exercises
Section titled “Exercises”- Add
22next to25so that the Java job also runs on Java 22, on the three OSes. How many Java jobs will run?
Solution
name: java ${{ matrix.java }} (${{ matrix.os }}) strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] java: ['25', '22'] steps: - uses: actions/setup-java@v6 with: distribution: temurin java-version: ${{ matrix.java }} cache: maven6 jobs (3 × 2). But the build fails on 22: pom.xml sets maven.compiler.release to 25. Checked with gha-02-exercises.yml:
[INFO] BUILD FAILURE[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.15.0:compile (default-compile) on project slugs: Fatal error compiling: error: release version 25 not supported -> [Help 1]A matrix tests combinations; the project must support them.
- You want the Windows job to run
dotnet testin bash, like the others. Where do you putshell: bash, so that it applies to everyrun:of the job?
Solution
defaults: run: shell: bash working-directory: code/github-actions/dotnetdefaults.run at the job level (or at the workflow level for all jobs). On Windows the log then shows:
shell: C:\Program Files\Git\bin\bash.EXE --noprofile --norc -e -o pipefail {0}bash 5.3.15(2)-release, options ehBNote -o pipefail: an explicit shell: bash also fails when a command in the middle of a pipe fails, which the implicit bash -e {0} of Linux runners doesn’t do.
- Without
using Xunit;, which error would CI show, and on how many jobs?
Solution
CS0246: The type or namespace name 'Theory' could not be found, on the three dotnet jobs. With fail-fast: false all three run to the end; with the default fail-fast: true, the first failure cancels the other dotnet jobs still in progress — the java jobs come from another job definition, with its own matrix, and aren’t affected.