Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9abe711
Introduce VersionSpec, a model meant for advanced version handling fo…
cotti Dec 7, 2025
73e8dd7
Apply usage of VersionSpec in Applicability
cotti Dec 8, 2025
e6d710f
Show base version if we don't get a specified version for a versioned…
cotti Dec 8, 2025
4bd996a
Adjust tests to better match the currently expected output
cotti Dec 8, 2025
7ead716
Add VersionSpec to YamlSerialization
cotti Dec 8, 2025
fcae628
Typo!
cotti Dec 8, 2025
e435de9
No need for it to be initialized here after all.
cotti Dec 8, 2025
7f5f2be
Handle "all" explicitly
cotti Dec 8, 2025
00ab8d0
Adopting a few review suggestions
cotti Dec 8, 2025
2613a13
Fix warnings on docs-builder docs
cotti Dec 8, 2025
d4cce98
Include applicability table in req.md
cotti Dec 8, 2025
043c470
Fix markdown formatting
cotti Dec 8, 2025
11580bd
Products with versions should show their base versions in badges with…
cotti Dec 8, 2025
56af075
Introduce implicit semantics for multiple lifecycles
cotti Dec 11, 2025
7d0f792
Add more examples in docs
cotti Dec 11, 2025
13301d0
Typo
cotti Dec 11, 2025
7cd899b
Fix interpretation of lifecycle - using ranges after current stack ve…
cotti Dec 11, 2025
072479e
Change popup to a popover component, alongside static descriptions
cotti Dec 12, 2025
8db2669
Preview: send a limited assembler build to the preview environment
cotti Dec 12, 2025
d953e71
Merge remote-tracking branch 'origin/main' into feat/versionspec
cotti Dec 12, 2025
817cfc6
Fix Call to the Renderer
cotti Dec 12, 2025
0f2ce56
Remove unused var
cotti Dec 12, 2025
917d18b
typo
cotti Dec 12, 2025
6a1fde8
Invalidate correct subfolder
cotti Dec 12, 2025
0458208
Fix policy path
cotti Dec 12, 2025
cd331f0
Fix policy
cotti Dec 12, 2025
d728b6a
Send path-prefix to assembler.
cotti Dec 12, 2025
b799f69
Revert temporary assembler build for now
cotti Dec 12, 2025
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
14 changes: 10 additions & 4 deletions docs/_snippets/applies_to-version.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
`applies_to` accepts the following version formats:

* `Major.Minor`
* `Major.Minor.Patch`
* **Greater than or equal to**: `x.x+`, `x.x`, `x.x.x+`, `x.x.x` (default behavior when no operator specified)
* **Range (inclusive)**: `x.x-y.y`, `x.x.x-y.y.y`, `x.x-y.y.y`, `x.x.x-y.y`
* **Exact version**: `=x.x`, `=x.x.x`

Regardless of the version format used in the source file, the version number is always rendered in the `Major.Minor.Patch` format.
**Version Display:**

- Versions are always displayed as **Major.Minor** (e.g., `9.1`) in badges, regardless of the format used in source files.
- Each version represents the **latest patch** of that minor version (e.g., `9.1` means 9.1.0, 9.1.1, 9.1.6, etc.).
- The `+` symbol indicates "this version and later" (e.g., `9.1+` means 9.1.0 and all subsequent releases).
- Ranges show both versions (e.g., `9.0-9.2`) when both are released, or convert to `+` format if the end version is unreleased.

:::{note}
**Automatic Version Sorting**: When you specify multiple versions for the same product, the build system automatically sorts them in descending order (highest version first) regardless of the order you write them in the source file. For example, `stack: ga 8.18.6, ga 9.1.2, ga 8.19.2, ga 9.0.6` will be displayed as `stack: ga 9.1.2, ga 9.0.6, ga 8.19.2, ga 8.18.6`. Items without versions (like `ga` without a version or `all`) are sorted last.
**Automatic Version Sorting**: When you specify multiple versions for the same product, the build system automatically sorts them in descending order (highest version first) regardless of the order you write them in the source file. For example, `stack: ga 9.1, beta 9.0, preview 8.18` will be displayed with the highest priority lifecycle and version first. Items without versions are sorted last.
:::
91 changes: 83 additions & 8 deletions docs/syntax/applies.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,41 @@ Where:
- The lifecycle is mandatory.
- The version is optional.

