Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
c266ed2
feat(129-01): add a Cove-dependent test project beside the pure one
alextomas955 Sep 1, 2026
d36f898
refactor(129-01): move the Cove-dependent tests out and delete the mo…
alextomas955 Sep 1, 2026
95934e0
fix(129-01): name EF Core in the pure test project so it loads in sou…
alextomas955 Sep 1, 2026
013ac54
chore: merge executor worktree (worktree-agent-a5a837ec0b40a05eb)
alextomas955 Sep 1, 2026
6fb1613
feat(129-02): name both test projects in the catalog and enforce the …
alextomas955 Sep 1, 2026
6db8b41
feat(129-02): run the Cove-dependent test project on the cove-present…
alextomas955 Sep 1, 2026
db8f481
feat(129-02): declare the Cove source mode on every lint leg
alextomas955 Sep 1, 2026
3bc993f
docs(129-03): describe the two test projects wherever a document name…
alextomas955 Sep 1, 2026
986ab8d
feat(129-04): let the solution build skip a project that needs a Cove…
alextomas955 Sep 1, 2026
720bd76
docs(129-04): describe the modes the CI legs build in and why a fallb…
alextomas955 Sep 1, 2026
4107e45
docs(129-04): stop the lint workflow citing a rewritten comment as it…
alextomas955 Sep 1, 2026
98f50e9
test(129-05): pin the sentence dotnet format prints when references d…
alextomas955 Sep 1, 2026
ad58bbd
feat(129-05): parse the projects dotnet format could not load referen…
alextomas955 Sep 1, 2026
23e6f7d
feat(129-05): make the C# format pass name what it could not check
alextomas955 Sep 1, 2026
9e6890e
docs(129-05): state the transport-smoke pin without naming an identifier
alextomas955 Sep 1, 2026
92dca05
docs(129-06): describe the skip line the build prints and a runnable …
alextomas955 Sep 1, 2026
1e0c474
docs(129-06): name the C# pre-commit entry as invoked and its blind spot
alextomas955 Sep 1, 2026
89bbb77
docs(129-06): describe the shared Cove fakes as the unconditional com…
alextomas955 Sep 1, 2026
d25f658
docs(129-06): state the refusal reason the gate's own error gives
alextomas955 Sep 1, 2026
c3e8888
docs(129-07): state the Windows coverage hole where the README denied…
alextomas955 Sep 1, 2026
a86f2d0
docs(129-07): the testing page names the same Windows coverage hole
alextomas955 Sep 1, 2026
835c6d2
docs(129-07): the whole-solution test commands describe what they do
alextomas955 Sep 1, 2026
813fc33
docs(129-07): the configuration reference drops the replaced fallback…
alextomas955 Sep 1, 2026
0ac9381
Renamer: make the Cove-dependent test project un-runnable when its so…
alextomas955 Sep 1, 2026
ceca8dd
Renamer: drop the Directory.Build.targets claim about how each CI leg…
alextomas955 Sep 1, 2026
ee2d410
Renamer: state what a checkout-free whole-solution test run actually …
alextomas955 Sep 1, 2026
7cd03af
fix(129-10): the C# format wrapper refuses the runtime it cannot dete…
alextomas955 Sep 1, 2026
f45209c
fix(129-10): the format wrapper's disclosure reads correctly and cann…
alextomas955 Sep 1, 2026
e2c866a
fix(129-10): the merge-gating C# format check runs the same script th…
alextomas955 Sep 1, 2026
3143082
docs(129-09): the development page's gate commands run and its claims…
alextomas955 Sep 1, 2026
58d901e
docs(129-09): the configuration reference names the hook entry leftho…
alextomas955 Sep 1, 2026
369ec44
docs(129-09): the contributing guide states the CI checkout the workf…
alextomas955 Sep 1, 2026
00a7156
test(129-11): the validator is asked to refuse a reference onto a Cov…
alextomas955 Sep 1, 2026
f0d884c
feat(129-11): the validator refuses a ProjectReference onto a Cove te…
alextomas955 Sep 1, 2026
3d57233
docs(129-11): the transport smoke test's comment states what the test…
alextomas955 Sep 1, 2026
a39d607
fix(129): CR-01 the format wrapper's disclosure survives its own exit
alextomas955 Sep 1, 2026
f850ca4
fix(129): CR-02 a checkout-free whole-solution test run names what it…
alextomas955 Sep 1, 2026
d1acca1
docs(129): WR-01 the skip target's comment states the mechanism the b…
alextomas955 Sep 1, 2026
4fcb25e
fix(129): CR-04 the transport smoke test drives every route MapEndpoi…
alextomas955 Sep 1, 2026
0ec66a2
docs(129): CR-03 the traps list names the dotnet test arrival at the …
alextomas955 Sep 1, 2026
af830e4
fix(129): WR-02 the ProjectReference rule reads every MSBuild Include…
alextomas955 Sep 1, 2026
2acede9
fix(129): WR-03 the reference rule skips XML comments and reads the r…
alextomas955 Sep 1, 2026
57fabe3
fix(129): WR-04 the reference walk states its blind spots in its own …
alextomas955 Sep 1, 2026
0966d47
fix(129): WR-05 the merge-gating format step refuses to pass on parti…
alextomas955 Sep 1, 2026
7a9b9ca
fix(129): WR-06 the entry guard survives the runtimes it exists for, …
alextomas955 Sep 1, 2026
3e4c2fc
fix(129): WR-07 MapEndpoints carries one summary, and it no longer de…
alextomas955 Sep 1, 2026
bc54855
docs(129): WR-08 the transport smoke comment drops a count nothing pins
alextomas955 Sep 1, 2026
c03b3f0
fix(129): the reference rule declines a project file whose comment ne…
alextomas955 Sep 1, 2026
62d8440
fix(129): the format wrapper states why it names its binary rather th…
alextomas955 Sep 1, 2026
a9d69ef
fix(129): the reference rule reads every props and targets, and says …
alextomas955 Sep 2, 2026
68a7070
fix(129): the skip removes only an output directory the project owns
alextomas955 Sep 2, 2026
151c090
fix(129): the highest declared Cove floor is resolved by a tested export
alextomas955 Sep 2, 2026
d38dc5f
fix(129): the lint workflow calls the tested resolver, and its commen…
alextomas955 Sep 2, 2026
ab0a2f7
docs(129): dotnet test drops the source line and passes the skip warning
alextomas955 Sep 2, 2026
65a20f5
docs(129): the compile refusal runs one way, and the other way is on …
alextomas955 Sep 2, 2026
9c7951d
docs(129): drop a paragraph about an absent property and three versio…
alextomas955 Sep 2, 2026
ebf5e86
docs(129): three comments the test-project split left describing the …
alextomas955 Sep 2, 2026
7c7565a
fix(129): the floor reduce carries an initial value
alextomas955 Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Tick only the boxes for the extensions this PR touches.
If this PR touches **Renamer**:

