Skip to content
Draft
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
9 changes: 9 additions & 0 deletions build/build/BuildLifetime.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Globalization;
using Build.Utilities;
using Common.Lifetime;
using Common.Utilities;
Expand Down Expand Up @@ -46,5 +47,13 @@ private static void SetMsBuildSettingsVersion(BuildContext context)

// https://github.com/dotnet/docs/issues/37674
msBuildSettings.WithProperty("IncludeSourceRevisionInInformationalVersion", "false");

if (ShouldEmbedReleaseDate(context))
{
msBuildSettings.WithProperty("GitVersionReleaseDate", DateTimeOffset.UtcNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
}
}

private static bool ShouldEmbedReleaseDate(BuildContext context) =>
context.IsTaggedRelease || context.IsTaggedPreRelease || context.IsInternalPreRelease;
}
20 changes: 16 additions & 4 deletions docs/input/docs/usage/cli/arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ GitVersion [path]
--show-config Outputs the effective GitVersion config (defaults + custom
from GitVersion.yml, GitVersion.yaml, .GitVersion.yml or .GitVersion.yaml) in yaml format
--override-config
Overrides GitVersion config values inline (semicolon-
separated key value pairs e.g. --override-config
tag-prefix=Foo)
Currently supported config overrides: tag-prefix
Overrides GitVersion config values inline (semicolon-
separated key value pairs e.g. --override-config
tag-prefix=Foo)
Currently supported config overrides: tag-prefix
--telemetry-opt-out
Disables telemetry for this invocation.
--no-cache Bypasses the cache, result will not be written to the cache.
--no-normalize Disables normalize step on a build server.
--allow-shallow Allows GitVersion to run on a shallow clone.
Expand Down Expand Up @@ -170,3 +172,13 @@ Will use only major and minor version numbers for assembly version. Assembly bui
### Example: How to override configuration option 'next-version'

`GitVersion.exe --output json --override-config next-version=6`

## Telemetry

GitVersion supports OSS-style telemetry opt-out controls:

1. `DO_NOT_TRACK`
2. `GITVERSION_TELEMETRY_OPTOUT`
3. `--telemetry-opt-out`

See [CLI Telemetry](/docs/usage/cli/telemetry) for the full disclosure, collected fields, redaction rules, reasons for collection, and opt-out behavior.
102 changes: 102 additions & 0 deletions docs/input/docs/usage/cli/telemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
Order: 25
Title: Telemetry
Description: Telemetry behavior, collected fields, redaction rules, and opt-out for the GitVersion CLI
---

GitVersion can support an optional telemetry pipeline for limited OSS data-collection windows when a telemetry sink is enabled in the distributed build. The purpose is to help maintainers answer product questions such as:

1. Which CLI arguments are used most often
2. Which parser implementation is still in active use
3. Which GitVersion versions are represented in incoming usage data
4. Which command-line flows should be prioritized for UX and documentation improvements

## When telemetry is active

Telemetry is only emitted when a concrete telemetry sink is enabled for the GitVersion distribution you are running.

When telemetry is active, GitVersion shows a one-time disclosure notice before the first telemetry-eligible command sends data. The notice is stored locally so it is not shown on every run.

Telemetry is also timeboxed to the first **3 months after the release date embedded in the assembly metadata**. If the release-date metadata is missing or invalid, telemetry is treated as disabled.

## What GitVersion collects

The telemetry payload is intentionally narrow and command-line focused. It is designed to support usage analysis, not repository inspection.

Each payload includes:

