How changes are made, verified, and shipped in this repo. GitHub is the single source of truth;
everything here is committed. AI assistants encode these as playbooks under .claude/skills/
(see AGENTS.md / CLAUDE.md).
Run the app you're changing with rask dev. It wraps dotnet watch run, so an edit to a component's
Render(), a scoped .css/.js, a [Route] template or a CQRS handler is applied to the running
process and every open session repaints in place — a "Hot reload applied" pill confirms it. Edits the
runtime can't apply (adding a type, changing a signature) restart the app instead, and the page reloads
itself. What hot-reloads has the full list, including what doesn't: a native app
running in-process on a device has no watch channel and must be restarted — though a Native + Server
shell hot-reloads like a browser, because that is what it is. WASM is covered — the host serves
the client's build output for the session, since a published bundle is trimmed and could never apply an
update.
When something breaks, it says what broke. A save that doesn't compile shows the compiler errors in the page — not "Reconnecting…" — and clears itself once the code builds; an exception from a handler or an async lifecycle hook shows over the running app, which stays mounted with its state intact. See when the build fails.
The framework side of that loop has its own gate, scripts/run-watch-e2e.sh — it scaffolds an app, runs
it under a real dotnet watch, edits a file, and asserts the change reached the open live session
without it being torn down. It's opt-in (RASK_WATCH_E2E=1); run it when you touch the hot-reload
coordinator, the scoped-asset registry, the generated registries, or rask dev.
Every change passes this gate before a PR (the rask-ship skill):
- Format + analyzers —
dotnet format Rask.slnxthen--verify-no-changes. Thepre-commitgate runs the verify for you, so this is a fast pre-check rather than the last line of defence. - Clean build, warnings-as-errors —
dotnet build Rask.slnx -c Release -warnaserror -p:EnforceCodeStyleInBuild=true. Enforced inDirectory.Build.props(TreatWarningsAsErrors,EnableNETAnalyzers,EnforceCodeStyleInBuild), so a plain build enforces it too. See code-analysis.md. - Tests — unit-test every feature/fix (
tests/Rask.*.Tests); add E2E only when a unit test can't reach the path. Everysamples/change gets an E2E journey update (tests/Rask.Examples.E2E.Tests). Inner loop — build once, then test with--no-buildso each run doesn't rebuild the whole solution (test execution itself is fast; the build dominates):Narrow further to one project (dotnet build Rask.slnx -c Release dotnet test Rask.slnx -c Release --no-build --filter "FullyQualifiedName!~Rask.Examples.E2E"
dotnet test tests/Rask.Core.Tests --no-build) or one class (--filter FullyQualifiedName~ATests) while iterating. The build runs in parallel by default — don't add-m:1(the former WASM copy-race workaround is fixed at the source inRask.Wasm.Hosting.targets). - Benchmarks — any render/live-runtime hot-path change runs
benchmarks/Rask.Benchmarksbefore/after and quotes theAllocateddelta in the PR. - Docs & examples — user-facing changes update a
samples/app, the relevantdocs/*.md,README.md,NUGET.md,llms.txt, and the templateAGENTS.md. Add aCHANGELOG.md[Unreleased]entry (Keep a Changelog). - Review — security, performance, and memory held together with UX; prefer standard .NET
APIs over hand-rolled code; refactor duplication you touch (the
rask-reviewskill). - PR — Conventional Commit
type(scope): subject(enforced by commitlint), structured body, no AI-attribution footers; delete the branch after squash-merge.
- Versions come from git tags via MinVer (
vX.Y.Z); assemblies carryAssemblyVersion,FileVersion, andInformationalVersionautomatically. - Stable release: promote
CHANGELOG.md[Unreleased]to a dated section, tagvX.Y.Z, push —release.ymlruns the unit gate, packs the NuGets, and publishes to nuget.org + a GitHub release (thecut-releaseskill). Run the local E2E gate (scripts/run-e2e-local.sh) before tagging. - Nightly: every push to
mainrunsnightly.yml— unit gate, then packs the MinVer prerelease versions and publishes them to nuget.org (prerelease) and GitHub Packages.
ci.yml— the deterministic benchmark byte-gates and a native compile gate (both native samples × android/ios). Tests do not run in CI — the unit/integration suite and the E2E suites run locally (see below).- Format + unit tests run locally, enforced before commit.
scripts/run-unit-local.shbuilds the solution once, runs the fulldotnet format Rask.slnx --verify-no-changes(whitespace + style + analyzers, one workspace load, ~36s), then every test except the browser E2E. The full pass earns its place: import ordering is enforced bydotnet formatalone — the warnings-as-errors build covers the analyzer rules but not the sorting of using directives, which is how a misorderedusingdrifted intoRask.Serverunnoticed (#584). Before formatting, the script buildssrc/*.Generatorsin Debug:dotnet formatevaluates the solution in the default configuration, so it resolves theOutputItemType="Analyzer"references tobin/Debug/, and without those DLLs no source generator runs —Routes.*is never emitted and the routing tests fail to bind with CS1503. That is the real cause of the "spurious CS1503" that kept this gate on the whitespace pass alone until #584. The.githooks/pre-commithook runs it whenever a commit stages code (enable hooks withgit config core.hooksPath .githooks; bypass withgit commit --no-verifyorRASK_SKIP_UNIT=1). - E2E runs locally, enforced before push. The browser-journey E2E
(
tests/Rask.Examples.E2E.Tests, Playwright) and the on-device native E2E (tests/Rask.Native.Appium.Tests, Appium) were moved out of the CI pipeline. Run the browser gate withscripts/run-e2e-local.sh; the.githooks/pre-pushhook runs it ongit push(enable hooks withgit config core.hooksPath .githooks; bypass withgit push --no-verifyorRASK_SKIP_E2E=1). The on-device native suite needs an emulator/simulator + Appium — run it manually (see native.md). - The CLI build gate runs locally, enforced before push.
scripts/run-cli-build-e2e.shis the only thing proving the code the CLI writes actually compiles — every other CLI test asserts on generated strings. It packs this commit's Rask packages to a local feed, scaffolds everyrask newflag combination plus a multi-entityrask generate featureand the whole tutorial walk-through, then builds each one with-warnaserror. Because it packs 15 packages and runs several full builds it is too slow for the pre-commit loop, so the.githooks/pre-pushhook runs it instead (bypass withgit push --no-verifyorRASK_SKIP_CLI_BUILD_E2E=1). The gates are opted into byRASK_CLI_BUILD_E2E=1, which the script exports; without it every case reports SKIPPED rather than passing silently, so an un-run gate is always visible in the test output. - The deploy gate runs locally, on pushes that touch the deploy path.
scripts/run-deploy-e2e-local.shpoints the realrask deployat a throwaway container standing in for a bare VPS — sshd plus its own Docker daemon (docker:dind, privileged) — and asserts on what happened on the host: an image that built over SSH, a container that answers its health check, a blue-green swap that retired the old colour, a Caddyfile a real Caddy accepted, and a named volume whose contents outlived the container. Every other deploy test is mocked, so this is the only coverage that the deploy actually deploys. It needs adockerCLI and a daemon that can run a privileged container; it installs nothing and never reads or writes your~/.ssh. The.githooks/pre-pushhook runs it only when the push changesDeployCommand/Host*/SshTarget/DockerProbe/DeployConfigor the deploy tests (bypass withRASK_SKIP_DEPLOY_E2E=1). Not covered: real DNS and Let's Encrypt issuance — the gate uses a.testdomain, so ACME never runs. - The provider gate runs locally, on changes to the claim or to provider registration.
scripts/run-providers-local.shstarts real PostgreSQL and SQL Server containers and races 20 processor instances for 200 jobs against each. (On arm64 it substitutes Azure SQL Edge, which is the SQL Server engine but not the full image — full SQL Server coverage needs an amd64 host.) It exists because the leasing design rests on one claim about the server — that anUPDATE … WHERE <claimable>re-evaluates its predicate against the row version the winner committed — and that cannot be proven on SQLite, where there is only one writer to begin with. Deliberately not part ofrun-unit-local.sh: requiring a Docker daemon on every commit is how a gate ends up permanently skipped (bypass withRASK_SKIP_PROVIDERS=1). commitlint.yml— Conventional Commits check on PRs.nightly.yml— prerelease publish onmain.release.yml— tag-triggered stable publish.- Dependencies are kept current by
.github/dependabot.yml(NuGet + Actions, weekly) and thecheck-nuget-updatesskill.