### Version Syntax

Versions can be specified using several formats to indicate different applicability scenarios:

| Description | Syntax | Example | Badge Display |
|:------------|:-------|:--------|:--------------|
| **Greater than or equal to** (default) | `x.x+` `x.x` `x.x.x+` `x.x.x` | `ga 9.1` or `ga 9.1+` | `9.1+` |
| **Range** (inclusive) | `x.x-y.y` `x.x.x-y.y.y` | `preview 9.0-9.2` | `9.0-9.2` or `9.0+`* |
| **Exact version** | `=x.x` `=x.x.x` | `beta =9.1` | `9.1` |

\* Range display depends on release status of the second version.

**Important notes:**

- Versions are always displayed as **Major.Minor** (e.g., `9.1`) in badges, regardless of whether you specify patch versions in the source.
- Each version statement corresponds to the **latest patch** of the specified minor version (e.g., `9.1` represents 9.1.0, 9.1.1, 9.1.6, etc.).
- When critical patch-level differences exist, use plain text descriptions alongside the badge rather than specifying patch versions.

### Version Validation Rules

The build process enforces the following validation rules:

- **One version per lifecycle**: Each lifecycle (GA, Preview, Beta, etc.) can only have one version declaration.
- ✅ `stack: ga 9.2+, beta 9.0-9.1`
- ❌ `stack: ga 9.2, ga 9.3`
- **One "greater than" per key**: Only one lifecycle per product key can use the `+` (greater than or equal to) syntax.
- ✅ `stack: ga 9.2+, beta 9.0-9.1`
- ❌ `stack: ga 9.2+, beta 9.0+`
- **Valid range order**: In ranges, the first version must be less than or equal to the second version.
- ✅ `stack: preview 9.0-9.2`
- ❌ `stack: preview 9.2-9.0`
- **No version overlaps**: Versions for the same key cannot overlap (ranges are inclusive).
- ✅ `stack: ga 9.2+, beta 9.0-9.1`
- ❌ `stack: ga 9.2+, beta 9.0-9.2`

### Page level