- [ ] `dotnet test extensions/Renamer/src/Renamer.Tests/Renamer.Tests.csproj` passes
- [ ] `dotnet test extensions/Renamer/src/Renamer.Cove.Tests/Renamer.Cove.Tests.csproj` passes (needs
the `../cove` sibling checked out)
- [ ] `cd extensions/Renamer/src/Renamer.Ui && npm run verify` passes

Always:
Expand All @@ -21,7 +23,7 @@ Describe what you actually ran and observed.

Run `dotnet test` locally with the `../cove` sibling checked out. That covers what CI cannot: it compiles
against whatever branch the sibling happens to be on, so a regression on your fork branch is visible
there and nowhere else. CI's own C# leg compiles the cove-absent set.
there and nowhere else. CI builds Cove from a released tag instead.

## Safety check

Expand Down
35 changes: 29 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ jobs:
PROJECT_PATH: ${{ matrix.extension.projectPath }}
EXTENSION_VERSION: ${{ steps.check.outputs.version }}

# Cove-absent tier: CoveSourceMode=none Compile-Removes the cove-referencing sources, leaving every
# test that needs no Cove source type. The mode is DECLARED, never inferred from whether a ../cove
# Cove-absent tier: the project this step runs carries no reference to a Cove source project, so
# `none` withholds nothing from it. The mode is DECLARED, never inferred from whether a ../cove
# sibling is on disk, or a smaller run reads as a pass. Before Publish, so a test failure blocks
# packaging. Skipped for an entry with no testProjectPath.
#
Expand Down Expand Up @@ -518,10 +518,13 @@ jobs:
path: tests/e2e/playwright-report/
retention-days: 7

