Skip to content
learn

Journal

  • Sample code: the same slug function in .NET (xUnit v3) and Java (JUnit 6)
  • Lesson 1: first workflow, reading runs with gh
  • Lesson 2: matrix build on three OSes, setup-dotnet, setup-java, Maven cache
  • Lesson 3: triggers, filters, inputs, concurrency
  • Lesson 4: expressions, contexts, outputs, conditions
  • Lesson 5: caches and artifacts (NuGet cache with packages.lock.json, actions/cache, artifacts between jobs)
  • Lesson 6: reusable workflows and composite actions
  • Lesson 7: security — permissions, secrets, pinning, OIDC
  • Lesson 8: deploying to GitHub Pages
  • Lesson 9: debugging runs
  • Lesson 10: writing your own action

2026-09-14 — Local first: two xUnit v3 traps

Section titled “2026-09-14 — Local first: two xUnit v3 traps”

Before writing any workflow, dotnet test on the sample project (SDK 10.0.112 locally, xunit.v3 4.0.1) failed twice:

  1. Testing with VSTest target is no longer supported by Microsoft.Testing.Platform on .NET 10 SDK and later. Fix: "test": { "runner": "Microsoft.Testing.Platform" } in global.json.
  2. CS0246: The type or namespace name 'Theory' could not be found. With ImplicitUsings enabled, this package version didn’t add using Xunit. Fix: the using in the test file.

The Java side (mvn -B verify, JUnit 6.1.3, Maven 3.9.16) passed first time.

2026-09-14 — First push: four workflows, one invalid

Section titled “2026-09-14 — First push: four workflows, one invalid”

Pushing the four lesson workflows (commit 61e7e42) started them all. GHA 01, GHA 02 (6 jobs) and GHA 04 passed. GHA 03 appeared under its file name, failed in 0 s, with no job: You have an error in your yaml syntax on line 41, a run: echo "cron: …" line (an unquoted value containing : ). The next push, unrelated to that file, failed it again, despite its paths filter.

A second commit the same afternoon repeated the mistake in gha-04-exercises.yml (run: echo "next: $BUILD_LABEL"); this time python -c "import yaml; yaml.safe_load(...)" caught it before the push. Lesson: validate YAML locally, every time.

2026-09-14 — Two deployments of this site collided

Section titled “2026-09-14 — Two deployments of this site collided”

Pushes 61e7e42 and cb69dca, 14 s apart, each started Deploy to GitHub Pages. The second deploy job failed:

##[error]HttpError: Deployment request failed for cb69dca950b667b876d8d32cfe778a24b6320c59 due to in progress deployment. Please cancel 61e7e4235b7bc138f2f35cd7f61195bdaed4b019 first or wait for it to complete.

The site stayed on the previous version until the next push. Fix in deploy.yml (commit 5c9896f): concurrency: { group: pages, cancel-in-progress: false }. Several sessions push to this repository, so collisions were bound to happen again.

  • Maven cache, gha-02 first run → second run: Ubuntu 16 s → 10 s, Windows 43 s → 28 s, macOS 20 s → 9 s. .NET jobs unchanged (21/60/20 s → 22/57/22 s): no NuGet cache yet.
  • global.json 10.0.100 + latestFeature → setup-dotnet installed SDK 10.0.401.
  • Temurin 25.0.4 was preinstalled on ubuntu-latest (Resolved Java 25.0.4+1 from tool-cache).
  • macos-latest is arm64 (setup-java-macOS-arm64-maven-… cache key).
  • Default run: shell: /usr/bin/bash -e {0} (Ubuntu), /bin/bash -e {0} (macOS), pwsh -command ". '{0}'" (Windows). Explicit shell: bash on Windows: bash.EXE --noprofile --norc -e -o pipefail {0}, bash 5.3.15.
  • GITHUB_TOKEN default permissions on this repository: Contents: read, Metadata: read, Packages: read. The token was 377 characters long.
  • Concurrency with cancel-in-progress: true: push run cancelled by a manual run 41 s later, itself cancelled 10 s later by another one before its job started.

2026-09-14 — Lesson 5: a cache that saves nothing, an artifact conflict that doesn’t happen

Section titled “2026-09-14 — Lesson 5: a cache that saves nothing, an artifact conflict that doesn’t happen”
  • setup-dotnet cache: true without any packages.lock.json: Dependencies lock file is not found in /home/runner/work/learn/learn. Added RestorePackagesWithLockFile in Directory.Build.props, committed the two lock files, dotnet restore --locked-mode in CI.
  • NuGet cache, miss → hit: restore step 3 s → 1 s (Ubuntu, macOS), 5 s → 4 s (Windows), for a 54 MB cache per OS. The download of the cache costs about what it saves on a project with three test packages. Kept for the lesson and for the lock files.
  • actions/cache output cache-hit: empty on a miss, false on a restore-keys match, true on an exact hit.
  • Two matrix jobs uploading an artifact named os: the README announces conflict errors; three runs out of three, both uploads succeeded and the run had two artifacts named os. download-artifact and gh run download --name os both returned the Ubuntu one, without warning.
  • Artifact expiry: 7 days with retention-days: 7, otherwise 90 days (gh api repos/spareilleux/learn/actions/permissions/artifact-and-log-retention → {"days":90,"maximum_allowed_days":90}).