Page level annotations are added in the YAML frontmatter, starting with the `applies_to` key and following the [key-value reference](#key-value-reference). For example:
Expand Down Expand Up @@ -134,6 +169,22 @@ Use the following key-value reference to find the appropriate key and value for

## Examples

### Version Syntax Examples

The following table demonstrates the various version syntax options and their rendered output:

| Source Syntax | Description | Badge Display | Notes |
|:-------------|:------------|:--------------|:------|
| `stack: ga 9.1` | Greater than or equal to 9.1 | `Stack│9.1+` | Default behavior, equivalent to `9.1+` |
| `stack: ga 9.1+` | Explicit greater than or equal to | `Stack│9.1+` | Explicit `+` syntax |
| `stack: preview 9.0-9.2` | Range from 9.0 to 9.2 (inclusive) | `Stack│Preview 9.0-9.2` | Shows range if 9.2.0 is released |
| `stack: preview 9.0-9.3` | Range where end is unreleased | `Stack│Preview 9.0+` | Shows `+` if 9.3.0 is not released |
| `stack: beta =9.1` | Exact version 9.1 only | `Stack│Beta 9.1` | No `+` symbol for exact versions |
| `stack: ga 9.2+, beta 9.0-9.1` | Multiple lifecycles | `Stack│9.2+` | Only highest priority lifecycle shown |
| `stack: ga 9.3, beta 9.1+` | Unreleased GA with Preview | `Stack│Beta 9.1+` | Shows Beta when GA unreleased with 2+ lifecycles |
| `serverless: ga` | No version (base 99999) | `Serverless` | No version badge for unversioned products |
| `deployment:`<br/>` ece: ga 9.0+` | Nested deployment syntax | `ECE│9.0+` | Deployment products shown separately |

### Versioning examples

Versioned products require a `version` tag to be used with the `lifecycle` tag:
Expand Down Expand Up @@ -240,22 +291,46 @@ applies_to:

## Look and feel

### Version Syntax Demonstrations

:::::{dropdown} New version syntax examples

The following examples demonstrate the new version syntax capabilities:

**Greater than or equal to:**
- {applies_to}`stack: ga 9.1` (implicit `+`)
- {applies_to}`stack: ga 9.1+` (explicit `+`)
- {applies_to}`stack: preview 9.0+`

**Ranges:**
- {applies_to}`stack: preview 9.0-9.2` (range display when both released)
- {applies_to}`stack: beta 9.1-9.3` (converts to `+` if end unreleased)

**Exact versions:**
- {applies_to}`stack: beta =9.1` (no `+` symbol)
- {applies_to}`stack: deprecated =9.0`

**Multiple lifecycles:**
- {applies_to}`stack: ga 9.2+, beta 9.0-9.1` (shows highest priority)

:::::

### Block

:::::{dropdown} Block examples

```{applies_to}
stack: preview 9.1
stack: preview 9.1+
serverless: ga

apm_agent_dotnet: ga 1.0.0
apm_agent_java: beta 1.0.0
edot_dotnet: preview 1.0.0
apm_agent_dotnet: ga 1.0+
apm_agent_java: beta 1.0+
edot_dotnet: preview 1.0+
edot_python:
edot_node: ga 1.0.0
elasticsearch: preview 9.0.0
security: removed 9.0.0
observability: deprecated 9.0.0
edot_node: ga 1.0+
elasticsearch: preview 9.0+
security: removed 9.0
observability: deprecated 9.0+
```
:::::

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ private bool ShouldIncludeOperation(OpenApiOperation operation, string product)
return true; // Could not parse version, safe to include

// Get current version for the product
var versioningSystemId = product == "elasticsearch"
var versioningSystemId = product.Equals("elasticsearch", StringComparison.OrdinalIgnoreCase)
? VersioningSystemId.Stack
: VersioningSystemId.Stack; // Both use Stack for now

Expand Down Expand Up @@ -294,14 +294,14 @@ private static ProductLifecycle ParseLifecycle(string stateValue)
/// <summary>
/// Parses the version from "Added in X.Y.Z" pattern in the x-state string.
/// </summary>
private static SemVersion? ParseVersion(string stateValue)
private static VersionSpec? ParseVersion(string stateValue)
{
var match = AddedInVersionRegex().Match(stateValue);
if (!match.Success)
return null;

var versionString = match.Groups[1].Value;
return SemVersion.TryParse(versionString, out var version) ? version : null;
return VersionSpec.TryParse(versionString, out var version) ? version : null;
}

/// <summary>
Expand Down
20 changes: 10 additions & 10 deletions src/Elastic.Documentation/AppliesTo/Applicability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static bool TryParse(string? value, IList<(Severity, string)> diagnostics
return false;

// Sort by version in descending order (the highest version first)
// Items without versions (AllVersions.Instance) are sorted last
// Items without versions (AllVersionsSpec.Instance) are sorted last
var sortedApplications = applications.OrderDescending().ToArray();
availability = new AppliesCollection(sortedApplications);
return true;
Expand Down Expand Up @@ -98,12 +98,12 @@ public override string ToString()
public record Applicability : IComparable<Applicability>, IComparable
{
public ProductLifecycle Lifecycle { get; init; }
public SemVersion? Version { get; init; }
public VersionSpec? Version { get; init; }

public static Applicability GenerallyAvailable { get; } = new()
{
Lifecycle = ProductLifecycle.GenerallyAvailable,
Version = AllVersions.Instance
Version = AllVersionsSpec.Instance
};


Expand All @@ -126,8 +126,8 @@ public string GetLifeCycleName() =>
/// <inheritdoc />
public int CompareTo(Applicability? other)
{
var xIsNonVersioned = Version is null || ReferenceEquals(Version, AllVersions.Instance);
var yIsNonVersioned = other?.Version is null || ReferenceEquals(other.Version, AllVersions.Instance);
var xIsNonVersioned = Version is null || ReferenceEquals(Version, AllVersionsSpec.Instance);
var yIsNonVersioned = other?.Version is null || ReferenceEquals(other.Version, AllVersionsSpec.Instance);

if (xIsNonVersioned && yIsNonVersioned)
return 0;
Expand Down Expand Up @@ -158,7 +158,7 @@ public override string ToString()
_ => throw new ArgumentOutOfRangeException()
};
_ = sb.Append(lifecycle);
if (Version is not null && Version != AllVersions.Instance)
if (Version is not null && Version != AllVersionsSpec.Instance)
_ = sb.Append(' ').Append(Version);
return sb.ToString();
}
Expand Down Expand Up @@ -224,10 +224,10 @@ public static bool TryParse(string? value, IList<(Severity, string)> diagnostics
? null
: tokens[1] switch
{
null => AllVersions.Instance,
"all" => AllVersions.Instance,
"" => AllVersions.Instance,
var t => SemVersionConverter.TryParse(t, out var v) ? v : null
null => AllVersionsSpec.Instance,
"all" => AllVersionsSpec.Instance,
"" => AllVersionsSpec.Instance,
var t => VersionSpec.TryParse(t, out var v) ? v : null
};
availability = new Applicability { Version = version, Lifecycle = lifecycle };
return true;
Expand Down
10 changes: 6 additions & 4 deletions src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,27 @@ public static Applicability GetPrimaryApplicability(IEnumerable<Applicability> a
};

var availableApplicabilities = applicabilityList
.Where(a => a.Version is null || a.Version is AllVersions || a.Version <= currentVersion)
.Where(a => a.Version is null || a.Version is AllVersionsSpec ||
(a.Version is VersionSpec vs && vs.Min <= currentVersion))
.ToList();

if (availableApplicabilities.Count != 0)
{
return availableApplicabilities
.OrderByDescending(a => a.Version ?? new SemVersion(0, 0, 0))
.OrderByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0))
.ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
.First();
}

var futureApplicabilities = applicabilityList
.Where(a => a.Version is not null && a.Version is not AllVersions && a.Version > currentVersion)
.Where(a => a.Version is not null && a.Version is not AllVersionsSpec &&
a.Version is VersionSpec vs && vs.Min > currentVersion)
.ToList();

if (futureApplicabilities.Count != 0)
{
return futureApplicabilities
.OrderBy(a => a.Version!.CompareTo(currentVersion))
.OrderBy(a => a.Version!.Min.CompareTo(currentVersion))
.ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
.First();
}
Expand Down
4 changes: 3 additions & 1 deletion src/Elastic.Documentation/AppliesTo/ApplicableTo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ public record ApplicableTo
Product = AppliesCollection.GenerallyAvailable
};