# ---- Cove-present tier. The build job above compiles against CoveSourceMode=none, which
# Compile-Removes every source referencing a Cove type, so the tests that need a real CoveContext
# never run there. This job supplies Cove by shallow-cloning the release tag the version axis
# resolved, which is what lets the full suite compile and run on a runner with no Cove checkout.
# ---- Cove-present tier. The tests needing a real CoveContext live in their own project, which the
# build job above does not run. This job supplies Cove by shallow-cloning the release tag the version
# axis resolved, which is what lets that project compile and run on a runner with no Cove checkout.
#
# It runs BOTH test projects. The pure tier is not duplicated coverage here: with a checkout it binds
# the checkout's Cove assemblies, where the build job binds the published packages' — the same tests
# against a different resolution.
#
# The mode is DECLARED, never inferred from whether a checkout landed: without an explicit
# -p:CoveSourceMode=source a failed clone selects the cove-absent branch and a smaller run reports
Expand Down Expand Up @@ -601,6 +604,26 @@ jobs:
TEST_PROJECT_PATH: ${{ matrix.extension.testProjectPath }}
COVE_REPO_ROOT: ${{ github.workspace }}/cove

# The emptiness guard is in the body rather than in `if:`, so this step stays on the same
# has_tier gate as the one above and an entry with a pure tier and no Cove tier is skipped rather
# than failed. Same zero-test property: the platform exits 8 on a run that executes nothing.
- name: Test (cove-present tier, the Cove-dependent project)
if: steps.check.outputs.has_tier == 'true'
working-directory: extensions
shell: bash
run: |
if [[ -z "$COVE_TEST_PROJECT_PATH" ]]; then
echo "::notice::$EXTENSION_NAME declares no coveTestProjectPath — it has no Cove-dependent tier to run."
exit 0
fi
dotnet test --project "$COVE_TEST_PROJECT_PATH" -c Release \
-p:CoveSourceMode=source \
-p:CoveRepoRoot="$COVE_REPO_ROOT"
env:
EXTENSION_NAME: ${{ matrix.extension.name }}
COVE_TEST_PROJECT_PATH: ${{ matrix.extension.coveTestProjectPath }}
COVE_REPO_ROOT: ${{ github.workspace }}/cove

# ---- Required-check aggregation.
# A single job to mark REQUIRED in branch protection on `main`, so a leg gates a merge by being in this
# job's `needs:` AND by having its result tested in the shell body below. Listing a job in `needs:`
Expand Down
113 changes: 95 additions & 18 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,96 @@ permissions:

jobs:
# ---- C# style + analyzer gate, run once against the shared .slnx, which is this gate's whole
# subject list. Adding an extension's projects to it stays manual because a .slnx is an explicit
# project list by design; an omission fails rather than passes, because validate-extension-repo.mjs
# asserts every catalog-implied project is declared there. ----
# subject list. dotnet format does not reach a referenced project, so narrowing the subject to one
# project silently drops the shared/ projects and the second test tier out of format coverage.
# Adding an extension's projects to the solution stays
# manual because a .slnx is an explicit project list by design; an omission fails rather than passes,
# because validate-extension-repo.mjs asserts every catalog-implied project is declared there.
#
# The subject list holds a test project that needs Cove's own source to compile, so this job checks
# Cove out and runs in source mode. The analyzer step gains independently: a `none` build sees a
# strict subset of the findings a source build sees.
#
# The two repositories are checked out as SIBLINGS under the workspace rather than nesting Cove
# inside this one. Cove carries no Directory.Packages.props, so a nested clone inherits this repo's
# central package management and every version-carrying PackageReference in Cove fails NU1008. ----
csharp-format:
runs-on: ubuntu-latest
timeout-minutes: 15
# Clones and builds Cove's own source, so the budget covers a cold clone and a full Release build,
# not only the format pass. A timeout here would read as a style failure on the contributor's branch.
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
path: extensions
- uses: actions/setup-node@v7
with:
node-version: "24.11.0"
# The ref is not named here: it is the highest floor the extensions themselves declare, read from
# each entry's own extension.json. A version written into this file would be a second declaration
# of a floor that already has one, free to disagree with it. Where entries disagree the highest
# wins, and the run names every floor it saw.
- name: Resolve the Cove ref from the declared floors
id: floor
working-directory: extensions
shell: bash
run: |
node --input-type=module -e '
import { readExtensionFloors, highestDeclaredFloor } from "./scripts/fetch-cove-assemblies.mjs";
const declared = readExtensionFloors();
const highest = highestDeclaredFloor(declared);
for (const { entry, floor, manifestPath } of declared) {
console.error(entry.name + " declares floor " + floor + " (from " + manifestPath + ")");
}
const distinct = new Set(declared.map((d) => d.floor));
if (distinct.size > 1) {
console.error("::notice::" + distinct.size + " distinct floors are declared; formatting and analysing against the highest, " + highest.floor + ".");
}
console.log("ref=v" + highest.floor);
' >> "$GITHUB_OUTPUT"
- name: Check out Cove at the declared floor
uses: actions/checkout@v7
with:
repository: yourcove/cove
ref: ${{ steps.floor.outputs.ref }}
fetch-depth: 1
path: cove
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
# Formatting/style check — reads root .editorconfig. Same invocation as `npm run
# format:cs:check`, so a contributor's local run and this job cannot disagree. The exclude is a
# no-op here (CI has no ../cove sibling, so Cove arrives from NuGet); it earns its place locally,
# where Directory.Build.props auto-detects the sibling and dotnet format then walks the
# ProjectReference graph into Cove's own source. A path that does not exist is accepted silently.
# Formatting/style check — reads root .editorconfig. Runs the same script the pre-commit hook
# and the repo-root npm scripts run; that script supplies the solution name itself. The exclude
# keeps the sibling checkout out of the subject set: dotnet format walks the ProjectReference
# graph into Cove's own source otherwise. dotnet format takes no -p:, so
# COVE_SOURCE_MODE is the only spelling of the mode available to it.
#
# --fail-on-partial is passed here and nowhere else: the checkout this job's own preamble makes
# is what the analyzer half needs, so a run that reports references did not load is a run whose
# subject set is smaller than this job was set up to check.
- name: dotnet format (verify)
run: dotnet format CoveExtensions.slnx --verify-no-changes --severity warn --exclude ../cove/
working-directory: extensions
run: node scripts/check-csharp-format.mjs --verify-no-changes --severity warn --exclude ../cove/ --fail-on-partial
env:
COVE_SOURCE_MODE: source
COVE_REPO: ${{ github.workspace }}/cove
# Analyzer gate: a Release build with TreatWarningsAsErrors=true (root Directory.Build.props)
# fails on any CA/IDE warning.
- name: Build with analyzers (warnings = errors)
run: dotnet build CoveExtensions.slnx -c Release -p:CoveSourceMode=none
working-directory: extensions
run: dotnet build CoveExtensions.slnx -c Release -p:CoveSourceMode=source -p:CoveRepoRoot="$COVE_REPO_ROOT"
env:
COVE_REPO_ROOT: ${{ github.workspace }}/cove