2026-09-14 — Lesson 6: reuse, and a branch for an invalid workflow

Section titled “2026-09-14 — Lesson 6: reuse, and a branch for an invalid workflow”
  • The composite action dotnet-restore restored the NuGet cache saved by gha-05: same key and path, another workflow. Caches belong to the repository and branch.
  • In the called workflow: github.workflow is the caller’s name, job.workflow_ref points to gha-06-dotnet-test.yml, the caller’s workflow-level env is empty.
  • Matrix of two reusable workflow calls: the output tested-on was Windows, the job that finished last.
  • Composite run: without shell:: Required property is missing: shell, only when a job runs the action.
  • Undeclared input: startup_failure, no job, and gh run view doesn’t show the reason (the run page does). Tested from a temporary branch gha-06-invalid so that main never carried an invalid workflow. Deleting the remote branch was blocked by a local safety hook: it’s still there, to delete by hand.

2026-09-14 — Lesson 7: permissions that don’t restrict, and an injection that runs

Section titled “2026-09-14 — Lesson 7: permissions that don’t restrict, and an injection that runs”
  • gh run list from a job with contents: read only (no actions: read) worked: public data of a public repository. Replaced by a label creation with an empty name: 403 Resource not accessible by integration without issues: write, 422 Validation Failed with it — nothing created either way.
  • Job-level permissions: actions: read → the token lost Contents, and actions/checkout still worked on this public repository.
  • workflow_dispatch input $(whoami) pasted with ${{ }} into run: printed runner; through env: it printed $(whoami).
  • ::add-mask:: hides the value only from the lines printed after it.
  • OIDC token for audience learn-course: lifetime 300 s, sub = repo:spareilleux@6644695/learn@1368550922:ref:refs/heads/main, the immutable format of repositories created after 2026-07-15.

2026-09-14 — Lesson 8: this site’s deployment, taken apart

Section titled “2026-09-14 — Lesson 8: this site’s deployment, taken apart”
  • A deploy run: build 30 s (310 pages built in 7.47 s, github-pages artifact of 9,108,634 bytes kept 1 day), deploy 8 s. Deployment statuses: waiting → queued → in_progress → success in 13 s.
  • The concurrency queue worked: a push 41 s after another one waited 14 s, and its build job was created at 13:32:18, the second the previous deploy job finished.
  • gh workflow run deploy.yml --ref gha-06-invalid: the build ran, the deploy job was rejected (Branch "gha-06-invalid" is not allowed to deploy to github-pages due to environment protection rules), the live site didn’t change. Checked beforehand that no deployment of main was pending, since the branch run shares the pages concurrency group.
  • https://spareilleux.github.io/method/ → 404: a root-absolute link leaves the project site.

2026-09-14 — Lesson 9: timeouts that take longer, and a failure that counts as success

Section titled “2026-09-14 — Lesson 9: timeouts that take longer, and a failure that counts as success”
  • gh run view --log-failed: 15 lines out of 169; the job cancelled by its timeout isn’t included.
  • gh run rerun --debug: ##[debug] condition evaluations, RUNNER_DEBUG=1, the noisy job from 70 to 190 lines, and a runner-diagnostic-logs folder (Runner and Worker logs) in the archive.
  • gh run rerun --failed re-ran the failed and the cancelled jobs, and kept the successful one from attempt 1.
  • timeout-minutes: 1 on a job: cancelled after 87 s, three times, with sleep 90 or sleep 300. On a step: failed after 72 s, The action 'Run date -u +%T' has timed out after 1 minutes.
  • A run whose jobs are successful, failed with continue-on-error, or cancelled by a timeout ends cancelled. With only the continue-on-error failure, it ends success, and needs.<job>.result is success.

2026-09-14 — Lesson 10: three failures before a working action

Section titled “2026-09-14 — Lesson 10: three failures before a working action”
  • require() in the JavaScript action: require is not defined in ES module scope, locally, because the repository’s package.json (the Astro site’s) says "type": "module". Switched to import; a CommonJS copy failed the same way on the runner.
  • Container action, first push, committed without the executable bit on purpose: exec: "/entrypoint.sh": permission denied. Files created on Windows are committed as 100644 (core.filemode is false); fixed with git update-index --chmod=+x.
  • Container action on windows-latest: Container action is only supported on Linux.
  • required: true isn’t enforced by the runner; an unknown input is only a warning (Unexpected input(s) 'txt', valid inputs are ['text', 'max-length']).
  • Step durations: JavaScript action under 1 s on the three OSes, container action 5 s (image build included).
  • Does the schedule trigger of gha-03 (Mondays 06:17 UTC) run on time? To verify on 2026-09-21.
  • Which artifact does download-artifact pick when two share a name, and is it stable? To verify.
  • Does actions/checkout fail on a private repository when the job token has no contents permission? To verify.
  • Why is a 1-minute job timeout enforced after 87 s? To verify with longer limits.
  • What exactly does a pull request page show for a required check skipped by paths? To verify.