Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
75 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
a013f99
Fix admonition tests
cotti Dec 15, 2025
2675baf
Transfer applicablity popover data properly as JSON
cotti Dec 15, 2025
f39f3d5
Merge remote-tracking branch 'origin/main' into feat/versionspec
cotti Dec 15, 2025
004ed8e
Avoid duplicate warnings
cotti Dec 15, 2025
e574153
Adjust existing tests
cotti Dec 15, 2025
171ef22
Merge remote-tracking branch 'origin/main' into feat/versionspec
cotti Dec 15, 2025
f824af8
Fix lint
cotti Dec 15, 2025
cb4b386
Introduce more test scenarios
cotti Dec 15, 2025
9e2875e
Adjust cursor behavior on desktop
cotti Dec 15, 2025
38d912b
Only use 'since' when a version is set
cotti Dec 15, 2025
ea8776f
Fix presentation of applicability details
cotti Dec 15, 2025
6852739
Readjust applicability ordering
cotti Dec 15, 2025
1899559
Adjust version display when no lifecycles are stated
cotti Dec 15, 2025
501fb5e
Handle multiple future versions
cotti Dec 15, 2025
2359938
Fix test and ranges between versions
cotti Dec 15, 2025
3af43c4
Typo
cotti Dec 15, 2025
4f7f107
Remove unused value
cotti Dec 15, 2025
8bf4658
Allow special rule for ranges
cotti Dec 15, 2025
ebca4ff
Allow version bump overlaps
cotti Dec 16, 2025
0be4340
Adjust availability list item display
cotti Dec 16, 2025
4c7bd53
Adjust tests
cotti Dec 16, 2025
40a3e59
Remove duplicate information
cotti Dec 17, 2025
5906947
Fix badge display ordering
cotti Dec 17, 2025
ae1186a
Temporary downgrade the severity of new warnings
cotti Dec 17, 2025
5ed1ed4
Merge remote-tracking branch 'origin/main' into feat/versionspec
cotti Dec 17, 2025
f4cd2de
Remove a few instantiations in favor of static definitions
cotti Dec 17, 2025
a7a55e4
Remove code duplications
cotti Dec 17, 2025
86fce10
Fix tests
cotti Dec 17, 2025
2724eb7
Fix range rendering when the difference is at the patch level
cotti Dec 17, 2025
c4b6fd4
Show range-level versions on applicability items
cotti Dec 17, 2025
0cf82bb
Update src/Elastic.Documentation.Site/Assets/web-components/AppliesTo…
cotti Dec 17, 2025
a7f0364
Update src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs
cotti Dec 17, 2025
da72ba0
Fix tests
cotti Dec 17, 2025
7d8546c
lint fix
cotti Dec 17, 2025
e293bd6
Revert showing patch-level versions in applicability headers
cotti Dec 17, 2025
34e97a6
Add applicability ruleset tables to the docs
cotti Dec 17, 2025
d45bcf9
Remove redundant snippet
cotti Dec 17, 2025
c6fb94a
Merge remote-tracking branch 'origin/main' into feat/versionspec
cotti Dec 17, 2025
788e680
Merge branch 'main' into feat/versionspec
cotti Dec 23, 2025
b1d1ddc
Include explicit ! operator to display patch versions.
cotti Dec 30, 2025
64eea66
Match EUI tooltip default delay
cotti Dec 30, 2025
56583cb
Merge branch 'main' into feat/versionspec
cotti Jan 5, 2026
05d1fe8
Adjust unavailable lifecycle description and add a link to the releas…
cotti Jan 6, 2026
9ddfbdd
Merge remote-tracking branch 'origin/main' into feat/versionspec
cotti Jan 6, 2026
7e35036
Fix remaining test
cotti Jan 6, 2026
b637f9e
Fix rendering
cotti Jan 6, 2026
e0f6c1e
Adjust version syntax demonstration
cotti Jan 6, 2026
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