Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .azure/pipelines/build-bravo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
35 changes: 35 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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~<Name>"` |

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/`. |
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
36 changes: 36 additions & 0 deletions docs/design/code-conventions.md
Original file line number Diff line number Diff line change
@@ -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.
54 changes: 54 additions & 0 deletions docs/design/versioning.md
Original file line number Diff line number Diff line change
@@ -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.
42 changes: 42 additions & 0 deletions docs/documentation-guidelines.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 1 addition & 4 deletions src/Infrastructure/AppEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -138,8 +137,6 @@ public static RegistryKey? ApplicationInstallerRegistryHKey
}
}

public static AppVersionInfo VersionInfo { get; }

public static JsonSerializerOptions DefaultJsonOptions { get; }

public static string ApplicationDataPath { get; }
Expand Down Expand Up @@ -193,7 +190,7 @@ private static void AddEnvironmentDiagnosticInfo()
//
ApplicationPublishMode = PublishMode.ToString(),
ApplicationDeploymentMode = DeploymentMode.ToString(),
ApplicationVersion = VersionInfo.InformationalVersion,
ApplicationVersion = AppVersion.InformationalVersion,
ApplicationDataPath,
ApplicationTempPath,
ApplicationUserSettingsFilePath = UserSettingsFilePath,
Expand Down
56 changes: 56 additions & 0 deletions src/Infrastructure/AppVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace Sqlbi.Bravo.Infrastructure;

/// <summary>
/// Application version, stamped from version.json by Nerdbank.GitVersioning.
/// </summary>
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);
}

/// <summary>
/// True if the build is a prerelease, false if it is a release.
/// </summary>
public static bool IsPrerelease { get; }

/// <summary>
/// True if the build is a public release, false if it is a internal build (e.g. CI build).
/// </summary>
public static bool IsPublicRelease { get; }

/// <summary>
/// Four-part assembly file version <c>Major.Minor.Patch.Height</c>, where <c>Height</c>
/// is the version height used to distinguish builds of the same release.
/// </summary>
public static string FileVersion { get; }

/// <summary>
/// Semantic version of the application, including the prerelease label when present
/// and excluding build metadata. e.g. <c>1.1.0-beta.1</c> or <c>1.1.0</c>.
/// </summary>
public static string SemanticVersion { get; }

/// <summary>
/// <see cref="FileVersion"/> with the prerelease tag, if any, and the git commit id:
/// <c>1.1.0.14-beta.1+1c52e441d1</c>.
/// </summary>
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..];
}
}
35 changes: 0 additions & 35 deletions src/Infrastructure/AppVersionInfo.cs

This file was deleted.

3 changes: 2 additions & 1 deletion src/Infrastructure/AppWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion src/Infrastructure/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <summary>
Expand Down
17 changes: 7 additions & 10 deletions src/Infrastructure/Helpers/CommonHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public static string NormalizeUriString(string uriString)
return directoryName;
}

public async static Task<BravoUpdate> CheckForUpdateAsync(UpdateChannelType updateChannel, CancellationToken cancellationToken)
public static async Task<BravoUpdate> CheckForUpdateAsync(UpdateChannelType updateChannel, CancellationToken cancellationToken)
{
var channelPath = updateChannel switch
{
Expand All @@ -108,19 +108,16 @@ public async static Task<BravoUpdate> 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)
Expand Down
6 changes: 3 additions & 3 deletions src/Infrastructure/Helpers/VpaxHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@
{
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)
{
Expand All @@ -73,12 +73,12 @@
var referentialIntegrityViolationSampleRows = 0; // RI violation sampling is not required for model analysis in Bravo nor for VPAX export.

cancellationToken.ThrowIfCancellationRequested();
StatExtractor.UpdateStatisticsModel(daxModel, connection, referentialIntegrityViolationSampleRows, analyzeDirectQuery, analyzeDirectLake); // TOFIX: remove deprecated (requires refactoring VertiPaqAnalyzer APIs)

Check warning on line 76 in src/Infrastructure/Helpers/VpaxHelper.cs

View workflow job for this annotation

GitHub Actions / build-and-test

'StatExtractor.UpdateStatisticsModel(Model, IDbConnection, int, bool, DirectLakeExtractionMode, int)' is obsolete: 'This method may produce incomplete results if used on a model with DirectLake partitions and DirectLakeExtractionMode parameter set to anything other than ResidentOnly. Use TomExtractor.GetDaxModel instead.'

Check warning on line 76 in src/Infrastructure/Helpers/VpaxHelper.cs

View workflow job for this annotation

GitHub Actions / build-and-test

'StatExtractor.UpdateStatisticsModel(Model, IDbConnection, int, bool, DirectLakeExtractionMode, int)' is obsolete: 'This method may produce incomplete results if used on a model with DirectLake partitions and DirectLakeExtractionMode parameter set to anything other than ResidentOnly. Use TomExtractor.GetDaxModel instead.'

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);
}
}
}
Expand Down
Loading
Loading