diff --git a/.azure/pipelines/build-bravo.yaml b/.azure/pipelines/build-bravo.yaml index 047d3438..89f3e83a 100644 --- a/.azure/pipelines/build-bravo.yaml +++ b/.azure/pipelines/build-bravo.yaml @@ -45,7 +45,7 @@ steps: - script: dotnet tool install --global AzureSignTool displayName: Setup AzureSignTool - bash: | - artifact="Bravo.$(NBGV_SimpleVersion).$(artifactSuffix)" + artifact="Bravo.$(NBGV_SemVer2).$(artifactSuffix)" echo "##vso[task.setvariable variable=artifact]$artifact" echo "Artifact name: $artifact" displayName: 'Compute variables' diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..25a89627 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,35 @@ +# AGENTS.md + +Operating instructions for agents working on this repository. + +## The application + +Bravo for Power BI, a Windows desktop tool. A WinForms shell hosts a WebView2 view for the UI and an +in-process ASP.NET Core server on loopback for the API that view calls. The .NET host lives in `src/`, the +TypeScript frontend in `src/Scripts` and is built into `src/wwwroot`. + +## Build and test + +| Task | Command | +| --- | --- | +| Build and test | `build.cmd` | +| Build | `dotnet build Bravo.sln` | +| Test | `dotnet test test/Bravo.Tests/Bravo.Tests.csproj` | +| One test | `dotnet test test/Bravo.Tests/Bravo.Tests.csproj --filter "FullyQualifiedName~"` | + +The .NET build runs `npm install` and webpack for the frontend. Add `-p:ClientAssetsEnabled=false` to skip +that step for a C#-only change: it needs no Node.js and is considerably faster. That flag also skips type +checking, so run a full build before finishing any change that touches `src/Scripts` or the shape of the +configuration passed to the frontend. + +`global.json` pins the SDK. The project targets `net10.0-windows`, `win-x64`. Tests use xUnit and reach the +internals of `Bravo` through `InternalsVisibleTo`. The build stamps the version from git history, so a clone +or a CI checkout must be complete: with a shallow clone the build fails or produces a wrong version. + +## Read before you write + +| Document | Read it before | +| --- | --- | +| [docs/design/code-conventions.md](docs/design/code-conventions.md) | creating or editing any `.cs` file. Encoding and design rules apply from its first line, and a wrong encoding rewrites the whole file. | +| [docs/design/versioning.md](docs/design/versioning.md) | touching `version.json`, the build number, or any code that compares versions. | +| [docs/documentation-guidelines.md](docs/documentation-guidelines.md) | writing or updating anything under `docs/`. | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/docs/design/code-conventions.md b/docs/design/code-conventions.md new file mode 100644 index 00000000..cd178285 --- /dev/null +++ b/docs/design/code-conventions.md @@ -0,0 +1,36 @@ +# Code conventions + +Rules that apply to code in this repository, beyond what `.editorconfig` enforces automatically. + +## Files + +New or rewritten `.cs` files are UTF-8 **with** BOM and use CRLF. This is not configured anywhere: it is what +every existing file under `src/` is, and `.editorconfig` sets `charset` only for project and resource files. +A tool that defaults to UTF-8 without BOM, or to LF, turns a small edit into a whole-file diff. + +## Naming and design + +Follow the Microsoft .NET naming and design guidelines. Where a pattern already in the repository diverges +from them, the guidelines win: a local precedent is not an argument for repeating a mistake. + +## Types + +Assign primary constructor parameters to explicit `private readonly` fields. Do not reference a parameter +directly from the body of the type: an explicit field declares the dependency, its lifetime and its +mutability at the top of the type, where they can be read. + +## Comments + +Comment only what the code cannot express: origin, constraints, or rationale. Never restate the code. Document +the member, not its callers. Usage rules belong to the caller or to docs/. Keep summaries short and semantic +and exclude implementation details; put them in inline comments when they matter. + +## Testability + +Design for testability from the start: + +- keep classes small and focused on one responsibility; +- inject dependencies through DI instead of relying on static state; +- pass external state — process, registry, filesystem, clock, network — through dependencies that tests can control. + +A type that can only be tested by running the application is not finished. diff --git a/docs/design/versioning.md b/docs/design/versioning.md new file mode 100644 index 00000000..76d57b39 --- /dev/null +++ b/docs/design/versioning.md @@ -0,0 +1,54 @@ +# Versioning + +Bravo has a single Semantic Version, declared in `version.json` and computed by Nerdbank.GitVersioning (NBGV). + +## Source of truth + +`version.json` at the repository root holds the only hand-edited value: + +- `1.1.0` — a released version. +- `1.1.0-beta.1` — a preview of the upcoming `1.1.0`. + +One release is one bump: minor for a feature, patch for a fix. The prerelease tag is chosen by hand and +incremented per preview, so that a preview never consumes a patch number of the stable line. + +Do not set `Version`, `FileVersion` or `InformationalVersion` in `Bravo.csproj`: NBGV stamps them from +`version.json` at build time. + +## Derived values + +| Value | Shape | Authoritative for | +| --- | --- | --- | +| `AssemblyFileVersion` | `X.Y.Z.{height}` | ordering two builds; MSI `ProductVersion` | +| `AssemblyInformationalVersion` | `X.Y.Z.{height}[-tag]+{commit}` | diagnostics | +| `AssemblyVersion` | `X.Y.0.0` | assembly identity (`assemblyVersion.precision: minor`) | +| `NBGV_SimpleVersion` | `X.Y.Z` | WiX `-dVersion` | +| `NBGV_SemVer2` | `X.Y.Z[-tag]` | artifact names, git tag | + +`{height}` is the number of commits since the numeric `X.Y.Z` last changed. It is a build counter: it makes +every build uniquely identifiable and orders builds that share the same `X.Y.Z`. + +## Rules + +- **The prerelease tag never reaches a numeric field.** `AssemblyFileVersion` and `NBGV_SimpleVersion` stay +numeric in every state, so Windows Installer and `System.Version` keep working unchanged. + +- **The height resets only when the numeric `X.Y.Z` changes.** Adding, changing or removing the prerelease tag +does not reset it. `AssemblyFileVersion` is therefore monotonic across `1.1.0-beta.1 → 1.1.0-beta.2 → 1.1.0`, +which is what makes a preview and its final release — identical on `X.Y.Z` — orderable. + +- **`pathFilters` must stay `:/`, the repository root.** Every commit must advance the height. A narrower filter +leaves the height unchanged for commits that touch only excluded paths, and the commit that promotes a preview +to its final release changes `version.json` alone: such a filter would give the two builds the same +`AssemblyFileVersion`. + +- **`publicReleaseRefSpec` lists the branches that produce clean versions.** Outside them, `SemVer2` and +`NuGetPackageVersion` carry a `.g{commit}` suffix; numeric fields are unaffected. A tag checkout runs in +detached HEAD and matches no branch pattern, so building from a tag requires adding the tag pattern. + +- **Artifact names come from `SemVer2`, not from `SimpleVersion`.** The two are identical for a release and +differ only for a preview, where `SimpleVersion` drops the tag: naming artifacts from it would give a preview +and the release that follows it the same file names. + +- **The height needs full history.** Shallow clones make NBGV fail or compute a wrong number, so both +pipelines check out with unlimited depth. diff --git a/docs/documentation-guidelines.md b/docs/documentation-guidelines.md new file mode 100644 index 00000000..daa84f92 --- /dev/null +++ b/docs/documentation-guidelines.md @@ -0,0 +1,42 @@ +# Documentation guidelines + +Rules for writing and updating the documents under `docs/`. The reader is an LLM working on this repository, +not an end user. + +## What a document contains + +Write what the code cannot state: + +- constraints and invariants, each with the consequence of violating it; +- the reason behind a choice, when the result alone does not explain it; +- facts that live outside the repository: external services, deployed clients, published artifacts. + +Do not restate the code: no property or method lists, no file walkthroughs, no description of a control flow +that can be read in the source. Anything copied from the code drifts as soon as the code changes. Reference a +file by link when the reader needs it; do not reproduce its content. + +## Scope + +One document, one topic. A document named after a topic covers that topic only. Adjacent material belongs to +the adjacent document, or does not exist yet. + +## Form + +- Schematic: short sections, tables for enumerable facts, one idea per paragraph. +- Direct, professional language. Short sentences. No filler, no narration. +- English. +- A rule states what holds and what breaks when it is violated. +- Motivation is a clause attached to the rule it justifies, never a section of its own. + +## What a document is not + +- Not a changelog: no dates, no "decision taken on", no "supersedes", no history of what changed. +- Not a plan: no open items, no TODO, no status. Planned work belongs to a plan document. +- Not a tutorial: no step-by-step walkthrough of ordinary tasks. + +A document describes the current state as if it had always been that way. + +## Maintenance + +Update a document in the same change that alters the behaviour it describes. When a rule stops holding, delete +it; do not annotate it as obsolete. diff --git a/src/Infrastructure/AppEnvironment.cs b/src/Infrastructure/AppEnvironment.cs index a8e35fd9..7dbec0da 100644 --- a/src/Infrastructure/AppEnvironment.cs +++ b/src/Infrastructure/AppEnvironment.cs @@ -83,7 +83,6 @@ static AppEnvironment() SessionId = currentProcess.SessionId; ProcessPath = Environment.ProcessPath!; - VersionInfo = new AppVersionInfo(); ApplicationDataPath = Path.Combine(Environment.GetFolderPath(DeploymentMode == AppDeploymentMode.Packaged ? Environment.SpecialFolder.UserProfile : Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify), ApplicationName); ApplicationTempPath = Path.Combine(ApplicationDataPath, ".temp"); UserSettingsFilePath = Path.Combine(ApplicationDataPath, "usersettings.json"); @@ -138,8 +137,6 @@ public static RegistryKey? ApplicationInstallerRegistryHKey } } - public static AppVersionInfo VersionInfo { get; } - public static JsonSerializerOptions DefaultJsonOptions { get; } public static string ApplicationDataPath { get; } @@ -193,7 +190,7 @@ private static void AddEnvironmentDiagnosticInfo() // ApplicationPublishMode = PublishMode.ToString(), ApplicationDeploymentMode = DeploymentMode.ToString(), - ApplicationVersion = VersionInfo.InformationalVersion, + ApplicationVersion = AppVersion.InformationalVersion, ApplicationDataPath, ApplicationTempPath, ApplicationUserSettingsFilePath = UserSettingsFilePath, diff --git a/src/Infrastructure/AppVersion.cs b/src/Infrastructure/AppVersion.cs new file mode 100644 index 00000000..e2de30bc --- /dev/null +++ b/src/Infrastructure/AppVersion.cs @@ -0,0 +1,56 @@ +namespace Sqlbi.Bravo.Infrastructure; + +/// +/// Application version, stamped from version.json by Nerdbank.GitVersioning. +/// +internal static class AppVersion +{ + static AppVersion() + { + IsPrerelease = ThisAssembly.IsPrerelease; + IsPublicRelease = ThisAssembly.IsPublicRelease; + FileVersion = ThisAssembly.AssemblyFileVersion; + InformationalVersion = ThisAssembly.AssemblyInformationalVersion; + SemanticVersion = System.Version.Parse(FileVersion).ToString(3) + GetPrereleaseTag(InformationalVersion); + } + + /// + /// True if the build is a prerelease, false if it is a release. + /// + public static bool IsPrerelease { get; } + + /// + /// True if the build is a public release, false if it is a internal build (e.g. CI build). + /// + public static bool IsPublicRelease { get; } + + /// + /// Four-part assembly file version Major.Minor.Patch.Height, where Height + /// is the version height used to distinguish builds of the same release. + /// + public static string FileVersion { get; } + + /// + /// Semantic version of the application, including the prerelease label when present + /// and excluding build metadata. e.g. 1.1.0-beta.1 or 1.1.0. + /// + public static string SemanticVersion { get; } + + /// + /// with the prerelease tag, if any, and the git commit id: + /// 1.1.0.14-beta.1+1c52e441d1. + /// + public static string InformationalVersion { get; } + + internal static string GetPrereleaseTag(string informationalVersion) + { + var value = informationalVersion; + + var metadataIndex = value.IndexOf('+'); + if (metadataIndex >= 0) + value = value[..metadataIndex]; + + var prereleaseIndex = value.IndexOf('-'); + return prereleaseIndex < 0 ? string.Empty : value[prereleaseIndex..]; + } +} diff --git a/src/Infrastructure/AppVersionInfo.cs b/src/Infrastructure/AppVersionInfo.cs deleted file mode 100644 index 0ad29709..00000000 --- a/src/Infrastructure/AppVersionInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Sqlbi.Bravo.Infrastructure; - -/// -/// Exposes the application version, stamped at build time by Nerdbank.GitVersioning from version.json. -/// -internal sealed class AppVersionInfo -{ - public AppVersionInfo() - { - var build = ThisAssembly.AssemblyFileVersion; - - Build = build; - Version = System.Version.Parse(build).ToString(3); - InformationalVersion = ThisAssembly.AssemblyInformationalVersion; - } - - /// - /// Gets the full four-part assembly file version Major.Minor.Patch.Height, where the fourth field is the - /// git-height build counter. Intended for diagnostics only (e.g. telemetry) - never used for update comparisons - /// or shown to users; use instead. - /// - public string Build { get; } - - /// - /// Gets the three-part Semantic Version Major.Minor.Patch. This is the canonical application version: - /// shown to users and used to compare versions when checking for updates. - /// - public string Version { get; } - - /// - /// Gets the informational version: the version with build metadata (the git commit id) appended, - /// e.g. 1.2.3.45+0a1b2c3d4e. Intended for diagnostics. - /// - public string InformationalVersion { get; } -} diff --git a/src/Infrastructure/AppWindow.cs b/src/Infrastructure/AppWindow.cs index e68e197c..c7c86f5f 100644 --- a/src/Infrastructure/AppWindow.cs +++ b/src/Infrastructure/AppWindow.cs @@ -319,7 +319,8 @@ private MemoryStream GetConfigJs() #endif address = _serverAddressProvider.GetListeningAddress(), token = AppEnvironment.ApiAuthenticationToken, - version = AppEnvironment.VersionInfo.Version, + version = AppVersion.SemanticVersion, + informationalVersion = AppVersion.InformationalVersion, options = BravoOptions.CreateFromUserPreferences(), policies = _policies, culture = new diff --git a/src/Infrastructure/Extensions/StringExtensions.cs b/src/Infrastructure/Extensions/StringExtensions.cs index 1719867b..0e475838 100644 --- a/src/Infrastructure/Extensions/StringExtensions.cs +++ b/src/Infrastructure/Extensions/StringExtensions.cs @@ -16,7 +16,14 @@ internal static class StringExtensions public static string AppendApplicationVersion(this string value) { - return $"{value} - v{AppEnvironment.VersionInfo.Version}"; + var result = $"{value} - v{AppVersion.SemanticVersion}"; + + if (AppVersion.IsPrerelease || !AppVersion.IsPublicRelease) + { + result += $" ({AppVersion.InformationalVersion})"; + } + + return result; } /// diff --git a/src/Infrastructure/Helpers/CommonHelper.cs b/src/Infrastructure/Helpers/CommonHelper.cs index 314124b2..53723c1d 100644 --- a/src/Infrastructure/Helpers/CommonHelper.cs +++ b/src/Infrastructure/Helpers/CommonHelper.cs @@ -90,7 +90,7 @@ public static string NormalizeUriString(string uriString) return directoryName; } - public async static Task CheckForUpdateAsync(UpdateChannelType updateChannel, CancellationToken cancellationToken) + public static async Task CheckForUpdateAsync(UpdateChannelType updateChannel, CancellationToken cancellationToken) { var channelPath = updateChannel switch { @@ -108,19 +108,16 @@ public async static Task CheckForUpdateAsync(UpdateChannelType upda using var document = JsonDocument.Parse(json); var rootElement = document.RootElement; - var version = Version.Parse(rootElement.GetProperty("version").GetString()!) - .ToString(3); // Versioning is SemVer-based: discard a 4th (build) digit if present - var isNewerVersion = Version.Parse(version) > Version.Parse(AppEnvironment.VersionInfo.Version); - var downloadUrl = GetDownloadUrl(rootElement.GetProperty("download").GetString()!); - var changelogUrl = rootElement.GetProperty("changelog").GetString()!; + var availableVersion = Version.Parse(rootElement.GetProperty("version").GetString()!); + var installedVersion = Version.Parse(AppVersion.FileVersion); return new BravoUpdate { UpdateChannel = updateChannel, - IsNewerVersion = isNewerVersion, - Version = version, - DownloadUrl = downloadUrl, - ChangelogUrl = changelogUrl, + IsNewerVersion = availableVersion > installedVersion, + Version = availableVersion.ToString(3), + DownloadUrl = GetDownloadUrl(rootElement.GetProperty("download").GetString()!), + ChangelogUrl = rootElement.GetProperty("changelog").GetString()!, }; static string GetDownloadUrl(string downloadUrl) diff --git a/src/Infrastructure/Helpers/VpaxHelper.cs b/src/Infrastructure/Helpers/VpaxHelper.cs index 9f519e3e..156f6aa4 100644 --- a/src/Infrastructure/Helpers/VpaxHelper.cs +++ b/src/Infrastructure/Helpers/VpaxHelper.cs @@ -58,12 +58,12 @@ public static Model GetDaxModel(TabularConnectionWrapper connectionWrapper, bool { var server = connectionWrapper.Server; var database = connectionWrapper.Database; - var daxModel = TomExtractor.GetDaxModel(database.Model, extractorApp: AppEnvironment.ApplicationName, extractorVersion: AppEnvironment.VersionInfo.Version); + var daxModel = TomExtractor.GetDaxModel(database.Model, extractorApp: AppEnvironment.ApplicationName, extractorVersion: AppVersion.SemanticVersion); using var connection = connectionWrapper.CreateAdomdConnection(open: false); { cancellationToken.ThrowIfCancellationRequested(); - DmvExtractor.PopulateFromDmv(daxModel, connection, server.Name, database.Name, extractorApp: AppEnvironment.ApplicationName, extractorVersion: AppEnvironment.VersionInfo.Version); + DmvExtractor.PopulateFromDmv(daxModel, connection, server.Name, database.Name, extractorApp: AppEnvironment.ApplicationName, extractorVersion: AppVersion.SemanticVersion); if (statisticsEnabled) { @@ -78,7 +78,7 @@ public static Model GetDaxModel(TabularConnectionWrapper connectionWrapper, bool if (analyzeDirectLake > DirectLakeExtractionMode.ResidentOnly && daxModel.HasDirectLakePartitions()) { cancellationToken.ThrowIfCancellationRequested(); - DmvExtractor.PopulateFromDmv(daxModel, connection, server.Name, database.Name, extractorApp: AppEnvironment.ApplicationName, extractorVersion: AppEnvironment.VersionInfo.Version); + DmvExtractor.PopulateFromDmv(daxModel, connection, server.Name, database.Name, extractorApp: AppEnvironment.ApplicationName, extractorVersion: AppVersion.SemanticVersion); } } } diff --git a/src/Infrastructure/Telemetry/TelemetrySessionInfo.cs b/src/Infrastructure/Telemetry/TelemetrySessionInfo.cs index f6b9ce4c..4b1b5141 100644 --- a/src/Infrastructure/Telemetry/TelemetrySessionInfo.cs +++ b/src/Infrastructure/Telemetry/TelemetrySessionInfo.cs @@ -9,7 +9,7 @@ internal static class TelemetrySessionInfo /// See public static Uri DefaultIngestionEndpoint { get; } = new Uri("https://dc.services.visualstudio.com/", UriKind.Absolute); public static string ConnectionString { get; } = "InstrumentationKey=47a8970c-6293-408a-9cce-5b7b311574d3"; - public static string ComponentVersion { get; } = AppEnvironment.VersionInfo.Version; + public static string ComponentVersion { get; } = AppVersion.SemanticVersion; public static string DeviceOperatingSystem { get; } = Environment.OSVersion.ToString(); public static string SessionId { get; } = Guid.NewGuid().ToString(); public static string UserId { get; } = $"{Environment.MachineName}\\{Environment.UserName}".ToSHA256Hash(); @@ -17,8 +17,8 @@ internal static class TelemetrySessionInfo public static IReadOnlyDictionary GlobalProperties { get; } = new Dictionary { { "ProductName", AppEnvironment.ApplicationName }, - { "Version", AppEnvironment.VersionInfo.Version }, - { "Build", AppEnvironment.VersionInfo.Build }, + { "Version", AppVersion.SemanticVersion }, + { "Build", AppVersion.FileVersion }, { "PublishMode", AppEnvironment.PublishMode.ToString() }, { "InstallScope", AppEnvironment.DeploymentMode.ToString() }, { "WebView2Version", AppEnvironment.WebView2VersionInfo ?? string.Empty }, diff --git a/src/Scripts/@types/global.d.ts b/src/Scripts/@types/global.d.ts index d3749134..cf426624 100644 --- a/src/Scripts/@types/global.d.ts +++ b/src/Scripts/@types/global.d.ts @@ -7,6 +7,7 @@ declare global { debug?: boolean, address: string version: string, + informationalVersion: string, options: Options, policies?: Policies, token?: string, diff --git a/src/Scripts/controllers/app.ts b/src/Scripts/controllers/app.ts index d311a31b..14a054ff 100644 --- a/src/Scripts/controllers/app.ts +++ b/src/Scripts/controllers/app.ts @@ -30,6 +30,7 @@ import { DialogResponse } from '../view/dialog'; export interface AppVersionInfo { version: string + informationalVersion?: string downloadUrl?: string changelogUrl?: string } diff --git a/src/Scripts/controllers/debug.ts b/src/Scripts/controllers/debug.ts index ce4ffe7a..99ae05ca 100644 --- a/src/Scripts/controllers/debug.ts +++ b/src/Scripts/controllers/debug.ts @@ -37,7 +37,8 @@ export class Debug { globalThis.CONFIG = { debug: true, address: "http://localhost", - version: "0.0.0", + version: "0.0.0-debug", + informationalVersion: "0.0.0.0-debug+0000000000", options: null, token: "", culture: { diff --git a/src/Scripts/main.ts b/src/Scripts/main.ts index 6830bb70..f782b547 100644 --- a/src/Scripts/main.ts +++ b/src/Scripts/main.ts @@ -31,7 +31,8 @@ let pbiDesktop = new PBIDesktop(); let notificationCenter = new NotifyCenter(); let app = new App(new AppVersion({ - version: CONFIG.version + version: CONFIG.version, + informationalVersion: CONFIG.informationalVersion })); export { debug, host, optionsController, themeController, auth, telemetry, pbiDesktop, notificationCenter, logger, app }; \ No newline at end of file diff --git a/src/Scripts/view/options-dialog-about.ts b/src/Scripts/view/options-dialog-about.ts index 6ae5ccba..989704e5 100644 --- a/src/Scripts/view/options-dialog-about.ts +++ b/src/Scripts/view/options-dialog-about.ts @@ -83,7 +83,7 @@ export class OptionsDialogAbout { _(".copy-version", element).addEventListener("click", e => { e.preventDefault(); - navigator.clipboard.writeText(app.currentVersion.toString()); + navigator.clipboard.writeText(app.currentVersion.info.informationalVersion ?? app.currentVersion.toString()); }); _(".auto-check-option input", element).addEventListener("change", e => { diff --git a/src/Services/ExportDataService.cs b/src/Services/ExportDataService.cs index ab349441..cddbdb0b 100644 --- a/src/Services/ExportDataService.cs +++ b/src/Services/ExportDataService.cs @@ -524,7 +524,7 @@ static void WriteSummary(ExportDataJob job, ExportExcelSettings settings, XlsxWr writer.BeginWorksheet("Bravo Export Summary"); writer.BeginRow().Write($"Exported with {AppEnvironment.ApplicationMainWindowTitle}", style: infoStyle); - writer.BeginRow().Write($"Version {AppEnvironment.VersionInfo.Version} (build {AppEnvironment.VersionInfo.Build})", style: infoStyle); + writer.BeginRow().Write($"Version {AppVersion.SemanticVersion}", style: infoStyle); writer.SkipRows(1); writer.SetDefaultStyle(headerStyle).BeginRow().Write("Worksheet").Write("Table").Write("Rows").Write("Status"); writer.SetDefaultStyle(XlsxStyle.Default); diff --git a/src/Services/FormatDaxService.cs b/src/Services/FormatDaxService.cs index e8795ce9..bf93ddca 100644 --- a/src/Services/FormatDaxService.cs +++ b/src/Services/FormatDaxService.cs @@ -113,7 +113,7 @@ private async Task> CallDaxFormatterAsync(IE ListSeparator = options.ListSeparator ?? ',', // TODO: Dax.Formatter declare ListSeparator nullable DecimalSeparator = options.DecimalSeparator ?? '.', // TODO: Dax.Formatter declare DecimalSeparator nullable CallerApp = AppEnvironment.ApplicationName, - CallerVersion = AppEnvironment.VersionInfo.Version, + CallerVersion = AppVersion.SemanticVersion, }; foreach (var measure in measures) diff --git a/test/Bravo.Tests/Infrastructure/AppVersionTests.cs b/test/Bravo.Tests/Infrastructure/AppVersionTests.cs new file mode 100644 index 00000000..b6f77063 --- /dev/null +++ b/test/Bravo.Tests/Infrastructure/AppVersionTests.cs @@ -0,0 +1,45 @@ +using Sqlbi.Bravo.Infrastructure; +using Xunit; + +namespace Bravo.Tests.Infrastructure; + +public class AppVersionTests +{ + [Theory] + [InlineData("1.1.0.11+ed89094be9", "")] + [InlineData("1.1.0.13-beta.1+57e453666e", "-beta.1")] + [InlineData("1.1.0.13-beta.1", "-beta.1")] + [InlineData("1.1.0.0-build.99+74e5ed577e", "-build.99")] + [InlineData("1.1.0.13", "")] + public void GetPrereleaseTag_ReadsTheTag(string informationalVersion, string expected) + { + var actual = AppVersion.GetPrereleaseTag(informationalVersion); + + Assert.Equal(expected, actual); + } + + [Fact] + public void FileVersion_IsNumericWithFourFields() + { + // The update check parses this value and compares all four fields, so a prerelease tag must never + // reach it and the fourth field must always be there. + var parsed = System.Version.TryParse(AppVersion.FileVersion, out var version); + + Assert.True(parsed, $"'{AppVersion.FileVersion}' is not a numeric version."); + Assert.True(version!.Revision >= 0, $"'{AppVersion.FileVersion}' has no fourth field."); + } + + [Fact] + public void SemanticVersion_StartsWithTheNumericReleaseVersion() + { + var releaseVersion = System.Version.Parse(AppVersion.FileVersion).ToString(3); + + Assert.StartsWith(releaseVersion, AppVersion.SemanticVersion); + } + + [Fact] + public void InformationalVersion_StartsWithFileVersion() + { + Assert.StartsWith(AppVersion.FileVersion, AppVersion.InformationalVersion); + } +} diff --git a/version.json b/version.json index cb5029bc..5dc29a5e 100644 --- a/version.json +++ b/version.json @@ -1,9 +1,12 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.1.0", + "version": "1.1.0-beta.1", "assemblyVersion": { - "precision": "build" + "precision": "minor" }, + "pathFilters": [ + ":/" + ], "publicReleaseRefSpec": [ "^refs/heads/main$" ],