diff --git a/.buildvana/hooks/release/post-release.cs b/.buildvana/hooks/release/post-release.cs index 3ecc6fab..c50e08bb 100644 --- a/.buildvana/hooks/release/post-release.cs +++ b/.buildvana/hooks/release/post-release.cs @@ -7,8 +7,8 @@ using System.Text.RegularExpressions; using Buildvana.Runtime; -// release/post-release hook: keeps the $schema URL in buildvana.jsonc pointing at the release tag -// of the version being released. The guard mirrors the built-in self-reference rewrites: the +// release/post-release hook: keeps the $schema URL in the configuration file pointing at the release +// tag of the version being released. The guard mirrors the built-in self-reference rewrites: the // $schema URL is itself a self-reference, so it moves only when dogfooding moves the rest. var hookArgs = PostReleaseHookArgs.Load(); if (!hookArgs.Dogfooding) @@ -16,8 +16,22 @@ return; } +// AFTER THE NEXT RELEASE: replace the search below with +// var configFile = hookArgs.RuntimeInfo.ConfigFile; +// keeping the null check. That member names the file bv itself read, which is what a hook rewriting the +// configuration file should act on, and what Hooks.md tells hooks to use instead of searching for one. +// It cannot be used yet: the SDK pins Buildvana.Runtime to its own version, so this hook compiles against +// the last published release, and RuntimeInfo.ConfigFile ships with the next one. Searching is correct in +// the meantime — it is the same search bv performs, over the directory bv reports as home — the hook just +// answers on its own rather than being told. +var configFile = BuildvanaConfig.FindFile(hookArgs.RuntimeInfo.HomeDirectory); +if (configFile is null) +{ + return; +} + // Same expression as SelfVersionService.SchemaUrlRegex in src/Buildvana.Tool, which `bv update` applies to // consumer repositories' configuration files; keep the two copies identical. -var text = File.ReadAllText("buildvana.jsonc"); +var text = File.ReadAllText(configFile); text = Regex.Replace(text, "(Tenacom/Buildvana/)[^/]+(/schemas/)", $"${{1}}{hookArgs.Release.SemVer}$2"); -File.WriteAllText("buildvana.jsonc", text); +File.WriteAllText(configFile, text); diff --git a/CHANGELOG.md b/CHANGELOG.md index bd418c16..07ed73bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,6 @@ See the Nerdbank.GitVersioning removal entry under _Changes to existing features - Commands that forward extra arguments to `dotnet` (`restore`, `build`, `test`, `pack`) are marked as such in `bv`'s root help, and their per-command help (`bv --help`) includes a `FORWARDED ARGUMENTS` section. - Buildvana now recognizes a repository-root configuration file, `buildvana.json` (or its commented variant `buildvana.jsonc`). It is discovered, parsed, validated, and exposed to `bv`; the settings it currently drives are listed below, and more are wired in over subsequent releases. A committed JSON schema (`schemas/buildvana.schema.json`) is generated from the typed model, so editors can validate and document the file; unknown keys, an invalid file, or the presence of both `buildvana.json` and `buildvana.jsonc` in the same directory are reported as errors and will prevent `bv` from executing _any_ subcommand, even those that are not driven by any configuration setting (e.g., `clean`). - Both `bv` and the SDK now treat a `buildvana.json`/`buildvana.jsonc` file as a home-directory marker, alongside Git markers; home-directory discovery now stops at the nearest directory (the starting directory included) that contains any marker. -- The configuration file is now also recognized in a `.buildvana` directory directly under the home directory (`.buildvana/buildvana.json` or `.buildvana/buildvana.jsonc`), so that repositories accumulating more Buildvana-related files can group them there instead of cluttering the root. Exactly one configuration file may exist per home directory across the four candidate locations; any two coexisting is an error naming all offending files, generalizing the previous rule against having both `buildvana.json` and `buildvana.jsonc`. A configuration file inside `.buildvana` is a home-directory marker like one in the root, marking the directory that contains `.buildvana` as home; a `.buildvana` directory without a configuration file is not a marker. - `buildvana.json` now drives several build and release settings that were previously CLI-only or hardcoded. Each resolves as CLI flag (where one exists) → `buildvana.json` → built-in default: - the default build configuration (`dotnet.configuration`, default `Release`), used by `bv restore`/`build`/`test`/`pack` and as the base of `bv release`'s configuration chain (`--configuration` → `release.configuration` → `dotnet.configuration`); - extra arguments and environment variables for each `dotnet` invocation: `dotnet.all` (applied to every invocation) merged with the per-command `dotnet.restore`/`dotnet.build`/`dotnet.test`/`dotnet.pack`/`dotnet.nugetPush`, each carrying `args` and `env`. Arguments are appended in the order base → `dotnet.all` → per-command → forwarded command-line arguments (so a `--` argument still wins); environment variables apply `dotnet.all` then the per-command entries; @@ -39,7 +38,7 @@ See the Nerdbank.GitVersioning removal entry under _Changes to existing features With a `VERSION` file present, every project built with Buildvana SDK gets `$(Version)`, `$(PackageVersion)`, `$(AssemblyVersion)` (precision-controlled), `$(FileVersion)` (`MAJOR.MINOR.HEIGHT.0`), and `$(InformationalVersion)` computed at build time by a compiled task using LibGit2Sharp — no `git` executable and no external package required — under plain `dotnet build` as well as `MSBuild.exe` and `bv`. The `UseVersioning` property overrides the automatic module activation in both directions. `bv release` now computes versions the same way, in-process. - Versioned C# projects get the generated `ThisAssembly` class by default: when the `Versioning` module is active, `GenerateThisAssemblyClass` defaults to `true`, and the module contributes versioning constants alongside the default assembly-attribute constants: `SimpleVersion`, `SemVer`, `IsPublicRelease`, `IsPrerelease`, and `GitCommitId`. Compared with Nerdbank.GitVersioning's `ThisAssembly`, the `GitCommitDate`/`GitCommitAuthorDate` and `PublicKey`/`PublicKeyToken` constants are not provided, while `AssemblyDescription`, `SimpleVersion`, and `SemVer` are new. - `bv` now runs optional repository-owned hooks: [file-based apps](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/file-based-programs) at well-known paths of the form `.buildvana/hooks//.cs` — `` names the context the event belongs to (currently always the invoking command), `` the moment of execution that triggers the hook — run via `dotnet run` from the home directory when the event occurs, and skipped with an info message when absent. Args are passed through a JSON file at a per-hook well-known path, `.buildvana-temp/hook-args//.json`, (re)written before each hook run and left in place afterwards, so a hook can be re-run by hand against the args of the last run; `.buildvana-temp/` is bv's scratch directory for machine-generated temporary files, recommended for gitignoring and always excluded from bv's own working-tree change detection. `bv clean` clears the hooks' file-based-app build caches and deletes the scratch directory. The first event is `release/post-release`, raised by `bv release` at the moment the post-release commit is assembled: before the built-in self-reference rewrites (when dogfooding is enabled) and before anything is pushed — the hook is a file-based app inside the repository tree, so it builds against the version pins as they stand before the release and reads the version being released from its args. The hook runs whether or not dogfooding is enabled; files it changes join the post-release commit alongside the built-in rewrites, and a non-zero exit code aborts the release before anything is pushed. See [docs/Hooks.md](docs/Hooks.md) for the full contract. -- A new packaged library, `Buildvana.Runtime`, holds the typed model of the Buildvana configuration file and of the run-time information `bv` shares with repository-owned hooks: `BuildvanaConfig` (with `BuildvanaConfig.Load()`, a strict loader usable from file-based apps, where reflection-based JSON serialization is disabled, plus the well-known configuration file names as constants), `PostReleaseHookArgs` (with `PostReleaseHookArgs.Load()`, structured into a `RuntimeInfo` section — shared by the args of every hook through the `HookArgs` base record, and carrying the running `bv`'s version, the delegating `bv`'s version when the run was delegated, and the absolute paths of the run's well-known directories — plus a `Release` section and other hook-specific members; every args type also implements the `IHookEvent` interface, naming its hook's context and event as static properties, so that `bv` dispatches a hook from its args type alone and a hook's identity can never be mismatched with its args), and the well-known paths shared by both sides of the hook contract (directory constants and per-hook path helpers). A hook references the package with an unversioned `#:package Buildvana.Runtime` directive; Buildvana SDK pins the package to its own version for every file-based app built in the repository, so `bv`, the SDK, and hooks always agree on the shape of the data. +- A new packaged library, `Buildvana.Runtime`, holds the typed model of the Buildvana configuration file and of the run-time information `bv` shares with repository-owned hooks: `BuildvanaConfig` (with `BuildvanaConfig.Load()` and `BuildvanaConfig.LoadFile()`, strict loaders usable from file-based apps, where reflection-based JSON serialization is disabled, plus the well-known configuration file names as constants), `PostReleaseHookArgs` (with `PostReleaseHookArgs.Load()`, structured into a `RuntimeInfo` section — shared by the args of every hook through the `HookArgs` base record, and carrying the running `bv`'s version, the delegating `bv`'s version when the run was delegated, the absolute paths of the run's well-known directories, and the absolute path of the configuration file the run read, so that a hook working on that file acts on the one `bv` read instead of searching for it; `hookArgs.LoadConfig()` loads that same file, so a hook reading settings does not search either — plus a `Release` section and other hook-specific members; every args type also implements the `IHookEvent` interface, naming its hook's context and event as static properties, so that `bv` dispatches a hook from its args type alone and a hook's identity can never be mismatched with its args), the well-known paths shared by both sides of the hook contract (directory constants and per-hook path helpers), and the built-in default of each setting that has one, as a constant next to the setting plus an `Effective…` accessor that applies it whether the setting or its whole section is absent — so `bv`, SDK tasks, and hooks resolve a setting to the same value instead of each spelling out its own fallback. A hook references the package with an unversioned `#:package Buildvana.Runtime` directive; Buildvana SDK pins the package to its own version for every file-based app built in the repository, so `bv`, the SDK, and hooks always agree on the shape of the data. - Before running any command that uses Buildvana SDK (`restore`, `build`, `test`, `pack`, and `release`), `bv` now verifies that the repository pins the SDK (the `Buildvana.Sdk` entry under `msbuild-sdks` in `global.json`) at its own version: `bv`, `Buildvana.Sdk`, and `Buildvana.Runtime` are released in lockstep, and a version mismatch — a half-updated repository, a newer globally-installed tool against an older pin — would otherwise produce silent behavior drift. A missing `global.json`, section, or entry counts as a mismatch. On mismatch, the command fails with a message naming both versions and the ways to align them. Versions are compared by SemVer precedence, ignoring build metadata. The new global option `--skip-sdk-check` skips the check, for scenarios that require a deliberate mismatch (e.g. bisecting an SDK regression in CI). - `bv` now delegates to the repository's pinned version: whenever the tool manifest (`.config/dotnet-tools.json`) pins `bv`, the pinned version is the one that runs, no matter which `bv` is invoked — like the Angular CLI, where the global `ng` always hands over to the project-local install. The invoked `bv` makes sure the pinned version is installed — probing the SDK's tool resolver cache the same way `dotnet tool run` does, and running `dotnet tool restore` only when needed; a failed restore is reported but does not block the attempt — and hands it the entire original command line (`dotnet tool run bv`) with inherited standard streams, forwarding its exit code. The delegated `bv` runs from the home directory, so a relative path inside forwarded arguments resolves against the home directory rather than the invocation directory; and `--version` answers for the pinned `bv` (pass `--skip-delegation` to ask the invoked binary). When the versions differ, an info line on standard error names the version that runs. A delegating `bv` does not judge the command line beyond the minimal split that finds the subcommand and the global options (only a value-bearing global option with no following value, such as a trailing `-v`, is rejected before delegation, with the same message in every version), and does not read the configuration file — both may be valid for the pinned version and not for the invoked one, and judging them is the pinned version's job. The new `update` subcommand is exempt (see below); the new global option `--skip-delegation` runs the exact binary invoked; and the `BV_DELEGATED` environment variable, set on the delegated child, guarantees that a delegated invocation never delegates again. The variable is removed from the environment of every other child process `bv` spawns, so a `bv` reached through a hook or a build makes its own delegation decision. A `bv` invoked outside a repository, or in a repository whose tool manifest does not pin `bv` (the entry is matched case-insensitively, like the dotnet CLI matches it; an entry with an unusable version is reported and treated as no pin), runs in place as before. - A new `bv update` command updates the repository's entire Buildvana surface to the running `bv`'s version in one operation: the `bv` pin in the tool manifest, via `dotnet tool update` (or `dotnet tool install --create-manifest-if-needed` when there is no entry yet), which also downloads the version; the `Buildvana.Sdk` pin in `global.json`, creating the file and/or the `msbuild-sdks` section if needed and preserving the file's formatting otherwise; and the version segment of the configuration file's `$schema` reference, when it points at the canonical `Tenacom/Buildvana//schemas/` URL. Afterwards, the configuration file is loaded with the new version's model, and any problems are reported as warnings for review. `update` is exempt from delegation — it updates the repository to the `bv` actually invoked ("bring this repository to me"), so the usual upgrade flow is `dotnet tool update -g bv` followed by `bv update`, and `dnx bv@ update` targets any specific version — and it refuses to downgrade a repository whose pins are newer than the running `bv`, unless `--force` is passed. A manifest whose `bv` entry pins an invalid version is beyond the dotnet CLI's reach entirely (the CLI cannot parse such a manifest), so `bv update` fails up front with a message naming the entry to fix. @@ -49,7 +48,7 @@ See the Nerdbank.GitVersioning removal entry under _Changes to existing features - **BREAKING CHANGE**: `bv` now defaults to `minimal` verbosity, for every command alike; it used to default to `normal`. The build pipeline commands wrap `dotnet restore`/`build`/`test`/`pack`, which default to `minimal` themselves and receive `bv`'s verbosity verbatim, so `bv build` used to produce a markedly noisier MSBuild log than plain `dotnet build`; it now produces a comparable one. Nothing fails and no migration is required: pass `-v normal` for the previous output. Note that `bv`'s own narration — activity start/finish lines and the `info:` lines describing what the tool is doing — is hidden at the new default, while the record of what a command _did_ survives it (see the new `notice:` level below), so `bv release` still logs its complete account of what it changed and published. - A new message level, rendered as `notice:`, sits between `warning:` and `info:` and is shown from `minimal` verbosity up. It carries the messages that record a fact — something changed, something was decided, something was deliberately skipped — as opposed to the narration of what a command is doing at a given moment, which stays at `info:`. This is also the level at which the version computed for a build is reported, so that line keeps appearing in `dotnet build` output at its default verbosity, as it did before. Buildvana SDK tasks and `bv` now agree on the verbosity at which each level becomes visible: previously a message logged by a task showed up one rung earlier than the same message printed by `bv`. -- **BREAKING CHANGE**: The `.buildvana-home` marker file is no longer recognized: home-directory discovery now only looks for a Buildvana configuration file (in the home directory root or in its `.buildvana` subdirectory) and Git markers. `.buildvana-home` predates the configuration file, and a `buildvana.json` containing an empty object (`{}`) does the same job — marking a directory as home without configuring anything — while being what one would naturally reach for. To migrate, replace `.buildvana-home` with a `buildvana.json` file containing `{}`. +- **BREAKING CHANGE**: The `.buildvana-home` marker file is no longer recognized: home-directory discovery now only looks for a Buildvana configuration file (in the home directory itself) and Git markers. `.buildvana-home` predates the configuration file, and a `buildvana.json` containing an empty object (`{}`) does the same job — marking a directory as home without configuring anything — while being what one would naturally reach for. To migrate, replace `.buildvana-home` with a `buildvana.json` file containing `{}`. - **BREAKING CHANGE**: The `JetBrainsAnnotations` module no longer adds any JetBrains annotations package to your project, and the `UseJetBrainsAnnotations` property has been removed. To export ReSharper external annotations, reference an annotations source yourself — the compiled `JetBrains.Annotations` package, `JetBrains.Annotations.Sources`, or your own attributes in the `JetBrains.Annotations` namespace — and set the new boolean `ExportJetBrainsAnnotations` property (default `false`) to `true`. When enabled, Buildvana SDK reads the annotations from source with Roslyn after each build and packs a `{AssemblyName}.ExternalAnnotations.xml` file next to the assembly in `lib/`, one per target framework. The export no longer depends on Mono.Cecil or on a second build pass; and, when the annotation attributes are `[Conditional("JETBRAINS_ANNOTATIONS")]` (as in the JetBrains packages), no JetBrains attribute metadata remains in the compiled assembly, leaving clean IL and AOT output. - **BREAKING CHANGE**: The `JetBrainsAnnotations` module no longer supports Visual Basic projects. `ExportJetBrainsAnnotations` is forced to `false` for any project that is not a C# (`.csproj`) project, because the exporter reads C# source directly. - `bv` may now be invoked from any subdirectory under the solution's directory (`HomeDirectory` property in Buildvana SDK). It will search upwards for the home directory, the same way the SDK does, and work from there. If it doesn't find the home directory, `bv` will exit with a non-zero exit code. As soon as the home directory is discovered, `bv` makes it the process's current directory: from that point on, every relative path — including relative paths in forwarded arguments — resolves against the home directory no matter where `bv` was invoked from, matching delegated runs, which are spawned from the home directory. Commands that never need the home directory (e.g. `bv --version`) leave the current directory untouched. diff --git a/.buildvana/buildvana.jsonc b/buildvana.jsonc similarity index 100% rename from .buildvana/buildvana.jsonc rename to buildvana.jsonc diff --git a/docs/DirectoryStructure.md b/docs/DirectoryStructure.md index dd274d30..b03e008c 100644 --- a/docs/DirectoryStructure.md +++ b/docs/DirectoryStructure.md @@ -37,8 +37,6 @@ We will follow the MSBuild convention of a backslash (`\`) as a path separator. | | +--- release\ | | | | | +--- post-release.cs -| | -| +--- buildvana.jsonc <<< Buildvana configuration file, if not in the home directory root | +--- .buildvana-temp\ <<< bv's scratch directory (machine-generated; add to .gitignore) | @@ -63,7 +61,7 @@ We will follow the MSBuild convention of a backslash (`\`) as a path separator. | +--- Common.props <<< Portions of MSBuild code common to all projects in tests\ | +--- Common.targets | -+--- buildvana.jsonc <<< Buildvana configuration file (or buildvana.json), if not in .buildvana\ ++--- buildvana.jsonc <<< Buildvana configuration file (or buildvana.json) | +--- Common.props <<< Common parts of MSBuild projects +--- Common.targets @@ -108,11 +106,11 @@ The full path of the home directory, including a trailing path separator, is sto Buildvana SDK determines the location of the home directory by walking up the directory hierarchy, starting from the project's directory (included), and stopping at the nearest directory that contains any of these home markers: -- a Buildvana configuration file (`buildvana.json` or `buildvana.jsonc`), either directly in the directory or in a `.buildvana` subdirectory (a `.buildvana` directory without a configuration file is _not_ a marker); +- a Buildvana configuration file (`buildvana.json` or `buildvana.jsonc`); - a Git worktree or submodule (a file named `.git`); - a regular Git repository (a file named `HEAD` in a `.git` subdirectory). -The directory containing the marker becomes the home directory, and its full path becomes the value of `HomeDirectory`. Note that a configuration file inside `.buildvana` marks the directory containing `.buildvana`, not `.buildvana` itself. A configuration file does not have to actually configure anything: an empty JSON object (`{}`) is valid content, making the file usable as a pure home-directory marker. +The directory containing the marker becomes the home directory, and its full path becomes the value of `HomeDirectory`. Every marker sits in the directory it marks, so nothing under a subdirectory — the `.buildvana` directory included — takes part in discovery: hooks are projects living under `.buildvana\`, and a marker recognized in there would make each of them discover `.buildvana\` as its own home directory. A configuration file does not have to actually configure anything: an empty JSON object (`{}`) is valid content, making the file usable as a pure home-directory marker. If no marker is found, the build (or project loading in Visual Studio) stops with error [BVSDK1003](SdkDiagnostics.md#buildvana-sdk-core-1000-1049). diff --git a/docs/Hooks.md b/docs/Hooks.md index 327df16c..67aaa254 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -80,6 +80,7 @@ The well-known paths themselves ship in the package too: `WellKnownPaths` expose | `RuntimeInfo.HomeDirectory` | string | Absolute path of the home directory, without a trailing separator (also the hook's working directory). | | `RuntimeInfo.ArtifactsDirectory` | string | Absolute path of the directory containing the build artifacts. | | `RuntimeInfo.ScratchDirectory` | string | Absolute path of bv's scratch directory (`.buildvana-temp/`), where hooks can write temporary files without affecting working-tree change detection. | +| `RuntimeInfo.ConfigFile` | string or null | Absolute path of the configuration file this run read, or `null` when the repository has none. See [Loading the repository configuration](#loading-the-repository-configuration). | | `Release.Version` | string | The version being released, in simple `MAJOR.MINOR.PATCH` form, without any prerelease tag. | | `Release.SemVer` | string | The version being released, in full semantic version form. This is the form used by release tags and embedded in artifact names. | | `Release.PreviousVersion` | string or null | The previously released version (the latest release tag reachable from `HEAD`), or `null` when no previous release exists. | @@ -94,14 +95,26 @@ In the JSON file, member names are camelCase (`runtimeInfo.homeDirectory`, `rele ## Loading the repository configuration -The args carry the facts of the run; for any standing repository setting, load the configuration file instead: `BuildvanaConfig.Load()` probes the four well-known candidates (`buildvana.json`, `buildvana.jsonc`, and the same names under `.buildvana/`), applies the usual exactly-one rule, tolerates comments and trailing commas, and returns the typed configuration (an empty instance when no configuration file exists): +The args carry the facts of the run; for any standing repository setting, load the configuration instead. `hookArgs.LoadConfig()` reads the file `bv` itself read for this run, and returns the typed configuration (an empty instance when the repository has no configuration file): ```csharp -var config = BuildvanaConfig.Load(); +var config = hookArgs.LoadConfig(); var branches = config.Release?.Branches; ``` -The loader is strict — an unknown member fails the load — but does not re-validate what `bv` has already validated with schema-based diagnostics before running any hook. +Which file to read comes from the args, so a hook never searches for one; what it says is read at the moment of the call, so the hook sees the file as it stands even if an earlier hook in the same run rewrote it. The loader tolerates comments and trailing commas, and is strict about content — an unknown member fails the load — but it does not re-validate what `bv` has already validated with schema-based diagnostics before running any hook. + +`BuildvanaConfig.Load()`, which searches a directory for a configuration file, remains available for code that has no hook args to hand. + +A hook that works on the configuration file _itself_ — rewriting a value in it, say — needs the path rather than the settings, and must act on the file `bv` actually read. That path is in the args, as `RuntimeInfo.ConfigFile` (`null` when the repository has no configuration file); do not hardcode a file name, and do not search for one: + +```csharp +var configFile = hookArgs.RuntimeInfo.ConfigFile; +if (configFile is not null) +{ + File.WriteAllText(configFile, Rewrite(File.ReadAllText(configFile))); +} +``` ## Dependencies @@ -124,4 +137,6 @@ Local file-based-app caching may not notice implicit-build-file changes; CI is a ## Contract evolution -The args file is written by the installed `bv` and read through the `Buildvana.Runtime` version pinned by the repository's Buildvana SDK; `bv` and the SDK are released in lockstep and designed as a matched pair. The contract is nevertheless additive-only: new members may be added, but existing ones are never removed or repurposed, and additions ship as optional members with default values — so an args file written before an update stays loadable after it. +The args file is written by the installed `bv` and read through the `Buildvana.Runtime` version pinned by the repository's Buildvana SDK — the version of the SDK in use, which `bv` refuses to run against unless it matches its own. The hook is compiled from source at every run, and its args file is rewritten immediately before it. Writer and reader are therefore the same version by construction, and the JSON never has to survive a version boundary. + +What must stay stable is the _source_ surface a hook compiles against: members are never removed or repurposed, so that a hook written today still compiles after an update. Additions may be required members — every run then states every fact the args carry, and none can be left unset by mistake. (An args file left over from a run that predates such an addition no longer loads; re-run the command that raises the hook, and it is rewritten.) diff --git a/docs/SdkDiagnostics.md b/docs/SdkDiagnostics.md index c58f1478..0818e18f 100644 --- a/docs/SdkDiagnostics.md +++ b/docs/SdkDiagnostics.md @@ -36,7 +36,7 @@ Each module is assigned a contiguous range of 100 diagnostics, as listed below. | BVSDK1002 | Error | Sdk.props and Sdk.targets are in different directories. | `Sdk.props` and `Sdk.targets` were imported from two different versions of Buildvana SDK; look for stray `Version` attributes in the `` directives. | | BVSDK1003 | Error | Home directory not defined. | No suitable value for the `HomeDirectory` property has been found. | | BVSDK1004 | Error | Buildvana SDK requires at least MSBuild v... | You are trying to use Buildvana SDK with an unsupported version of MSBuild. See [the README](../README.md#toolchain) for a list of supported MSBuild versions. | -| BVSDK1005 | Error | Multiple Buildvana configuration files found. | A home directory contains more than one configuration file (in the root and/or the .buildvana subdirectory); keep only one. | +| BVSDK1005 | Error | Multiple Buildvana configuration files found. | A home directory contains both `buildvana.json` and `buildvana.jsonc`; keep only one. | ## Buildvana SDK tasks (1050-1099) diff --git a/src/Buildvana.Core.Configuration/BuildvanaConfigLoader.cs b/src/Buildvana.Core.Configuration/BuildvanaConfigProvider.cs similarity index 58% rename from src/Buildvana.Core.Configuration/BuildvanaConfigLoader.cs rename to src/Buildvana.Core.Configuration/BuildvanaConfigProvider.cs index 133eacd4..b7dcd1a4 100644 --- a/src/Buildvana.Core.Configuration/BuildvanaConfigLoader.cs +++ b/src/Buildvana.Core.Configuration/BuildvanaConfigProvider.cs @@ -1,11 +1,13 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; +using Buildvana.Core.HomeDirectory; using Buildvana.Core.IO; using Buildvana.Core.JsonSchema; using Buildvana.Runtime; @@ -14,15 +16,19 @@ namespace Buildvana.Core.Configuration; /// -/// Loads and validates the Buildvana configuration file found in a home directory. +/// Provides the Buildvana configuration of a home directory: which file holds it, and what it says. /// /// /// Unlike the lean loader shipped with Buildvana.Runtime (), this -/// loader validates the file against the configuration schema and reports each problem as a diagnostic with its -/// source location. It is the loader used by bv and by Buildvana SDK tasks; hooks, which read a file -/// bv has already validated, use the lean loader instead. +/// one validates the file against the configuration schema and reports each problem as a diagnostic with its +/// source location. It is used by bv and by Buildvana SDK tasks; hooks, which read a file bv has +/// already validated, use the lean loader instead. +/// and are each resolved on first read and cached — result and +/// exception alike — for the lifetime of the instance, as HomeDirectoryProvider does for the home +/// directory. Finding the file is this class's business alone, so the path a run reports and the file it parses +/// are the same by construction rather than by agreement between callers. /// -public static class BuildvanaConfigLoader +public sealed class BuildvanaConfigProvider { private static readonly JsonDocumentOptions DocumentOptions = new() { @@ -30,31 +36,59 @@ public static class BuildvanaConfigLoader AllowTrailingCommas = true, }; + private readonly Lazy _lazyPath; + private readonly Lazy _lazyConfig; + + /// + /// Initializes a new instance of the class. + /// + /// The provider of the home directory the configuration file is looked for in. + public BuildvanaConfigProvider(IHomeDirectoryProvider home) + { + Guard.IsNotNull(home); + _lazyPath = new Lazy(() => FindFile(home)); + + // Reads the path through its own Lazy, so that a run holding both facts has probed exactly once. + _lazyConfig = new Lazy(() => LoadFile(_lazyPath.Value)); + } + /// - /// Loads the configuration file found in . + /// Gets the absolute path of the configuration file, or when the home directory + /// holds none. /// - /// The home directory to search for a configuration file. - /// The parsed configuration, or an empty if no file is present. /// - /// More than one configuration file is present (among buildvana.json, buildvana.jsonc, - /// .buildvana/buildvana.json, and .buildvana/buildvana.jsonc), or the file cannot be read. + /// Both configuration files (buildvana.json and buildvana.jsonc) are present. + /// + public string? Path => _lazyPath.Value; + + /// + /// Gets the parsed configuration, or an empty when the home directory holds + /// no configuration file. + /// + /// + /// Both configuration files (buildvana.json and buildvana.jsonc) are present, + /// or the file cannot be read. /// The file is present but not valid JSON, or does not conform to the schema; in that case /// lists each problem with its source location. /// - public static BuildvanaConfig Load(string homeDirectory) - { - Guard.IsNotNullOrEmpty(homeDirectory); - - string? path; - try - { - path = BuildvanaConfig.FindFile(homeDirectory); - } - catch (BuildvanaRuntimeException e) - { - throw new BuildFailedException(e.Message, e); - } + public BuildvanaConfig Config => _lazyConfig.Value; + /// + /// Loads the configuration file at an already-known path, bypassing both the search and the cache. + /// + /// The path of the configuration file, or for none. + /// The parsed configuration, or an empty if + /// is . + /// + /// The file cannot be read, is not valid JSON, or does not conform to the schema; in the latter cases + /// lists each problem with its source location. + /// + /// + /// For the caller that must re-read a file it has just rewritten, and therefore wants the parse this + /// instance has cached to be bypassed rather than reused. Everything else reads . + /// + public static BuildvanaConfig LoadFile(string? path) + { if (path is null) { return new BuildvanaConfig(); @@ -68,6 +102,20 @@ public static BuildvanaConfig Load(string homeDirectory) return node!.Deserialize(BuildvanaJsonContext.Default.BuildvanaConfig) ?? new BuildvanaConfig(); } + // The one probe for the configuration file: nothing outside this class asks which file a home directory + // holds, so no two callers can answer differently. + private static string? FindFile(IHomeDirectoryProvider home) + { + try + { + return BuildvanaConfig.FindFile(home.HomeDirectory); + } + catch (BuildvanaRuntimeException e) + { + throw new BuildFailedException(e.Message, e); + } + } + // Removes a leading UTF-8 byte order mark, if present, so the reader sees only JSON and positions start at 1. private static byte[] StripBom(byte[] bytes) => bytes is [0xEF, 0xBB, 0xBF, .. var rest] ? rest : bytes; diff --git a/src/Buildvana.Core.HomeDirectory/DiscoveredHomeDirectoryProvider.cs b/src/Buildvana.Core.HomeDirectory/DiscoveredHomeDirectoryProvider.cs index d869c9cf..e71301cb 100644 --- a/src/Buildvana.Core.HomeDirectory/DiscoveredHomeDirectoryProvider.cs +++ b/src/Buildvana.Core.HomeDirectory/DiscoveredHomeDirectoryProvider.cs @@ -32,5 +32,5 @@ public DiscoveredHomeDirectoryProvider(string startDirectory) protected override string Resolve() => HomeDirectoryDiscovery.TryDiscover(_startDirectory, out var homeDirectory) ? homeDirectory - : throw new BuildFailedException($"Home directory not defined (no buildvana.json[c], .buildvana/buildvana.json[c], .git, or .git/HEAD found at or above '{_startDirectory}')."); + : throw new BuildFailedException($"Home directory not defined (no buildvana.json[c], .git, or .git/HEAD found at or above '{_startDirectory}')."); } diff --git a/src/Buildvana.Core.HomeDirectory/HomeDirectoryDiscovery.cs b/src/Buildvana.Core.HomeDirectory/HomeDirectoryDiscovery.cs index 151f43a7..7fbbd173 100644 --- a/src/Buildvana.Core.HomeDirectory/HomeDirectoryDiscovery.cs +++ b/src/Buildvana.Core.HomeDirectory/HomeDirectoryDiscovery.cs @@ -11,15 +11,14 @@ namespace Buildvana.Core.HomeDirectory; /// /// Canonical implementation of the Buildvana "home directory" discovery algorithm: /// the nearest directory, starting at a given directory and walking upward, that contains any home marker — -/// a buildvana.json or buildvana.jsonc configuration file (either directly in the directory -/// or in a .buildvana subdirectory), a .git file (worktree or submodule), -/// or a .git/HEAD file (regular repository). +/// a buildvana.json or buildvana.jsonc configuration file, a .git file +/// (worktree or submodule), or a .git/HEAD file (regular repository). /// /// /// The search stops at the first directory (the start directory included) that contains any marker; -/// a configuration file only counts when it sits at that directory or in its .buildvana subdirectory. -/// A .buildvana directory without a configuration file is not a marker. Whether a configuration file -/// is actually present there — and which one — is determined separately by BuildvanaConfigLoader. +/// every marker sits in the directory it marks, so nothing under a subdirectory — the .buildvana +/// directory included — takes part in discovery. Whether a configuration file is actually present at the +/// discovered directory — and which one — is determined separately by BuildvanaConfigProvider. /// This algorithm mirrors the discovery performed by the Buildvana SDK in /// src/Buildvana.Sdk/Sdk/Sdk.props. Any change made here MUST be applied to that file as well. /// @@ -57,9 +56,7 @@ public static bool TryDiscover(string startDirectory, [MaybeNullWhen(false)] out private static bool DirectoryContainsMarker(string directory) { var hasConfigFile = File.Exists(Path.Combine(directory, BuildvanaConfig.JsonFileName)) - || File.Exists(Path.Combine(directory, BuildvanaConfig.JsoncFileName)) - || File.Exists(Path.Combine(directory, WellKnownPaths.BuildvanaDirectory, BuildvanaConfig.JsonFileName)) - || File.Exists(Path.Combine(directory, WellKnownPaths.BuildvanaDirectory, BuildvanaConfig.JsoncFileName)); + || File.Exists(Path.Combine(directory, BuildvanaConfig.JsoncFileName)); // A regular repository has a .git directory containing HEAD; a worktree or submodule has a .git file. var hasGitMarker = File.Exists(Path.Combine(directory, ".git", "HEAD")) diff --git a/src/Buildvana.Core.Versioning/VersioningSettings.cs b/src/Buildvana.Core.Versioning/VersioningSettings.cs index 4912730c..92aaee38 100644 --- a/src/Buildvana.Core.Versioning/VersioningSettings.cs +++ b/src/Buildvana.Core.Versioning/VersioningSettings.cs @@ -27,7 +27,7 @@ public VersioningSettings(BuildvanaConfig config) Guard.IsNotNull(config); PublicReleaseBranchPatterns = config.Release?.Branches ?? []; PrereleaseTag = config.Versioning?.PrereleaseTag; - AssemblyVersionPrecision = config.Versioning?.AssemblyVersionPrecision ?? AssemblyVersionPrecision.Major; + AssemblyVersionPrecision = config.Versioning.EffectiveAssemblyVersionPrecision; } /// @@ -44,7 +44,7 @@ public VersioningSettings(BuildvanaConfig config) /// /// Gets the assembly-version precision (versioning.assemblyVersionPrecision, or - /// when unset). + /// when unset). /// public AssemblyVersionPrecision AssemblyVersionPrecision { get; } diff --git a/src/Buildvana.Runtime/BuildvanaConfig-Load.cs b/src/Buildvana.Runtime/BuildvanaConfig-Load.cs index 0fa3e6df..c81a3c17 100644 --- a/src/Buildvana.Runtime/BuildvanaConfig-Load.cs +++ b/src/Buildvana.Runtime/BuildvanaConfig-Load.cs @@ -10,20 +10,20 @@ namespace Buildvana.Runtime; public partial record BuildvanaConfig { /// - /// The name of the configuration file in plain JSON form. The file may live directly in the - /// home directory or in its subdirectory. + /// The name of the configuration file in plain JSON form. The file lives in the home directory itself: + /// a configuration file elsewhere, included, is not one. /// public const string JsonFileName = "buildvana.json"; /// /// The name of the configuration file in JSON-with-comments form, subject to the same - /// two candidate locations as . + /// single candidate location as . /// public const string JsoncFileName = "buildvana.jsonc"; /// - /// Finds the configuration file in a home directory, probing the four well-known candidates - /// (buildvana.json, buildvana.jsonc, and the same names under .buildvana/). + /// Finds the configuration file in a home directory, probing the two well-known candidates + /// (buildvana.json and buildvana.jsonc). /// /// The home directory to probe; the current directory when omitted. /// The path of the configuration file, or when none exists. @@ -35,8 +35,6 @@ public partial record BuildvanaConfig [ Path.Combine(baseDirectory, JsonFileName), Path.Combine(baseDirectory, JsoncFileName), - Path.Combine(baseDirectory, WellKnownPaths.BuildvanaDirectory, JsonFileName), - Path.Combine(baseDirectory, WellKnownPaths.BuildvanaDirectory, JsoncFileName), ]; var existingPaths = Array.FindAll(candidatePaths, File.Exists); if (existingPaths.Length > 1) @@ -61,9 +59,23 @@ public partial record BuildvanaConfig /// This loader does not validate the file beyond deserialization: bv has already validated it, /// with schema-based diagnostics, before any hook runs. /// - public static BuildvanaConfig Load(string? homeDirectory = null) + public static BuildvanaConfig Load(string? homeDirectory = null) => LoadFile(FindFile(homeDirectory)); + + /// + /// Loads the configuration file at an already-known path. + /// + /// The path of the configuration file, or for none. + /// The parsed configuration, or an empty when + /// is . + /// + /// The file cannot be read, or its contents are invalid. + /// + /// + /// A hook reaches this through , which passes the path bv + /// itself read; call it directly only when the path comes from somewhere else. + /// + public static BuildvanaConfig LoadFile(string? path) { - var path = FindFile(homeDirectory); if (path is null) { return new BuildvanaConfig(); diff --git a/src/Buildvana.Runtime/DotNetConfig.cs b/src/Buildvana.Runtime/DotNetConfig.cs index 52f55689..227fca94 100644 --- a/src/Buildvana.Runtime/DotNetConfig.cs +++ b/src/Buildvana.Runtime/DotNetConfig.cs @@ -12,6 +12,12 @@ namespace Buildvana.Runtime; [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public sealed record DotNetConfig { + /// + /// The build configuration used when is not set. Read it through + /// EffectiveConfiguration, which applies it whether the setting or the whole section is absent. + /// + public const string DefaultConfiguration = "Release"; + /// /// Gets the default build configuration passed to dotnet. /// diff --git a/src/Buildvana.Runtime/DotNetConfigExtensions.cs b/src/Buildvana.Runtime/DotNetConfigExtensions.cs new file mode 100644 index 00000000..625d204d --- /dev/null +++ b/src/Buildvana.Runtime/DotNetConfigExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace Buildvana.Runtime; + +/// +/// Provides extension methods for `DotNetConfig` instances. +/// +/// +/// The receiver is nullable because a configuration file may omit the dotnet section entirely: +/// an absent section and an absent setting mean the same thing, so both resolve to the default here rather +/// than at each call site. +/// +#pragma warning disable CA1034 // Nested types should not be visible — false positive on C# 14 extension blocks; fixed in .NET 11, backport to .NET 10 requested in https://github.com/dotnet/sdk/issues/53984 +#pragma warning disable CA1708 // Identifiers should differ by more than case — false positive on classes with C# 14 extension blocks; fixed in .NET 11, https://github.com/dotnet/sdk/issues/51716 +public static class DotNetConfigExtensions +{ + extension(DotNetConfig? @this) + { + /// + /// Gets the build configuration to pass to dotnet: , + /// or when the configuration file does not set it. + /// + /// + /// The default belongs to the model rather than to any one consumer, so that bv, SDK tasks, + /// and hooks answer this question identically. + /// + public string EffectiveConfiguration => @this?.Configuration ?? DotNetConfig.DefaultConfiguration; + } +} diff --git a/src/Buildvana.Runtime/HookArgs.cs b/src/Buildvana.Runtime/HookArgs.cs index 0f3b65d4..ab3b85d4 100644 --- a/src/Buildvana.Runtime/HookArgs.cs +++ b/src/Buildvana.Runtime/HookArgs.cs @@ -32,6 +32,20 @@ public abstract record HookArgs [JsonPropertyOrder(-1)] public required RuntimeInfo RuntimeInfo { get; init; } + /// + /// Loads the Buildvana configuration from the file bv read for this run + /// (). + /// + /// The parsed configuration, or an empty when the repository has + /// no configuration file. + /// The file cannot be read, or its contents are invalid. + /// + /// Which file to read comes from the args, so a hook never has to search for one; what the file says + /// is read from disk at the moment of the call, so a hook sees the file as it stands even after an earlier + /// hook in the same run has rewritten it. + /// + public BuildvanaConfig LoadConfig() => BuildvanaConfig.LoadFile(RuntimeInfo.ConfigFile); + /// /// Loads the args of the current run of the hook identified by /// from the hook's args file. diff --git a/src/Buildvana.Runtime/PublicAPI.Unshipped.txt b/src/Buildvana.Runtime/PublicAPI.Unshipped.txt index 7f9b7e10..7f8344d0 100644 --- a/src/Buildvana.Runtime/PublicAPI.Unshipped.txt +++ b/src/Buildvana.Runtime/PublicAPI.Unshipped.txt @@ -50,6 +50,9 @@ Buildvana.Runtime.DotNetConfig.Restore.get -> Buildvana.Runtime.DotNetInvocation Buildvana.Runtime.DotNetConfig.Restore.init -> void Buildvana.Runtime.DotNetConfig.Test.get -> Buildvana.Runtime.DotNetInvocationConfig? Buildvana.Runtime.DotNetConfig.Test.init -> void +Buildvana.Runtime.DotNetConfigExtensions +Buildvana.Runtime.DotNetConfigExtensions.extension(Buildvana.Runtime.DotNetConfig?) +Buildvana.Runtime.DotNetConfigExtensions.extension(Buildvana.Runtime.DotNetConfig?).EffectiveConfiguration.get -> string! Buildvana.Runtime.DotNetInvocationConfig Buildvana.Runtime.DotNetInvocationConfig.$() -> Buildvana.Runtime.DotNetInvocationConfig! Buildvana.Runtime.DotNetInvocationConfig.Args.get -> System.Collections.Generic.IReadOnlyList? @@ -81,6 +84,7 @@ Buildvana.Runtime.GitIdentityConfig.Name.init -> void Buildvana.Runtime.HookArgs Buildvana.Runtime.HookArgs.HookArgs() -> void Buildvana.Runtime.HookArgs.HookArgs(Buildvana.Runtime.HookArgs! original) -> void +Buildvana.Runtime.HookArgs.LoadConfig() -> Buildvana.Runtime.BuildvanaConfig! Buildvana.Runtime.HookArgs.RuntimeInfo.get -> Buildvana.Runtime.RuntimeInfo! Buildvana.Runtime.HookArgs.RuntimeInfo.init -> void Buildvana.Runtime.IHookEvent @@ -151,6 +155,8 @@ Buildvana.Runtime.RuntimeInfo Buildvana.Runtime.RuntimeInfo.$() -> Buildvana.Runtime.RuntimeInfo! Buildvana.Runtime.RuntimeInfo.ArtifactsDirectory.get -> string! Buildvana.Runtime.RuntimeInfo.ArtifactsDirectory.init -> void +Buildvana.Runtime.RuntimeInfo.ConfigFile.get -> string? +Buildvana.Runtime.RuntimeInfo.ConfigFile.init -> void Buildvana.Runtime.RuntimeInfo.DelegatingVersion.get -> string? Buildvana.Runtime.RuntimeInfo.DelegatingVersion.init -> void Buildvana.Runtime.RuntimeInfo.Equals(Buildvana.Runtime.RuntimeInfo? other) -> bool @@ -169,12 +175,17 @@ Buildvana.Runtime.VersioningConfig.Equals(Buildvana.Runtime.VersioningConfig? ot Buildvana.Runtime.VersioningConfig.PrereleaseTag.get -> string? Buildvana.Runtime.VersioningConfig.PrereleaseTag.init -> void Buildvana.Runtime.VersioningConfig.VersioningConfig() -> void +Buildvana.Runtime.VersioningConfigExtensions +Buildvana.Runtime.VersioningConfigExtensions.extension(Buildvana.Runtime.VersioningConfig?) +Buildvana.Runtime.VersioningConfigExtensions.extension(Buildvana.Runtime.VersioningConfig?).EffectiveAssemblyVersionPrecision.get -> Buildvana.Runtime.AssemblyVersionPrecision Buildvana.Runtime.WellKnownPaths abstract Buildvana.Runtime.HookArgs.$() -> Buildvana.Runtime.HookArgs! const Buildvana.Runtime.BuildvanaConfig.JsonFileName = "buildvana.json" -> string! const Buildvana.Runtime.BuildvanaConfig.JsoncFileName = "buildvana.jsonc" -> string! +const Buildvana.Runtime.DotNetConfig.DefaultConfiguration = "Release" -> string! const Buildvana.Runtime.PostReleaseHookArgs.Context = "release" -> string! const Buildvana.Runtime.PostReleaseHookArgs.Event = "post-release" -> string! +const Buildvana.Runtime.VersioningConfig.DefaultAssemblyVersionPrecision = Buildvana.Runtime.AssemblyVersionPrecision.Major -> Buildvana.Runtime.AssemblyVersionPrecision const Buildvana.Runtime.WellKnownPaths.BuildvanaDirectory = ".buildvana" -> string! const Buildvana.Runtime.WellKnownPaths.HookArgsDirectory = ".buildvana-temp/hook-args" -> string! const Buildvana.Runtime.WellKnownPaths.HooksDirectory = ".buildvana/hooks" -> string! @@ -229,11 +240,13 @@ override Buildvana.Runtime.VersioningConfig.ToString() -> string! override sealed Buildvana.Runtime.PostReleaseHookArgs.Equals(Buildvana.Runtime.HookArgs? other) -> bool static Buildvana.Runtime.BuildvanaConfig.FindFile(string? homeDirectory = null) -> string? static Buildvana.Runtime.BuildvanaConfig.Load(string? homeDirectory = null) -> Buildvana.Runtime.BuildvanaConfig! +static Buildvana.Runtime.BuildvanaConfig.LoadFile(string? path) -> Buildvana.Runtime.BuildvanaConfig! static Buildvana.Runtime.BuildvanaConfig.operator !=(Buildvana.Runtime.BuildvanaConfig? left, Buildvana.Runtime.BuildvanaConfig? right) -> bool static Buildvana.Runtime.BuildvanaConfig.operator ==(Buildvana.Runtime.BuildvanaConfig? left, Buildvana.Runtime.BuildvanaConfig? right) -> bool static Buildvana.Runtime.BuildvanaJsonContext.Default.get -> Buildvana.Runtime.BuildvanaJsonContext! static Buildvana.Runtime.DotNetConfig.operator !=(Buildvana.Runtime.DotNetConfig? left, Buildvana.Runtime.DotNetConfig? right) -> bool static Buildvana.Runtime.DotNetConfig.operator ==(Buildvana.Runtime.DotNetConfig? left, Buildvana.Runtime.DotNetConfig? right) -> bool +static Buildvana.Runtime.DotNetConfigExtensions.get_EffectiveConfiguration(Buildvana.Runtime.DotNetConfig? this) -> string! static Buildvana.Runtime.DotNetInvocationConfig.operator !=(Buildvana.Runtime.DotNetInvocationConfig? left, Buildvana.Runtime.DotNetInvocationConfig? right) -> bool static Buildvana.Runtime.DotNetInvocationConfig.operator ==(Buildvana.Runtime.DotNetInvocationConfig? left, Buildvana.Runtime.DotNetInvocationConfig? right) -> bool static Buildvana.Runtime.GitConfig.operator !=(Buildvana.Runtime.GitConfig? left, Buildvana.Runtime.GitConfig? right) -> bool @@ -262,6 +275,7 @@ static Buildvana.Runtime.RuntimeInfo.operator !=(Buildvana.Runtime.RuntimeInfo? static Buildvana.Runtime.RuntimeInfo.operator ==(Buildvana.Runtime.RuntimeInfo? left, Buildvana.Runtime.RuntimeInfo? right) -> bool static Buildvana.Runtime.VersioningConfig.operator !=(Buildvana.Runtime.VersioningConfig? left, Buildvana.Runtime.VersioningConfig? right) -> bool static Buildvana.Runtime.VersioningConfig.operator ==(Buildvana.Runtime.VersioningConfig? left, Buildvana.Runtime.VersioningConfig? right) -> bool +static Buildvana.Runtime.VersioningConfigExtensions.get_EffectiveAssemblyVersionPrecision(Buildvana.Runtime.VersioningConfig? this) -> Buildvana.Runtime.AssemblyVersionPrecision static Buildvana.Runtime.WellKnownPaths.GetHookArgsFile(string! context, string! event) -> string! static Buildvana.Runtime.WellKnownPaths.GetHookFile(string! context, string! event) -> string! virtual Buildvana.Runtime.HookArgs.EqualityContract.get -> System.Type! diff --git a/src/Buildvana.Runtime/RuntimeInfo.cs b/src/Buildvana.Runtime/RuntimeInfo.cs index 9db21f02..4778675c 100644 --- a/src/Buildvana.Runtime/RuntimeInfo.cs +++ b/src/Buildvana.Runtime/RuntimeInfo.cs @@ -7,7 +7,8 @@ namespace Buildvana.Runtime; /// /// Run-time information about the bv run a hook belongs to: the running version, how the run was -/// launched, and the absolute paths of the run's well-known directories. Shared by every hook's args. +/// launched, and the absolute paths of the run's well-known directories and configuration file. +/// Shared by every hook's args. /// [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public sealed record RuntimeInfo @@ -39,4 +40,19 @@ public sealed record RuntimeInfo /// where hooks can write temporary files without affecting working-tree change detection. /// public required string ScratchDirectory { get; init; } + + /// + /// Gets the absolute path of the configuration file this run read ( + /// or ), or when the repository has none. + /// + /// + /// A hook that reads settings gets there through , which reads the + /// file named here and hands back the typed configuration. This member is for a hook that works on the file + /// itself — rewriting a value, checking it into the post-release commit — and must act on the very file + /// bv read rather than guess which one it was. + /// Required of whoever writes the args, so that a run always states which file it read; the value is + /// nonetheless when the repository has no configuration file, which a repository + /// whose home directory is marked by Git alone legitimately does not. + /// + public required string? ConfigFile { get; init; } } diff --git a/src/Buildvana.Runtime/VersioningConfig.cs b/src/Buildvana.Runtime/VersioningConfig.cs index 190f84b0..e029ac8e 100644 --- a/src/Buildvana.Runtime/VersioningConfig.cs +++ b/src/Buildvana.Runtime/VersioningConfig.cs @@ -12,6 +12,14 @@ namespace Buildvana.Runtime; [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public sealed record VersioningConfig { + /// + /// The assembly-version precision used when is not set. Read it + /// through EffectiveAssemblyVersionPrecision, which applies it whether the setting or the whole + /// section is absent. + /// + public const AssemblyVersionPrecision DefaultAssemblyVersionPrecision + = Buildvana.Runtime.AssemblyVersionPrecision.Major; + /// Gets the prerelease tag applied to prerelease versions. [Description("Prerelease tag applied to prerelease versions. When omitted, prerelease versions are not allowed.")] public string? PrereleaseTag { get; init; } diff --git a/src/Buildvana.Runtime/VersioningConfigExtensions.cs b/src/Buildvana.Runtime/VersioningConfigExtensions.cs new file mode 100644 index 00000000..98b9f2e8 --- /dev/null +++ b/src/Buildvana.Runtime/VersioningConfigExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace Buildvana.Runtime; + +/// +/// Provides extension methods for `VersioningConfig` instances. +/// +/// +/// The receiver is nullable because a configuration file may omit the versioning section entirely: +/// an absent section and an absent setting mean the same thing, so both resolve to the default here rather +/// than at each call site. +/// +#pragma warning disable CA1034 // Nested types should not be visible — false positive on C# 14 extension blocks; fixed in .NET 11, backport to .NET 10 requested in https://github.com/dotnet/sdk/issues/53984 +#pragma warning disable CA1708 // Identifiers should differ by more than case — false positive on classes with C# 14 extension blocks; fixed in .NET 11, https://github.com/dotnet/sdk/issues/51716 +public static class VersioningConfigExtensions +{ + extension(VersioningConfig? @this) + { + /// + /// Gets how much of the computed version goes into the assembly version: + /// , or + /// when the configuration file + /// does not set it. + /// + /// + /// The default belongs to the model rather than to any one consumer, so that bv, SDK tasks, + /// and hooks answer this question identically. + /// + public AssemblyVersionPrecision EffectiveAssemblyVersionPrecision + => @this?.AssemblyVersionPrecision ?? VersioningConfig.DefaultAssemblyVersionPrecision; + } +} diff --git a/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs index 04658ea5..489d47ce 100644 --- a/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs +++ b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs @@ -38,10 +38,11 @@ private static CachedVersion GetOrComputeVersion(string homeDirectory, IReporter private static CachedVersion ComputeCore(string homeDirectory, IReporter reporter, string? fingerprint) { + var home = new FixedHomeDirectoryProvider(homeDirectory); var service = new VersioningService( reporter, - new FixedHomeDirectoryProvider(homeDirectory), - new VersioningSettings(BuildvanaConfigLoader.Load(homeDirectory)), + home, + new VersioningSettings(new BuildvanaConfigProvider(home).Config), new GitHeightCalculator(VersionFile.FileName)); return new CachedVersion( fingerprint, diff --git a/src/Buildvana.Sdk/Sdk/Sdk.props b/src/Buildvana.Sdk/Sdk/Sdk.props index e972cbbb..8779b54f 100644 --- a/src/Buildvana.Sdk/Sdk/Sdk.props +++ b/src/Buildvana.Sdk/Sdk/Sdk.props @@ -20,12 +20,14 @@ Rules for HomeDirectory: Walking upward from the project's directory (the project's directory included), find the NEAREST directory containing any of these home markers, and use it as the home directory: - * a Buildvana configuration file (buildvana.json or buildvana.jsonc), either directly in the directory - or in a .buildvana subdirectory (a .buildvana directory without a configuration file is NOT a marker); + * a Buildvana configuration file (buildvana.json or buildvana.jsonc); * a Git worktree or submodule (a file named .git); * a regular Git repository (a file named HEAD in a .git subdirectory). "Nearest" means the deepest candidate: since every candidate is the project's directory or an ancestor of it, the candidate with the longest path is the closest one. + Every marker sits in the directory it marks, so nothing under a subdirectory — the .buildvana directory + included — takes part in discovery. That directory holds hooks, which are projects themselves: were a marker + to be recognized inside it, every hook would discover .buildvana as its own home directory. The configuration file is only a marker for the purpose of locating the home directory; the SDK does not parse it here. Whether a configuration file is actually present at the home directory is the loader's concern. @@ -38,15 +40,10 @@ NOTE: Check for ".git/HEAD", not ".git\HEAD", because the latter only works on Windows. MSBuild doesn't convert directory separators on the second argument to GetDirectoryNameOfFileAbove, probably due to https://github.com/microsoft/msbuild/issues/1024 - The same applies to the ".buildvana/buildvana.json" and ".buildvana/buildvana.jsonc" probes. - For subdirectory probes, GetDirectoryNameOfFileAbove returns the directory CONTAINING the subdirectory, - i.e. the probe result itself is the home directory. --> <_HomeDir_ByConfigJson>$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)','buildvana.json')) <_HomeDir_ByConfigJsonc>$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)','buildvana.jsonc')) - <_HomeDir_BySubdirConfigJson>$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)','.buildvana/buildvana.json')) - <_HomeDir_BySubdirConfigJsonc>$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)','.buildvana/buildvana.jsonc')) <_HomeDir_ByGitFile>$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)','.git')) <_HomeDir_ByGitHead>$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)','.git/HEAD')) @@ -56,8 +53,6 @@ $(_HomeDir_ByConfigJson) $(_HomeDir_ByConfigJsonc) - $(_HomeDir_BySubdirConfigJson) - $(_HomeDir_BySubdirConfigJsonc) $(_HomeDir_ByGitFile) $(_HomeDir_ByGitHead) @@ -74,12 +69,10 @@ Text="Home directory not defined." /> - + <_BV_FoundConfigFile Condition="Exists('$(HomeDirectory)buildvana.json')" Include="$(HomeDirectory)buildvana.json" /> <_BV_FoundConfigFile Condition="Exists('$(HomeDirectory)buildvana.jsonc')" Include="$(HomeDirectory)buildvana.jsonc" /> - <_BV_FoundConfigFile Condition="Exists('$(HomeDirectory).buildvana/buildvana.json')" Include="$(HomeDirectory).buildvana/buildvana.json" /> - <_BV_FoundConfigFile Condition="Exists('$(HomeDirectory).buildvana/buildvana.jsonc')" Include="$(HomeDirectory).buildvana/buildvana.jsonc" /> ())) .AddSingleton(static sp => UpdateSettings.Parse(sp.GetRequiredService().Options)) - // Lazy by design: this factory (and thus discovery, parsing, and validation) runs on first resolve. - // A malformed buildvana.json stays inert until a consumer (e.g. DotNetSettings or ReleaseSettings) reads it. - .AddSingleton(static sp => BuildvanaConfigLoader.Load(sp.GetRequiredService().HomeDirectory)) + // Lazy by design: the provider finds, parses, and validates the file on first read of what is asked + // of it. A malformed buildvana.json stays inert until a consumer (e.g. DotNetSettings or + // ReleaseSettings) reads the configuration. Registering the parsed configuration separately keeps + // those consumers depending on the data alone, while the provider answers whoever needs the path. + .AddSingleton() + .AddSingleton(static sp => sp.GetRequiredService().Config) .AddSingleton() .AddSingleton() .AddSingleton() @@ -89,6 +92,7 @@ public IServiceCollection AddBvServices() .AddSingleton(static sp => new SelfVersionService( sp.GetRequiredService(), sp.GetRequiredService(), + sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), OwnVersion.Value)); diff --git a/src/Buildvana.Tool/Services/DotNetSettings.cs b/src/Buildvana.Tool/Services/DotNetSettings.cs index 84567ef1..dff9d84a 100644 --- a/src/Buildvana.Tool/Services/DotNetSettings.cs +++ b/src/Buildvana.Tool/Services/DotNetSettings.cs @@ -16,11 +16,6 @@ namespace Buildvana.Tool.Services; /// internal sealed class DotNetSettings { - /// - /// The build configuration used when neither the configuration chain nor the command line specifies one. - /// - public const string DefaultConfiguration = "Release"; - private readonly NuGetConfig? _nuget; /// @@ -30,12 +25,15 @@ internal sealed class DotNetSettings public DotNetSettings(BuildvanaConfig config) { Guard.IsNotNull(config); - Configuration = config.DotNet?.Configuration ?? DefaultConfiguration; + Configuration = config.DotNet.EffectiveConfiguration; Invocations = new DotNetInvocationsSettings(config.DotNet); _nuget = config.NuGet; } - /// Gets the default build configuration (dotnet.configuration, or "Release"). + /// + /// Gets the default build configuration (dotnet.configuration, or + /// when unset). + /// public string Configuration { get; } /// Gets the per-command dotnet invocation settings (the dotnet section). diff --git a/src/Buildvana.Tool/Services/Hooks/HookArgsFactory`1.cs b/src/Buildvana.Tool/Services/Hooks/HookArgsFactory`1.cs index 8c0f45ac..ec2b2dc3 100644 --- a/src/Buildvana.Tool/Services/Hooks/HookArgsFactory`1.cs +++ b/src/Buildvana.Tool/Services/Hooks/HookArgsFactory`1.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using Buildvana.Core.Configuration; using Buildvana.Core.HomeDirectory; using Buildvana.Runtime; using Buildvana.Tool.Infrastructure; @@ -19,13 +20,14 @@ namespace Buildvana.Tool.Services.Hooks; /// /// The type of the hook args the factory creates. /// The home directory provider. -internal abstract class HookArgsFactory(IHomeDirectoryProvider home) +/// The provider of the configuration file this run reads. +internal abstract class HookArgsFactory(IHomeDirectoryProvider home, BuildvanaConfigProvider config) where TArgs : HookArgs, IHookEvent { /// /// Creates the section shared by every hook's args: /// the running bv's version, the delegating bv's version when the run was delegated, and the - /// absolute paths of the run's well-known directories. + /// absolute paths of the run's well-known directories and configuration file. /// /// The path of the directory containing the build artifacts, /// either absolute or relative to the home directory. @@ -47,6 +49,10 @@ protected RuntimeInfo CreateRuntimeInfo(string artifactsPath) HomeDirectory = Path.TrimEndingDirectorySeparator(home.HomeDirectory), ArtifactsDirectory = home.GetFullPath(artifactsPath), ScratchDirectory = home.GetFullPath(CommonPaths.Scratch), + + // Already absolute, and already the file this run read: hooks are told which one it is instead of + // running their own search and possibly landing on a different answer. + ConfigFile = config.Path, }; } } diff --git a/src/Buildvana.Tool/Services/Hooks/PostReleaseHookArgsFactory.cs b/src/Buildvana.Tool/Services/Hooks/PostReleaseHookArgsFactory.cs index 58d2a0fc..dfe0f9dd 100644 --- a/src/Buildvana.Tool/Services/Hooks/PostReleaseHookArgsFactory.cs +++ b/src/Buildvana.Tool/Services/Hooks/PostReleaseHookArgsFactory.cs @@ -2,6 +2,7 @@ // See the LICENSE file in the project root for full license information. using System.Collections.Generic; +using Buildvana.Core.Configuration; using Buildvana.Core.HomeDirectory; using Buildvana.Runtime; using CommunityToolkit.Diagnostics; @@ -14,7 +15,9 @@ namespace Buildvana.Tool.Services.Hooks; /// (see ). /// /// The home directory provider. -internal sealed class PostReleaseHookArgsFactory(IHomeDirectoryProvider home) : HookArgsFactory(home) +/// The provider of the configuration file this run reads. +internal sealed class PostReleaseHookArgsFactory(IHomeDirectoryProvider home, BuildvanaConfigProvider config) + : HookArgsFactory(home, config) { /// /// Creates the args for a release/post-release hook run. diff --git a/src/Buildvana.Tool/Services/SelfVersionService.cs b/src/Buildvana.Tool/Services/SelfVersionService.cs index 440ff0bf..d2b0286f 100644 --- a/src/Buildvana.Tool/Services/SelfVersionService.cs +++ b/src/Buildvana.Tool/Services/SelfVersionService.cs @@ -15,7 +15,6 @@ using Buildvana.Core.IO; using Buildvana.Core.Json; using Buildvana.Core.Process; -using Buildvana.Runtime; using Buildvana.Tool.Utilities; using CommunityToolkit.Diagnostics; using NuGet.Versioning; @@ -45,6 +44,7 @@ internal sealed partial class SelfVersionService private readonly IReporter _reporter; private readonly IHomeDirectoryProvider _home; + private readonly BuildvanaConfigProvider _config; private readonly IJsonHelper _jsonHelper; private readonly IProcessRunner _processRunner; private readonly NuGetVersion _ownVersion; @@ -54,23 +54,27 @@ internal sealed partial class SelfVersionService /// /// The reporter to log to. /// The provider of the home directory, where global.json and the tool manifest live. + /// The provider of the configuration file whose schema reference is rewritten. /// The JSON helper used to read and rewrite pins. /// The process runner used to invoke dotnet tool update. /// The version of the running bv. public SelfVersionService( IReporter reporter, IHomeDirectoryProvider home, + BuildvanaConfigProvider config, IJsonHelper jsonHelper, IProcessRunner processRunner, NuGetVersion ownVersion) { Guard.IsNotNull(reporter); Guard.IsNotNull(home); + Guard.IsNotNull(config); Guard.IsNotNull(jsonHelper); Guard.IsNotNull(processRunner); Guard.IsNotNull(ownVersion); _reporter = reporter; _home = home; + _config = config; _jsonHelper = jsonHelper; _processRunner = processRunner; _ownVersion = ownVersion; @@ -287,20 +291,11 @@ private string UpdateGlobalJson(string? currentPinText, NuGetVersion? currentPin } // Rewrites the version segment of the configuration file's $schema URL in place, when the URL has the - // well-known shape; a hand-rolled or absent reference is reported, not touched. Runs on whichever of the - // four candidate locations holds the configuration file; no file at all means nothing to update or validate. + // well-known shape; a hand-rolled or absent reference is reported, not touched. Runs on whichever candidate + // the home directory holds; no file at all means nothing to update or validate. private string? UpdateConfigSchemaReference() { - string? path; - try - { - path = BuildvanaConfig.FindFile(_home.HomeDirectory); - } - catch (BuildvanaRuntimeException e) - { - throw new BuildFailedException(e.Message, e); - } - + var path = _config.Path; if (path is null) { return null; @@ -324,18 +319,20 @@ private string UpdateGlobalJson(string? currentPinText, NuGetVersion? currentPin : changed ? $"{fileName}: schema reference updated" : SchemaUrlRegex.IsMatch(schemaReference) ? $"{fileName}: schema reference unchanged" : $"{fileName}: schema reference not recognized, left unchanged"; - ReportConfigValidationProblems(fileName); + ReportConfigValidationProblems(path, fileName); return line; } // The configuration file's content may predate this bv's model; loading it with this version's validating // loader turns the drift into actionable diagnostics. Problems are warnings, not errors: the file keeps // working for the commands that do not read it, and the user decides how to migrate it. - private void ReportConfigValidationProblems(string fileName) + // The file has just been rewritten, so this reads it afresh rather than through the provider, whose parse + // (if a command in this run asked for one) predates the rewrite. + private void ReportConfigValidationProblems(string path, string fileName) { try { - _ = BuildvanaConfigLoader.Load(_home.HomeDirectory); + _ = BuildvanaConfigProvider.LoadFile(path); } catch (BuildFailedException e) { diff --git a/tests/Buildvana.Core.Configuration.Tests/Buildvana.Core.Configuration.Tests.csproj b/tests/Buildvana.Core.Configuration.Tests/Buildvana.Core.Configuration.Tests.csproj index 35eff46e..8046105f 100644 --- a/tests/Buildvana.Core.Configuration.Tests/Buildvana.Core.Configuration.Tests.csproj +++ b/tests/Buildvana.Core.Configuration.Tests/Buildvana.Core.Configuration.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/tests/Buildvana.Core.Configuration.Tests/BuildvanaConfigLoaderTests.cs b/tests/Buildvana.Core.Configuration.Tests/BuildvanaConfigProviderTests.cs similarity index 60% rename from tests/Buildvana.Core.Configuration.Tests/BuildvanaConfigLoaderTests.cs rename to tests/Buildvana.Core.Configuration.Tests/BuildvanaConfigProviderTests.cs index 469510ce..bb6f2cd6 100644 --- a/tests/Buildvana.Core.Configuration.Tests/BuildvanaConfigLoaderTests.cs +++ b/tests/Buildvana.Core.Configuration.Tests/BuildvanaConfigProviderTests.cs @@ -4,16 +4,17 @@ using System.Text; using Buildvana.Core; using Buildvana.Core.Configuration; +using Buildvana.Core.HomeDirectory; -internal sealed class BuildvanaConfigLoaderTests +internal sealed class BuildvanaConfigProviderTests { [Test] - public async Task Load_NoFile_ReturnsEmptyConfig() + public async Task Config_NoFile_IsEmptyConfig() { var dir = NewDir(); try { - var config = BuildvanaConfigLoader.Load(dir); + var config = NewProvider(dir).Config; await Assert.That(config.Release).IsNull(); } finally @@ -23,13 +24,13 @@ public async Task Load_NoFile_ReturnsEmptyConfig() } [Test] - public async Task Load_ValidConfig_Loads() + public async Task Config_ValidConfig_Loads() { var dir = NewDir(); try { Write(dir, "buildvana.jsonc", """{ "release": { "branches": ["main"] } }"""); - var config = BuildvanaConfigLoader.Load(dir); + var config = NewProvider(dir).Config; await Assert.That(config.Release!.Branches!.Count).IsEqualTo(1); } finally @@ -41,13 +42,13 @@ public async Task Load_ValidConfig_Loads() // An empty JSON object is a valid configuration; it makes a configuration file usable // as a pure home-directory marker (the replacement for the retired .buildvana-home file). [Test] - public async Task Load_EmptyObject_Loads() + public async Task Config_EmptyObject_Loads() { var dir = NewDir(); try { Write(dir, "buildvana.json", "{}"); - var config = BuildvanaConfigLoader.Load(dir); + var config = NewProvider(dir).Config; await Assert.That(config.Release).IsNull(); } finally @@ -57,14 +58,12 @@ public async Task Load_EmptyObject_Loads() } [Test] - public async Task Load_ConfigInSubdirectory_Loads() + public async Task Path_NoFile_IsNull() { var dir = NewDir(); try { - Write(dir, Path.Combine(".buildvana", "buildvana.jsonc"), """{ "release": { "branches": ["main"] } }"""); - var config = BuildvanaConfigLoader.Load(dir); - await Assert.That(config.Release!.Branches!.Count).IsEqualTo(1); + await Assert.That(NewProvider(dir).Path).IsNull(); } finally { @@ -73,16 +72,15 @@ public async Task Load_ConfigInSubdirectory_Loads() } [Test] - public async Task Load_BothFilesPresent_ThrowsWithoutDiagnostics() + [Arguments("buildvana.json")] + [Arguments("buildvana.jsonc")] + public async Task Path_OneFile_IsItsPath(string fileName) { var dir = NewDir(); try { - Write(dir, "buildvana.json", "{}"); - Write(dir, "buildvana.jsonc", "{}"); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Diagnostics.Count).IsEqualTo(0); + Write(dir, fileName, "{}"); + await Assert.That(NewProvider(dir).Path).IsEqualTo(Path.Combine(dir, fileName)); } finally { @@ -90,18 +88,61 @@ public async Task Load_BothFilesPresent_ThrowsWithoutDiagnostics() } } + // Deleting the file between the two reads is what proves the answer is cached rather than recomputed: + // one run gets one answer, whichever of the provider's two facts is asked for first. [Test] - public async Task Load_FilesInRootAndSubdirectory_ThrowsNamingAllOffenders() + public async Task PathAndConfig_ReadAfterFileRemoval_KeepTheFirstAnswer() { var dir = NewDir(); try { + Write(dir, "buildvana.jsonc", """{ "release": { "branches": ["main"] } }"""); + var provider = NewProvider(dir); + var path = provider.Path; + var config = provider.Config; + + File.Delete(Path.Combine(dir, "buildvana.jsonc")); + + await Assert.That(provider.Path).IsEqualTo(path); + await Assert.That(provider.Config).IsSameReferenceAs(config); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + + // Reading the configuration without having read the path first must find the file just the same: + // the two are resolved independently on demand, from a single probe. + [Test] + public async Task Config_ReadBeforePath_FindsTheFile() + { + var dir = NewDir(); + try + { + Write(dir, "buildvana.jsonc", """{ "release": { "branches": ["main"] } }"""); + var provider = NewProvider(dir); + + await Assert.That(provider.Config.Release!.Branches!.Count).IsEqualTo(1); + await Assert.That(provider.Path).IsEqualTo(Path.Combine(dir, "buildvana.jsonc")); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + + [Test] + public async Task Path_BothFilesPresent_Throws() + { + var dir = NewDir(); + try + { + Write(dir, "buildvana.json", "{}"); Write(dir, "buildvana.jsonc", "{}"); - Write(dir, Path.Combine(".buildvana", "buildvana.json"), "{}"); - var exception = Catch(dir); - await Assert.That(exception).IsNotNull(); - await Assert.That(exception!.Message).Contains(Path.Combine(dir, "buildvana.jsonc")); - await Assert.That(exception.Message).Contains(Path.Combine(dir, ".buildvana", "buildvana.json")); + var provider = NewProvider(dir); + + _ = await Assert.That(() => provider.Path).Throws(); } finally { @@ -110,15 +151,18 @@ public async Task Load_FilesInRootAndSubdirectory_ThrowsNamingAllOffenders() } [Test] - public async Task Load_BothFilesInSubdirectory_Throws() + public async Task Config_BothFilesPresent_ThrowsNamingBothWithoutDiagnostics() { var dir = NewDir(); try { - Write(dir, Path.Combine(".buildvana", "buildvana.json"), "{}"); - Write(dir, Path.Combine(".buildvana", "buildvana.jsonc"), "{}"); + Write(dir, "buildvana.json", "{}"); + Write(dir, "buildvana.jsonc", "{}"); var exception = Catch(dir); await Assert.That(exception).IsNotNull(); + await Assert.That(exception!.Message).Contains(Path.Combine(dir, "buildvana.json")); + await Assert.That(exception.Message).Contains(Path.Combine(dir, "buildvana.jsonc")); + await Assert.That(exception.Diagnostics.Count).IsEqualTo(0); } finally { @@ -127,7 +171,7 @@ public async Task Load_BothFilesInSubdirectory_Throws() } [Test] - public async Task Load_InvalidJson_ReportsBV1100() + public async Task Config_InvalidJson_ReportsBV1100() { var dir = NewDir(); try @@ -145,7 +189,7 @@ public async Task Load_InvalidJson_ReportsBV1100() } [Test] - public async Task Load_SchemaViolation_ReportsCodeAndPosition() + public async Task Config_SchemaViolation_ReportsCodeAndPosition() { var dir = NewDir(); try @@ -164,7 +208,7 @@ public async Task Load_SchemaViolation_ReportsCodeAndPosition() } [Test] - public async Task Load_UnknownProperty_ReportsBV1103() + public async Task Config_UnknownProperty_ReportsBV1103() { var dir = NewDir(); try @@ -181,7 +225,7 @@ public async Task Load_UnknownProperty_ReportsBV1103() } [Test] - public async Task Load_BomPrefixedFile_DoesNotOffsetPositions() + public async Task Config_BomPrefixedFile_DoesNotOffsetPositions() { var dir = NewDir(); try @@ -197,6 +241,8 @@ public async Task Load_BomPrefixedFile_DoesNotOffsetPositions() } } + private static BuildvanaConfigProvider NewProvider(string dir) => new(new FixedHomeDirectoryProvider(dir)); + private static string NewDir() { var dir = Path.Combine(Path.GetTempPath(), "bvtest_" + Guid.NewGuid().ToString("N")); @@ -215,7 +261,7 @@ private static void Write(string dir, string fileName, string content, bool bom { try { - _ = BuildvanaConfigLoader.Load(dir); + _ = NewProvider(dir).Config; return null; } catch (BuildFailedException exception) diff --git a/tests/Buildvana.Core.HomeDirectory.Tests/HomeDirectoryDiscoveryTests.cs b/tests/Buildvana.Core.HomeDirectory.Tests/HomeDirectoryDiscoveryTests.cs index b22ffca8..8d293d7c 100644 --- a/tests/Buildvana.Core.HomeDirectory.Tests/HomeDirectoryDiscoveryTests.cs +++ b/tests/Buildvana.Core.HomeDirectory.Tests/HomeDirectoryDiscoveryTests.cs @@ -8,8 +8,6 @@ internal sealed class HomeDirectoryDiscoveryTests [Test] [Arguments("buildvana.json")] [Arguments("buildvana.jsonc")] - [Arguments(".buildvana/buildvana.json")] - [Arguments(".buildvana/buildvana.jsonc")] [Arguments(".git")] [Arguments(".git/HEAD")] public async Task TryDiscover_MarkerInStartDirectory_MarksIt(string marker) @@ -28,14 +26,16 @@ public async Task TryDiscover_MarkerInStartDirectory_MarksIt(string marker) } } + // Hooks are projects living under .buildvana/, so discovery runs from there on every hook build. + // Nothing inside that directory is a marker, which is what keeps a hook's home directory the repository's own. [Test] - public async Task TryDiscover_ConfigInSubdirectory_MarksContainingDirectory() + public async Task TryDiscover_StartingUnderBuildvanaDirectory_FindsHomeDirectory() { var root = NewDir(); try { - WriteFile(root, ".buildvana/buildvana.jsonc"); - var start = Path.Combine(root, "src", "MyProject"); + WriteFile(root, "buildvana.jsonc"); + var start = Path.Combine(root, ".buildvana", "hooks", "release"); _ = Directory.CreateDirectory(start); var found = HomeDirectoryDiscovery.TryDiscover(start, out var home); await Assert.That(found).IsTrue(); @@ -48,14 +48,14 @@ public async Task TryDiscover_ConfigInSubdirectory_MarksContainingDirectory() } [Test] - public async Task TryDiscover_BareSubdirectory_IsNotMarker() + public async Task TryDiscover_BuildvanaDirectory_IsNotMarker() { var root = NewDir(); try { WriteFile(root, ".git/HEAD"); var child = Path.Combine(root, "child"); - _ = Directory.CreateDirectory(Path.Combine(child, ".buildvana")); + WriteFile(child, ".buildvana/hooks/release/post-release.cs"); var found = HomeDirectoryDiscovery.TryDiscover(child, out var home); await Assert.That(found).IsTrue(); await Assert.That(home).IsEqualTo(WithTrailingSeparator(root)); @@ -66,6 +66,27 @@ public async Task TryDiscover_BareSubdirectory_IsNotMarker() } } + // A configuration file marks the directory it sits in, and only that one. This is the rule that used to have + // an exception for .buildvana/, and the exception is what made every hook's own directory a home directory. + [Test] + public async Task TryDiscover_ConfigFileInBuildvanaSubdirectory_DoesNotMarkTheParent() + { + var root = NewDir(); + try + { + WriteFile(root, ".git/HEAD"); + var nested = Path.Combine(root, "nested"); + WriteFile(nested, ".buildvana/buildvana.jsonc"); + var found = HomeDirectoryDiscovery.TryDiscover(nested, out var home); + await Assert.That(found).IsTrue(); + await Assert.That(home).IsEqualTo(WithTrailingSeparator(root)); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + [Test] public async Task TryDiscover_MultipleMarkers_NearestWins() { @@ -74,7 +95,7 @@ public async Task TryDiscover_MultipleMarkers_NearestWins() { WriteFile(root, ".git/HEAD"); var nested = Path.Combine(root, "nested"); - WriteFile(nested, ".buildvana/buildvana.json"); + WriteFile(nested, "buildvana.json"); var start = Path.Combine(nested, "src"); _ = Directory.CreateDirectory(start); var found = HomeDirectoryDiscovery.TryDiscover(start, out var home); diff --git a/tests/Buildvana.Runtime.Tests/BuildvanaConfigLoadTests.cs b/tests/Buildvana.Runtime.Tests/BuildvanaConfigLoadTests.cs index 320e7acc..6228669e 100644 --- a/tests/Buildvana.Runtime.Tests/BuildvanaConfigLoadTests.cs +++ b/tests/Buildvana.Runtime.Tests/BuildvanaConfigLoadTests.cs @@ -21,24 +21,21 @@ public async Task Load_NoFile_ReturnsEmptyConfig() } [Test] - public async Task Load_ValidJsoncFile_LoadsTypedConfig() + public async Task LoadFile_NullPath_ReturnsEmptyConfig() + => await Assert.That(BuildvanaConfig.LoadFile(null).Release).IsNull(); + + // Loading a known path skips the search, so the exactly-one rule does not apply to it: a caller holding + // a path (a hook, from its args) reads that file, whatever else sits next to it. + [Test] + public async Task LoadFile_KnownPath_LoadsItRegardlessOfSiblings() { var dir = NewDir(); try { - const string json = """ - { - // A comment, and a trailing comma below. - "release": { "branches": ["main"] }, - "versioning": { "assemblyVersionPrecision": "minor" }, - "nuget": { "feeds": { "release": { "source": "https://release.example", "apiKeyEnv": "KEY" } } }, - } - """; - Write(dir, "buildvana.jsonc", json); - var config = BuildvanaConfig.Load(dir); + Write(dir, "buildvana.json", """{ "release": { "branches": ["main"] } }"""); + Write(dir, "buildvana.jsonc", "{}"); + var config = BuildvanaConfig.LoadFile(Path.Combine(dir, "buildvana.json")); await Assert.That(config.Release!.Branches!.Count).IsEqualTo(1); - await Assert.That(config.Versioning!.AssemblyVersionPrecision).IsEqualTo(AssemblyVersionPrecision.Minor); - await Assert.That(config.NuGet!.Feeds!.Release!.Source).IsEqualTo("https://release.example"); } finally { @@ -47,14 +44,24 @@ public async Task Load_ValidJsoncFile_LoadsTypedConfig() } [Test] - public async Task Load_ConfigInSubdirectory_Loads() + public async Task Load_ValidJsoncFile_LoadsTypedConfig() { var dir = NewDir(); try { - Write(dir, Path.Combine(".buildvana", "buildvana.json"), """{ "release": { "checkPublicApi": true } }"""); + const string json = """ + { + // A comment, and a trailing comma below. + "release": { "branches": ["main"] }, + "versioning": { "assemblyVersionPrecision": "minor" }, + "nuget": { "feeds": { "release": { "source": "https://release.example", "apiKeyEnv": "KEY" } } }, + } + """; + Write(dir, "buildvana.jsonc", json); var config = BuildvanaConfig.Load(dir); - await Assert.That(config.Release!.CheckPublicApi).IsTrue(); + await Assert.That(config.Release!.Branches!.Count).IsEqualTo(1); + await Assert.That(config.Versioning!.AssemblyVersionPrecision).IsEqualTo(AssemblyVersionPrecision.Minor); + await Assert.That(config.NuGet!.Feeds!.Release!.Source).IsEqualTo("https://release.example"); } finally { @@ -148,6 +155,23 @@ public async Task FindFile_OneFile_ReturnsItsPath() } } + // The .buildvana subdirectory was a candidate location until it turned every hook's directory into a home + // directory of its own. A file left there is not the repository's configuration, and is not found. + [Test] + public async Task FindFile_FileInBuildvanaSubdirectory_ReturnsNull() + { + var dir = NewDir(); + try + { + Write(dir, ".buildvana/buildvana.jsonc", "{}"); + await Assert.That(BuildvanaConfig.FindFile(dir)).IsNull(); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + private static string NewDir() { var dir = Path.Combine(Path.GetTempPath(), "bvtest_" + Guid.NewGuid().ToString("N")); diff --git a/tests/Buildvana.Runtime.Tests/DotNetConfigExtensionsTests.cs b/tests/Buildvana.Runtime.Tests/DotNetConfigExtensionsTests.cs new file mode 100644 index 00000000..d852baf3 --- /dev/null +++ b/tests/Buildvana.Runtime.Tests/DotNetConfigExtensionsTests.cs @@ -0,0 +1,33 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Buildvana.Runtime; + +internal sealed class DotNetConfigExtensionsTests +{ + // An absent section and an absent setting are the same statement — "not configured" — and the default + // answers both, so that no consumer has to spell out the fallback and risk spelling it differently. + [Test] + public async Task EffectiveConfiguration_WithoutSection_IsTheDefault() + { + DotNetConfig? config = null; + + await Assert.That(config.EffectiveConfiguration).IsEqualTo(DotNetConfig.DefaultConfiguration); + } + + [Test] + public async Task EffectiveConfiguration_WithoutValue_IsTheDefault() + { + var config = new DotNetConfig(); + + await Assert.That(config.EffectiveConfiguration).IsEqualTo(DotNetConfig.DefaultConfiguration); + } + + [Test] + public async Task EffectiveConfiguration_WithValue_IsThatValue() + { + var config = new DotNetConfig { Configuration = "Debug" }; + + await Assert.That(config.EffectiveConfiguration).IsEqualTo("Debug"); + } +} diff --git a/tests/Buildvana.Runtime.Tests/PostReleaseHookArgsTests.cs b/tests/Buildvana.Runtime.Tests/PostReleaseHookArgsTests.cs index 076a64ba..34dee89a 100644 --- a/tests/Buildvana.Runtime.Tests/PostReleaseHookArgsTests.cs +++ b/tests/Buildvana.Runtime.Tests/PostReleaseHookArgsTests.cs @@ -22,13 +22,18 @@ public async Task Load_NoArgsFile_Throws() // Serializes the way bv does (same serializer context, resolved by runtime type) and loads the result // back, proving the two sides of the hook contract agree. + // Both configuration-file cases run because ConfigFile is required and nullable: a repository with no + // configuration file round-trips only as long as the serializer writes the null, and a default ignore + // condition on the context would take that away. Here that costs a failing test, not a failing release. [Test] - public async Task Load_RoundTripsWhatBvWrites() + [Arguments(true)] + [Arguments(false)] + public async Task Load_RoundTripsWhatBvWrites(bool withConfigFile) { var dir = NewDir(); try { - var written = SampleArgs(dir); + var written = SampleArgs(dir, withConfigFile); var relativePath = WellKnownPaths.GetHookArgsFile(PostReleaseHookArgs.Context, PostReleaseHookArgs.Event); var path = Path.Combine(dir, relativePath); _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); @@ -120,7 +125,41 @@ public async Task Load_UnreadableArgsFile_Throws() } } - private static PostReleaseHookArgs SampleArgs(string home) => new() + // A hook is told which file to read and reads it then and there, so it sees the file as it stands + // rather than a copy taken when the args were written. + [Test] + public async Task LoadConfig_ReadsTheFileNamedInTheArgs() + { + var dir = NewDir(); + try + { + await File.WriteAllTextAsync( + Path.Combine(dir, BuildvanaConfig.JsoncFileName), + """{ "release": { "branches": ["main"] } }""").ConfigureAwait(false); + var config = SampleArgs(dir).LoadConfig(); + await Assert.That(config.Release!.Branches!.Count).IsEqualTo(1); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + + [Test] + public async Task LoadConfig_WithoutConfigFile_ReturnsEmptyConfig() + { + var dir = NewDir(); + try + { + await Assert.That(SampleArgs(dir, withConfigFile: false).LoadConfig().Release).IsNull(); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + + private static PostReleaseHookArgs SampleArgs(string home, bool withConfigFile = true) => new() { RuntimeInfo = new() { @@ -129,6 +168,7 @@ public async Task Load_UnreadableArgsFile_Throws() HomeDirectory = home, ArtifactsDirectory = Path.Combine(home, "artifacts", "Release"), ScratchDirectory = Path.Combine(home, WellKnownPaths.ScratchDirectory), + ConfigFile = withConfigFile ? Path.Combine(home, BuildvanaConfig.JsoncFileName) : null, }, Release = new() { diff --git a/tests/Buildvana.Runtime.Tests/VersioningConfigExtensionsTests.cs b/tests/Buildvana.Runtime.Tests/VersioningConfigExtensionsTests.cs new file mode 100644 index 00000000..edada011 --- /dev/null +++ b/tests/Buildvana.Runtime.Tests/VersioningConfigExtensionsTests.cs @@ -0,0 +1,35 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Buildvana.Runtime; + +internal sealed class VersioningConfigExtensionsTests +{ + // An absent section and an absent setting are the same statement — "not configured" — and the default + // answers both, so that no consumer has to spell out the fallback and risk spelling it differently. + [Test] + public async Task EffectiveAssemblyVersionPrecision_WithoutSection_IsTheDefault() + { + VersioningConfig? config = null; + + await Assert.That(config.EffectiveAssemblyVersionPrecision) + .IsEqualTo(VersioningConfig.DefaultAssemblyVersionPrecision); + } + + [Test] + public async Task EffectiveAssemblyVersionPrecision_WithoutValue_IsTheDefault() + { + var config = new VersioningConfig(); + + await Assert.That(config.EffectiveAssemblyVersionPrecision) + .IsEqualTo(VersioningConfig.DefaultAssemblyVersionPrecision); + } + + [Test] + public async Task EffectiveAssemblyVersionPrecision_WithValue_IsThatValue() + { + var config = new VersioningConfig { AssemblyVersionPrecision = AssemblyVersionPrecision.Build }; + + await Assert.That(config.EffectiveAssemblyVersionPrecision).IsEqualTo(AssemblyVersionPrecision.Build); + } +} diff --git a/tests/Buildvana.Sdk.Tests/SdkPropsTests.cs b/tests/Buildvana.Sdk.Tests/SdkPropsTests.cs index f6bde679..12cf4be2 100644 --- a/tests/Buildvana.Sdk.Tests/SdkPropsTests.cs +++ b/tests/Buildvana.Sdk.Tests/SdkPropsTests.cs @@ -10,8 +10,6 @@ internal sealed class SdkPropsTests [Test] [Arguments("buildvana.json")] [Arguments("buildvana.jsonc")] - [Arguments(".buildvana/buildvana.json")] - [Arguments(".buildvana/buildvana.jsonc")] [Arguments(".git")] [Arguments(".git/HEAD")] public async Task Evaluate_MarkerInRepoRoot_SetsHomeDirectory(string marker) @@ -23,48 +21,40 @@ public async Task Evaluate_MarkerInRepoRoot_SetsHomeDirectory(string marker) await Assert.That(result.Errors).IsEmpty(); } + // Hooks are projects living under .buildvana/, so the SDK evaluates from there on every hook build. + // Nothing inside that directory is a marker, which is what keeps a hook's home directory the repository's own. [Test] - public async Task Evaluate_ConfigInSubdirectory_HomeIsContainingDirectory() + public async Task Evaluate_ProjectUnderBuildvanaDirectory_HomeIsRepositoryRoot() { using var fixture = new SdkPropsFixture(); - fixture.WriteFile(".git/HEAD"); - fixture.WriteFile("nested/.buildvana/buildvana.jsonc"); - var result = fixture.Evaluate("nested/src/Test"); - var expected = Path.Combine(fixture.RepoDirectory, "nested") + Path.DirectorySeparatorChar; - await Assert.That(result.HomeDirectory).IsEqualTo(expected); + fixture.WriteFile("buildvana.jsonc"); + var result = fixture.Evaluate(".buildvana/hooks/release"); + await Assert.That(result.HomeDirectory).IsEqualTo(fixture.RepoDirectory + Path.DirectorySeparatorChar); await Assert.That(result.Errors).IsEmpty(); } + // The same rule HomeDirectoryDiscoveryTests pins for the C# implementation: a configuration file marks the + // directory it sits in, and .buildvana/ is not an exception. Nothing but this test checks the two agree. [Test] - public async Task Evaluate_BothVariantsInHomeDirectory_ReportsBVSDK1005() + public async Task Evaluate_ConfigFileInBuildvanaSubdirectory_DoesNotMarkTheParent() { using var fixture = new SdkPropsFixture(); - fixture.WriteFile("buildvana.json"); - fixture.WriteFile("buildvana.jsonc"); - var result = fixture.Evaluate(); - var error = result.Errors.Single(static e => e.Code == "BVSDK1005"); - await Assert.That(error.Text).Contains("buildvana.json"); - await Assert.That(error.Text).Contains("buildvana.jsonc"); + fixture.WriteFile(".git/HEAD"); + fixture.WriteFile("nested/.buildvana/buildvana.jsonc"); + var result = fixture.Evaluate("nested"); + await Assert.That(result.HomeDirectory).IsEqualTo(fixture.RepoDirectory + Path.DirectorySeparatorChar); + await Assert.That(result.Errors).IsEmpty(); } [Test] - public async Task Evaluate_ConfigInRootAndSubdirectory_ReportsBVSDK1005NamingAllOffenders() + public async Task Evaluate_BothVariantsInHomeDirectory_ReportsBVSDK1005() { using var fixture = new SdkPropsFixture(); + fixture.WriteFile("buildvana.json"); fixture.WriteFile("buildvana.jsonc"); - fixture.WriteFile(".buildvana/buildvana.json"); var result = fixture.Evaluate(); var error = result.Errors.Single(static e => e.Code == "BVSDK1005"); + await Assert.That(error.Text).Contains("buildvana.json"); await Assert.That(error.Text).Contains("buildvana.jsonc"); - await Assert.That(error.Text).Contains(".buildvana"); - } - - [Test] - public async Task Evaluate_SingleConfigFile_ReportsNoError() - { - using var fixture = new SdkPropsFixture(); - fixture.WriteFile(".buildvana/buildvana.jsonc"); - var result = fixture.Evaluate(); - await Assert.That(result.Errors).IsEmpty(); } } diff --git a/tests/Buildvana.Tool.Tests/HookRunnerTests.cs b/tests/Buildvana.Tool.Tests/HookRunnerTests.cs index f830e0f2..3f830468 100644 --- a/tests/Buildvana.Tool.Tests/HookRunnerTests.cs +++ b/tests/Buildvana.Tool.Tests/HookRunnerTests.cs @@ -195,6 +195,7 @@ private static string ArgsPath(TempHome home) HomeDirectory = home.RootPath, ArtifactsDirectory = Path.Combine(home.RootPath, "artifacts", "Release"), ScratchDirectory = Path.Combine(home.RootPath, WellKnownPaths.ScratchDirectory), + ConfigFile = null, }, Release = new() { diff --git a/tests/Buildvana.Tool.Tests/PostReleaseHookArgsFactoryTests.cs b/tests/Buildvana.Tool.Tests/PostReleaseHookArgsFactoryTests.cs index ed6b0ca6..e9f8813a 100644 --- a/tests/Buildvana.Tool.Tests/PostReleaseHookArgsFactoryTests.cs +++ b/tests/Buildvana.Tool.Tests/PostReleaseHookArgsFactoryTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using Buildvana.Core.Configuration; using Buildvana.Core.HomeDirectory; using Buildvana.Core.Testing; using Buildvana.Runtime; @@ -71,6 +72,31 @@ public async Task Create_ReportsHomeDirectoryWithoutTrailingSeparator() await Assert.That(args.RuntimeInfo.HomeDirectory).IsEqualTo(home.RootPath); } + // A hook rewriting the configuration file must act on the file bv read, so the args name it outright + // rather than leaving the hook to search for it. + [Test] + [Arguments("buildvana.json")] + [Arguments("buildvana.jsonc")] + public async Task Create_ReportsTheConfigurationFileTheRunRead(string fileName) + { + using var home = new TempHome(); + home.WriteFile(fileName, "{}\n"); + + var args = Create(home.Provider); + + await Assert.That(args.RuntimeInfo.ConfigFile).IsEqualTo(Path.Combine(home.RootPath, fileName)); + } + + [Test] + public async Task Create_LeavesConfigFileNull_WhenRepositoryHasNone() + { + using var home = new TempHome(); + + var args = Create(home.Provider); + + await Assert.That(args.RuntimeInfo.ConfigFile).IsNull(); + } + [Test] public async Task Create_SetsOwnVersionAsRuntimeVersion() { @@ -156,7 +182,7 @@ private static PostReleaseHookArgs Create( bool isPublicRelease = false, IReadOnlyDictionary? producedPackages = null, bool dogfooding = false) - => new PostReleaseHookArgsFactory(home).Create( + => new PostReleaseHookArgsFactory(home, new BuildvanaConfigProvider(home)).Create( artifactsPath ?? Path.Combine("artifacts", "Release"), simpleVersion, semVer, diff --git a/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs b/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs index 1a0c8694..f280d757 100644 --- a/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs +++ b/tests/Buildvana.Tool.Tests/SelfVersionServiceTests.cs @@ -2,6 +2,7 @@ // See the LICENSE file in the project root for full license information. using Buildvana.Core; +using Buildvana.Core.Configuration; using Buildvana.Core.ConsoleOutput; using Buildvana.Core.Json; using Buildvana.Core.Process; @@ -528,6 +529,7 @@ private static SelfVersionService CreateService( => new( reporter ?? NullReporter.Instance, home.Provider, + new BuildvanaConfigProvider(home.Provider), new JsonHelper(), processRunner ?? new FakeProcessRunner(), NuGetVersion.Parse(ownVersion)); diff --git a/tests/Buildvana.Tool.Tests/UpdateCommandTests.cs b/tests/Buildvana.Tool.Tests/UpdateCommandTests.cs index ed896de6..eb6e05f7 100644 --- a/tests/Buildvana.Tool.Tests/UpdateCommandTests.cs +++ b/tests/Buildvana.Tool.Tests/UpdateCommandTests.cs @@ -1,6 +1,7 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using Buildvana.Core.Configuration; using Buildvana.Core.ConsoleOutput; using Buildvana.Core.Json; using Buildvana.Core.Testing; @@ -66,6 +67,7 @@ private static SelfVersionService CreateService(TempHome home, FakeProcessRunner => new( NullReporter.Instance, home.Provider, + new BuildvanaConfigProvider(home.Provider), new JsonHelper(), processRunner ?? new FakeProcessRunner(), NuGetVersion.Parse(OwnVersion));