| Field | Description | Reason |
| --- | --- | --- |
| `toolVersion` | The GitVersion CLI version | Helps correlate behavior with a released version |
| `parserImplementation` | `ArgumentParser` or `LegacyArgumentParser` | Helps understand adoption of the legacy parser |
| `continuousIntegrationProvider` | The detected CI provider such as `github-actions`, or `unknown` when GitVersion is not running in a recognized CI environment | Helps understand where CLI usage comes from without collecting repository data |
| `invocationSource` | `gitversion-msbuild` when the CLI is invoked internally by the `GitVersion.MsBuild` package, otherwise `direct` | Helps distinguish direct CLI usage from package-driven usage |
| `command` | The CLI command name (`gitversion`) | Identifies the invoked entry point |
| `subcommand` | Reserved for future CLI command trees; currently `null` in the stable CLI | Keeps the payload shape stable |
| `arguments[].name` | Canonical argument name | Shows which switches/options are used |
| `arguments[].values` | Argument values when allowed | Shows which non-sensitive modes and options are used |

## Argument value rules

GitVersion keeps argument names but applies redaction rules to values when they may contain file-system or sensitive information.

### Collected as plain values

Examples include:

- `output`
- `show-variable`
- `format`
- `verbosity`
- `branch`
- `commit`
- `override-config`

### Redacted as path values

These arguments keep their names, but their values are replaced with `<redacted:path>`:

- `path`
- `target-path`
- `log-file`
- `config`
- `dynamic-repo-location`
- `update-assembly-info`
- `update-project-files`

For path-oriented switches that accept boolean forms, explicit boolean values such as `true`, `false`, `1`, or `0` are kept. Non-boolean path values are redacted.

### Redacted as sensitive values

These arguments keep their names, but their values are replaced with `<redacted:sensitive>`:

- `url`
- `username`
- `password`

## What GitVersion does not collect

GitVersion telemetry is not intended to inspect repositories or build artifacts. In particular, it does not collect:

- repository contents
- commit messages
- branch history
- configuration file contents
- generated version variables
- file contents
- raw path values
- raw credential values

## Opting out

You can disable telemetry in any of these ways:

1. Set `DO_NOT_TRACK=1` or `DO_NOT_TRACK=true`
2. Set `GITVERSION_TELEMETRY_OPTOUT=1` or `GITVERSION_TELEMETRY_OPTOUT=true`
3. Pass `--telemetry-opt-out` for a single invocation

If any of these opt-out mechanisms are used, GitVersion treats telemetry as disabled for that invocation.

## Collection windows

Telemetry is intended for time-boxed collection windows to support OSS design decisions. A distribution can enable telemetry for a limited period, disable it, and later enable it again for a new decision-making window.
7 changes: 7 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

<ItemGroup Condition="'$(GitVersionReleaseDate)' != ''">
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>GitVersionReleaseDate</_Parameter1>
<_Parameter2>$(GitVersionReleaseDate)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup Condition=" '$(DisableAnalyzers)' == 'false' ">
<PackageReference Include="Roslynator.Analyzers">
<PrivateAssets>all</PrivateAssets>
Expand Down
45 changes: 45 additions & 0 deletions src/GitVersion.App.Tests/ArgumentParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,51 @@ public void UsernameAndPasswordCanBeParsed()
arguments.IsHelp.ShouldBe(false);
}

[Test]
public void TelemetryOptOutCanBeParsed()
{
var arguments = this.argumentParser.ParseArguments("--telemetry-opt-out");
arguments.TelemetryOptOut.ShouldBe(true);
}

[Test]
public void TelemetryOptOutCanBeProvidedByEnvironment()
{
this.environment.SetEnvironmentVariable(TelemetryPolicy.DoNotTrackEnvironmentVariable, "1");

var arguments = this.argumentParser.ParseArguments("targetDirectoryPath");

arguments.TelemetryOptOut.ShouldBe(true);
}