private static readonly VersionSpec DefaultVersion = VersionSpec.TryParse("9.0", out var v) ? v! : AllVersionsSpec.Instance;

public static ApplicableTo Default { get; } = new()
{
Stack = new AppliesCollection([new Applicability { Version = new SemVersion(9, 0, 0), Lifecycle = ProductLifecycle.GenerallyAvailable }]),
Stack = new AppliesCollection([new Applicability { Version = DefaultVersion, Lifecycle = ProductLifecycle.GenerallyAvailable }]),
Serverless = ServerlessProjectApplicability.All
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class ApplicableToJsonConverter : JsonConverter<ApplicableTo>
string? type = null;
string? subType = null;
var lifecycle = ProductLifecycle.GenerallyAvailable;
SemVersion? version = null;
VersionSpec? version = null;

while (reader.Read())
{
Expand Down Expand Up @@ -72,7 +72,7 @@ public class ApplicableToJsonConverter : JsonConverter<ApplicableTo>
break;
case "version":
var versionStr = reader.GetString();
if (versionStr != null && SemVersionConverter.TryParse(versionStr, out var v))
if (versionStr != null && VersionSpec.TryParse(versionStr, out var v))
version = v;
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,102 @@ private static bool TryGetApplicabilityOverTime(Dictionary<object, object?> dict
if (target is null || (target is string s && string.IsNullOrWhiteSpace(s)))
availability = AppliesCollection.GenerallyAvailable;
else if (target is string stackString)
{
availability = AppliesCollection.TryParse(stackString, diagnostics, out var a) ? a : null;

if (availability is not null)
ValidateApplicabilityCollection(key, availability, diagnostics);
}
return availability is not null;
}

private static void ValidateApplicabilityCollection(string key, AppliesCollection collection, List<(Severity, string)> diagnostics)
{
var items = collection.ToList();

// Rule: Only one version declaration per lifecycle
var lifecycleGroups = items.GroupBy(a => a.Lifecycle).ToList();
foreach (var group in lifecycleGroups)
{
var lifecycleVersionedItems = group.Where(a => a.Version is not null &&
a.Version != AllVersionsSpec.Instance).ToList();
if (lifecycleVersionedItems.Count > 1)
{
diagnostics.Add((Severity.Warning,
$"Key '{key}': Multiple version declarations for {group.Key} lifecycle. Only one version per lifecycle is allowed."));
}
}

// Rule: Only one item per key can use greater-than syntax
var greaterThanItems = items.Where(a =>
a.Version is { Kind: VersionSpecKind.GreaterThanOrEqual } &&
a.Version != AllVersionsSpec.Instance).ToList();

if (greaterThanItems.Count > 1)
{
diagnostics.Add((Severity.Warning,
$"Key '{key}': Multiple items use greater-than-or-equal syntax. Only one item per key can use this syntax."));
}

// Rule: In a range, the first version must be less than or equal the last version
foreach (var item in items)
{
if (item.Version is { Kind: VersionSpecKind.Range } spec)
{
if (spec.Min.CompareTo(spec.Max!) > 0)
{
diagnostics.Add((Severity.Warning,
$"Key '{key}', {item.Lifecycle}: Range has first version ({spec.Min.Major}.{spec.Min.Minor}) greater than last version ({spec.Max!.Major}.{spec.Max.Minor})."));
}
}
}

// Rule: No overlapping version ranges for the same key
var versionedItems = items.Where(a => a.Version is not null &&
a.Version != AllVersionsSpec.Instance).ToList();

for (var i = 0; i < versionedItems.Count; i++)
{
for (var j = i + 1; j < versionedItems.Count; j++)
{
if (CheckVersionOverlap(versionedItems[i].Version!, versionedItems[j].Version!, out var overlapMsg))
{
diagnostics.Add((Severity.Warning,
$"Key '{key}': Overlapping versions between {versionedItems[i].Lifecycle} and {versionedItems[j].Lifecycle}. {overlapMsg}"));
}
}
}
}

private static bool CheckVersionOverlap(VersionSpec v1, VersionSpec v2, out string message)
{
message = string.Empty;

// Get the effective ranges for each version spec
// For GreaterThanOrEqual: [min, infinity)
// For Range: [min, max]
// For Exact: [exact, exact]

var (v1Min, v1Max) = GetEffectiveRange(v1);
var (v2Min, v2Max) = GetEffectiveRange(v2);

var overlaps = v1Min.CompareTo(v2Max ?? new SemVersion(99999, 99999, 99999)) <= 0 &&
v2Min.CompareTo(v1Max ?? new SemVersion(99999, 99999, 99999)) <= 0;

if (overlaps)
message = $"Version ranges overlap.";

return overlaps;
}

private static (SemVersion min, SemVersion? max) GetEffectiveRange(VersionSpec spec) => spec.Kind switch
{
VersionSpecKind.Exact => (spec.Min, spec.Min),
VersionSpecKind.Range => (spec.Min, spec.Max),
VersionSpecKind.GreaterThanOrEqual => (spec.Min, null),
_ => throw new ArgumentOutOfRangeException(nameof(spec), spec.Kind, "Unknown VersionSpecKind")
};

public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) =>
serializer.Invoke(value, type);
}
Loading
Loading