# ---- Windows build + unit-test signal. Compiles every solution member, then runs each catalog
# entry's testProjectPath, so adding an extension is a catalog edit.
# ---- Windows build + unit-test signal. Builds each catalog entry's cove-absent test project and
# then runs it, so adding an extension is a catalog edit. dotnet build follows the ProjectReference
# graph, so building a test project also builds the extension and the shared projects it reaches.
# A solution member no test project reaches is therefore not built here.
#
# No E2E here, and this will not change: GitHub-hosted windows-latest runners fix Docker to Windows
# containers with no supported switch to Linux ones, so Testcontainers cannot start the Cove app and
# Postgres images at all. What is achievable is verified instead — the solution builds and the
# cove-absent unit tier passes on Windows. A full Windows E2E would need a self-hosted runner. ----
# Postgres images at all. What is achievable is verified instead — each cove-absent test project
# builds and passes on Windows. A full Windows E2E would need a self-hosted runner. ----
windows-build-test:
runs-on: windows-latest
timeout-minutes: 25
Expand All @@ -60,8 +119,26 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Build
run: dotnet build CoveExtensions.slnx -c Release -p:CoveSourceMode=none
# The whole-slnx Build above already compiled every test project, so --no-build still holds.
shell: bash
run: |
mapfile -t tests < <(node -e '
const fs = require("fs");
const catalog = JSON.parse(fs.readFileSync("extensions/catalog.json", "utf8"));
for (const entry of catalog.extensions) {
if (entry.manifestOnly === true) continue;
if (entry.testProjectPath) console.log(entry.testProjectPath);
}
')
for test in "${tests[@]}"; do
echo "::group::dotnet build $test"
dotnet build "$test" -c Release -p:CoveSourceMode=none
echo "::endgroup::"
done
# The step above built each of these projects, so --no-build still holds.
#
# The loop stays sequential. Tests that map and unmap a subst drive letter are serialized by a
# collection, and xUnit scopes a collection to one assembly, so two assemblies running at once
# can reclaim a letter another is still holding.
- name: Test (unit tier)
shell: bash
run: |
Expand All @@ -75,7 +152,7 @@ jobs:
')
for test in "${tests[@]}"; do
echo "::group::dotnet test $test"
dotnet test --project "$test" -c Release --no-build
dotnet test --project "$test" -c Release --no-build -p:CoveSourceMode=none
echo "::endgroup::"
done

Expand Down
10 changes: 6 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,12 @@ Jobs/ · Options/`). UI = feature slices directly under `src/` next to `index.ts
race and made "how much history is kept" a number someone had to choose, where a row insert is atomic
and a whole batch either falls inside the window or is gone.
- **Testing.** Tests MIRROR their source folders; only test-only groups (`TestSupport/`,
`TransportSmoke/`, e2e) sit outside the mirror. The bare-CI (cove-absent) leg is a compile/pure
SMOKE — any test that references a Cove source type is Compile-Removed there — and is NOT the
safety gate; the containerized e2e job is the required safety gate. Keep `*Logic.ts` offline-gated
so pinned wire-casing enums fail a gate on drift.
`TransportSmoke/`, e2e) sit outside the mirror. An extension's backend suite is TWO projects split
on whether a test needs a real `CoveContext` from `Cove.Data`; a test that needs one and is not in
the Cove-dependent project does not compile. The bare-CI (cove-absent) leg runs the project holding
no Cove-source reference, and that leg is a compile/pure SMOKE and is NOT the safety gate; the
containerized e2e job is the required safety gate. Keep `*Logic.ts` offline-gated so pinned
wire-casing enums fail a gate on drift.
- **Tooling as merge gates.** jscpd, knip, syncpack, and import-boundaries run as **blocking** merge
gates. Rollout: land each tool, get it green on `main`, THEN flip to blocking. **Only a check a CI
workflow runs can block a merge** — an entry in the local hook runner is advice a contributor can
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ npm run format:cs
Run it through the script rather than calling `dotnet format` directly. Two reasons, both of which
have bitten here: with a `../cove` sibling checked out, `dotnet format` follows the ProjectReference
graph into Cove's own source and reports hundreds of issues that are not yours — the script excludes
it, and that exclude is a harmless no-op in CI, which has no sibling. And a folder path passed to
`--include`/`--exclude` **must end in a separator**: `--include ./src` matches nothing and exits 0,
while `--include ./src/` works. A scoping typo there does not fail — it silently passes.
it, and that exclude does the same work in CI, which checks Cove out beside this repo. And a folder
path passed to `--include`/`--exclude` **must end in a separator**: `--include ./src` matches nothing
and exits 0, while `--include ./src/` works. A scoping typo there does not fail — it silently passes.

Each extension has its own build/test/verify commands — see that extension's own README
([`extensions/Renamer/README.md`](extensions/Renamer/README.md)) and
Expand Down
1 change: 1 addition & 0 deletions CoveExtensions.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<Folder Name="/extensions/">
<Project Path="extensions/Renamer/src/Renamer/Renamer.csproj" />
<Project Path="extensions/Renamer/src/Renamer.Tests/Renamer.Tests.csproj" />
<Project Path="extensions/Renamer/src/Renamer.Cove.Tests/Renamer.Cove.Tests.csproj" />
</Folder>
<Folder Name="/shared/">
<Project Path="shared/Cove.Extensions.Shared/Cove.Extensions.Shared.csproj" />
Expand Down
Loading
Loading