[Test]
public void TelemetryPayloadIncludesParserVersionAndRedactedArguments()
{
var arguments = this.argumentParser.ParseArguments(
"targetDirectoryPath --log-file console --output buildserver --format {SemVer} --url https://example.com/repo.git --username user --password pass --dynamic-repo-location /tmp/dynamic");

arguments.Telemetry.ShouldNotBeNull();
arguments.Telemetry.ParserImplementation.ShouldBe(nameof(ArgumentParser));
arguments.Telemetry.Command.ShouldBe("gitversion");
arguments.Telemetry.ToolVersion.ShouldNotBeNullOrWhiteSpace();
arguments.Telemetry.ContinuousIntegrationProvider.ShouldBe(TelemetryContextValues.Unknown);
arguments.Telemetry.InvocationSource.ShouldBe(TelemetryContextValues.Direct);
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.Path && argument.Values.Single() == "<redacted:path>");
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.LogFile && argument.Values.Single() == "<redacted:path>");
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.Output && argument.Values.Single() == "buildserver");
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.Url && argument.Values.Single() == "<redacted:sensitive>");
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.Username && argument.Values.Single() == "<redacted:sensitive>");
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.Password && argument.Values.Single() == "<redacted:sensitive>");
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.DynamicRepoLocation && argument.Values.Single() == "<redacted:path>");
}

[Test]
public void UnknownOutputShouldThrow()
{
Expand Down
1 change: 1 addition & 0 deletions src/GitVersion.App.Tests/HelpWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public void AllArgsAreInHelp()
var helpText = string.Empty;

this.helpWriter.WriteTo(s => helpText = s);
helpText.ShouldContain("/telemetryoptout");

var ignored = new[]
{
Expand Down
45 changes: 45 additions & 0 deletions src/GitVersion.App.Tests/LegacyArgumentParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
public void TargetDirectoryAndLogFilePathCanBeParsed()
{
var arguments = this.argumentParser.ParseArguments("targetDirectoryPath -l logFilePath");
arguments.TargetPath.ShouldBe("targetDirectoryPath");

Check warning on line 82 in src/GitVersion.App.Tests/LegacyArgumentParserTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of using this literal 'targetDirectoryPath' 5 times.

See more on https://sonarcloud.io/project/issues?id=GitTools_GitVersion&issues=AZ3Z6Z49dZz2WwURDTn-&open=AZ3Z6Z49dZz2WwURDTn-&pullRequest=4927
arguments.LogFilePath.ShouldBe("logFilePath");
arguments.IsHelp.ShouldBe(false);
}
Expand All @@ -94,6 +94,51 @@
arguments.IsHelp.ShouldBe(false);
}

[Test]
public void TelemetryOptOutCanBeParsed()
{
var arguments = this.argumentParser.ParseArguments("/telemetryoptout");
arguments.TelemetryOptOut.ShouldBe(true);
}

[Test]
public void TelemetryOptOutCanBeProvidedByEnvironment()
{
this.environment.SetEnvironmentVariable(TelemetryPolicy.GitVersionTelemetryOptOutEnvironmentVariable, "true");

var arguments = this.argumentParser.ParseArguments("targetDirectoryPath");

arguments.TelemetryOptOut.ShouldBe(true);
}

[Test]
public void TelemetryPayloadIncludesParserVersionAndRedactedArguments()
{
var arguments = this.argumentParser.ParseArguments(
"targetDirectoryPath /l console /output buildserver /format {SemVer} /url https://example.com/repo.git /u user /p pass /dynamicRepoLocation /tmp/dynamic");

arguments.Telemetry.ShouldNotBeNull();
arguments.Telemetry.ParserImplementation.ShouldBe(nameof(LegacyArgumentParser));
arguments.Telemetry.Command.ShouldBe("gitversion");
arguments.Telemetry.ToolVersion.ShouldNotBeNullOrWhiteSpace();
arguments.Telemetry.ContinuousIntegrationProvider.ShouldBe(TelemetryContextValues.Unknown);
arguments.Telemetry.InvocationSource.ShouldBe(TelemetryContextValues.Direct);
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.Path && argument.Values.Single() == "<redacted:path>");
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.LogFile && argument.Values.Single() == "<redacted:path>");
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.Output && argument.Values.Single() == "buildserver");
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.Url && argument.Values.Single() == "<redacted:sensitive>");
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.Username && argument.Values.Single() == "<redacted:sensitive>");
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.Password && argument.Values.Single() == "<redacted:sensitive>");
arguments.Telemetry.Arguments.ShouldContain(argument =>
argument.Name == TelemetryArgumentNames.DynamicRepoLocation && argument.Values.Single() == "<redacted:path>");
}

