From 89349044f90faded9ccb6a317b23160d5fe69e20 Mon Sep 17 00:00:00 2001 From: Alberto Spelta Date: Tue, 1 Sep 2026 18:25:16 +0200 Subject: [PATCH] Support prerelease versions in the UI and the update check AppVersion now exposes SemanticVersion, so the About dialog and window title include the prerelease tag, while the copy action uses the informational version with the commit id. Update checks compare all four file-version fields because a prerelease and its final release share Major.Minor.Patch. Artifact names use SemVer 2 for the same reason. --- .azure/pipelines/build-bravo.yaml | 2 +- AGENTS.md | 35 ++++++++++++ CLAUDE.md | 1 + docs/design/code-conventions.md | 36 ++++++++++++ docs/design/versioning.md | 54 ++++++++++++++++++ docs/documentation-guidelines.md | 42 ++++++++++++++ src/Infrastructure/AppEnvironment.cs | 5 +- src/Infrastructure/AppVersion.cs | 56 +++++++++++++++++++ src/Infrastructure/AppVersionInfo.cs | 35 ------------ src/Infrastructure/AppWindow.cs | 3 +- .../Extensions/StringExtensions.cs | 9 ++- src/Infrastructure/Helpers/CommonHelper.cs | 17 +++--- src/Infrastructure/Helpers/VpaxHelper.cs | 6 +- .../Telemetry/TelemetrySessionInfo.cs | 6 +- src/Scripts/@types/global.d.ts | 1 + src/Scripts/controllers/app.ts | 1 + src/Scripts/controllers/debug.ts | 3 +- src/Scripts/main.ts | 3 +- src/Scripts/view/options-dialog-about.ts | 2 +- src/Services/ExportDataService.cs | 2 +- src/Services/FormatDaxService.cs | 2 +- .../Infrastructure/AppVersionTests.cs | 45 +++++++++++++++ version.json | 7 ++- 23 files changed, 308 insertions(+), 65 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 docs/design/code-conventions.md create mode 100644 docs/design/versioning.md create mode 100644 docs/documentation-guidelines.md create mode 100644 src/Infrastructure/AppVersion.cs delete mode 100644 src/Infrastructure/AppVersionInfo.cs create mode 100644 test/Bravo.Tests/Infrastructure/AppVersionTests.cs 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$" ],