[Test]
public void UnknownOutputShouldThrow()
{
Expand Down
106 changes: 106 additions & 0 deletions src/GitVersion.App.Tests/TelemetryContextEnricherTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using GitVersion.OutputVariables;

namespace GitVersion.App.Tests;

[TestFixture]
public class TelemetryContextEnricherTests
{
[Test]
public void EnrichMapsSupportedContinuousIntegrationProviders()
{
var enricher = new TelemetryContextEnricher(new GitHubActions(), new TestEnvironment());

var telemetry = enricher.Enrich(CreateTelemetry());

telemetry.ContinuousIntegrationProvider.ShouldBe("github-actions");
telemetry.InvocationSource.ShouldBe(TelemetryContextValues.Direct);
}

[Test]
public void EnrichDetectsGitVersionMsBuildInvocations()
{
var environment = new TestEnvironment();
environment.SetEnvironmentVariable(TelemetryContextValues.InternalCallerEnvironmentVariable, "GitVersion.MsBuild");
var enricher = new TelemetryContextEnricher(new LocalBuild(), environment);

var telemetry = enricher.Enrich(CreateTelemetry());

telemetry.ContinuousIntegrationProvider.ShouldBe(TelemetryContextValues.Unknown);
telemetry.InvocationSource.ShouldBe(TelemetryContextValues.GitVersionMsBuild);
}

[Test]
public void EnrichDefaultsInvocationSourceToDirectForUnsupportedContext()
{
var enricher = new TelemetryContextEnricher(new UnsupportedBuildAgent(), new TestEnvironment());

var telemetry = enricher.Enrich(CreateTelemetry());

telemetry.ContinuousIntegrationProvider.ShouldBe(TelemetryContextValues.Unknown);
telemetry.InvocationSource.ShouldBe(TelemetryContextValues.Direct);
}

private static CommandLineTelemetry CreateTelemetry() => new(
"1.2.3",
nameof(ArgumentParser),
TelemetryContextValues.Unknown,
TelemetryContextValues.Direct,
"gitversion",
null,
[]);

private sealed class TestEnvironment : IEnvironment
{
private readonly Dictionary<string, string?> variables = new(StringComparer.Ordinal);

public string? GetEnvironmentVariable(string variableName) =>
this.variables.TryGetValue(variableName, out var value) ? value : null;

public void SetEnvironmentVariable(string variableName, string? value)
{
if (value == null)
{
this.variables.Remove(variableName);
return;
}

this.variables[variableName] = value;
}
}

private sealed class GitHubActions : Agents.ICurrentBuildAgent
{
public bool IsDefault => false;
public bool CanApplyToCurrentContext() => true;
public string? GetCurrentBranch(bool usingDynamicRepos) => null;
public bool PreventFetch() => true;
public bool ShouldCleanUpRemotes() => false;
public void WriteIntegration(Action<string?> writer, GitVersionVariables variables, bool updateBuildNumber = true)
{
}
}

private sealed class LocalBuild : Agents.ICurrentBuildAgent
{
public bool IsDefault => true;
public bool CanApplyToCurrentContext() => true;
public string? GetCurrentBranch(bool usingDynamicRepos) => null;
public bool PreventFetch() => true;
public bool ShouldCleanUpRemotes() => false;
public void WriteIntegration(Action<string?> writer, GitVersionVariables variables, bool updateBuildNumber = true)
{
}
}

private sealed class UnsupportedBuildAgent : Agents.ICurrentBuildAgent
{
public bool IsDefault => false;
public bool CanApplyToCurrentContext() => true;
public string? GetCurrentBranch(bool usingDynamicRepos) => null;
public bool PreventFetch() => true;
public bool ShouldCleanUpRemotes() => false;
public void WriteIntegration(Action<string?> writer, GitVersionVariables variables, bool updateBuildNumber = true)
{
}
}
}
Loading
Loading