|
2 | 2 | // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. |
3 | 3 | // See the LICENSE file in the project root for more information |
4 | 4 |
|
| 5 | +using System.IO.Abstractions; |
5 | 6 | using Elastic.Documentation; |
6 | 7 | using Elastic.Documentation.Configuration; |
7 | 8 | using Elastic.Documentation.Configuration.Assembler; |
@@ -287,37 +288,91 @@ private void ExtractBundlesFolderPath() |
287 | 288 | } |
288 | 289 |
|
289 | 290 | /// <summary> |
290 | | - /// Reserved for future config loading (e.g., bundle.directory). The directive no longer applies rules.publish. |
291 | | - /// Emits a warning when an explicit :config: path is specified but the file is not found. |
| 291 | + /// Loads changelog configuration settings from the config file. |
| 292 | + /// Uses the explicit :config: path if specified, otherwise auto-discovers changelog.yml. |
| 293 | + /// Reserved for future directive-relevant settings. |
292 | 294 | /// </summary> |
293 | | - private void LoadConfiguration() |
294 | | - { |
295 | | - if (string.IsNullOrWhiteSpace(ConfigPath)) |
296 | | - return; |
| 295 | + private void LoadConfiguration() => |
| 296 | + // Config file resolution is kept so the path validation infrastructure |
| 297 | + // stays exercised; settings are currently handled at bundle time. |
| 298 | + _ = ResolveConfigPath(); |
297 | 299 |
|
298 | | - var trimmedPath = ConfigPath.TrimStart('/'); |
299 | | - if (Path.IsPathRooted(trimmedPath)) |
| 300 | + /// <summary> |
| 301 | + /// The trust boundary for changelog config file resolution: checkout (git) root |
| 302 | + /// when available, otherwise the documentation source directory. |
| 303 | + /// Both explicit <c>:config:</c> paths and auto-discovered candidates are validated |
| 304 | + /// against this same root. |
| 305 | + /// </summary> |
| 306 | + private IDirectoryInfo ConfigTrustRoot => |
| 307 | + Build.DocumentationCheckoutDirectory ?? Build.DocumentationSourceDirectory; |
| 308 | + |
| 309 | + private string? ResolveConfigPath() |
| 310 | + { |
| 311 | + if (!string.IsNullOrWhiteSpace(ConfigPath)) |
300 | 312 | { |
301 | | - this.EmitError("Changelog config path must not be an absolute path."); |
302 | | - return; |
| 313 | + // A leading '/' or '\' is treated as relative to docset root |
| 314 | + var trimmedPath = ConfigPath.TrimStart('/', '\\'); |
| 315 | + if (Path.IsPathRooted(trimmedPath)) |
| 316 | + { |
| 317 | + this.EmitError("Changelog config path must not be an absolute path."); |
| 318 | + return null; |
| 319 | + } |
| 320 | + |
| 321 | + var explicitPath = Path.GetFullPath(Build.DocumentationSourceDirectory.ResolvePathFrom(trimmedPath)); |
| 322 | + return ValidateConfigCandidate(explicitPath, emitDiagnostics: true); |
303 | 323 | } |
304 | 324 |
|
305 | | - var explicitPath = Path.GetFullPath(Build.DocumentationSourceDirectory.ResolvePathFrom(ConfigPath)); |
306 | | - var file = Build.ReadFileSystem.FileInfo.New(explicitPath); |
307 | | - if (!file.IsSubPathOf(Build.DocumentationSourceDirectory)) |
| 325 | + // Auto-discover: try .yml and .yaml in each candidate location. |
| 326 | + string[] relativePaths = |
| 327 | + [ |
| 328 | + "changelog.yml", "changelog.yaml", |
| 329 | + "../changelog.yml", "../changelog.yaml" |
| 330 | + ]; |
| 331 | + |
| 332 | + return relativePaths |
| 333 | + .Select(rel => Path.GetFullPath(Build.DocumentationSourceDirectory.ResolvePathFrom(rel))) |
| 334 | + .Select(abs => ValidateConfigCandidate(abs, emitDiagnostics: false)) |
| 335 | + .FirstOrDefault(p => p != null); |
| 336 | + } |
| 337 | + |
| 338 | + /// <summary> |
| 339 | + /// Validates a config file candidate against the shared trust rules: |
| 340 | + /// must be within <see cref="ConfigTrustRoot"/>, must not be/traverse symlinks, |
| 341 | + /// and must exist on the (scoped) filesystem. |
| 342 | + /// </summary> |
| 343 | + private string? ValidateConfigCandidate(string fullPath, bool emitDiagnostics) |
| 344 | + { |
| 345 | + try |
308 | 346 | { |
309 | | - this.EmitError("Changelog config path must resolve within the documentation source directory."); |
310 | | - return; |
311 | | - } |
| 347 | + var file = Build.ReadFileSystem.FileInfo.New(fullPath); |
| 348 | + |
| 349 | + if (!file.IsSubPathOf(ConfigTrustRoot)) |
| 350 | + { |
| 351 | + if (emitDiagnostics) |
| 352 | + this.EmitError("Changelog config path must resolve within the documentation directory."); |
| 353 | + return null; |
| 354 | + } |
| 355 | + |
| 356 | + if (SymlinkValidator.ValidateFileAccess(file, ConfigTrustRoot) is { } accessError) |
| 357 | + { |
| 358 | + if (emitDiagnostics) |
| 359 | + this.EmitError(accessError); |
| 360 | + return null; |
| 361 | + } |
312 | 362 |
|
313 | | - if (SymlinkValidator.ValidateFileAccess(file, Build.DocumentationSourceDirectory) is { } accessError) |
| 363 | + if (!Build.ReadFileSystem.File.Exists(fullPath)) |
| 364 | + { |
| 365 | + if (emitDiagnostics) |
| 366 | + this.EmitWarning($"Specified changelog config path '{ConfigPath}' not found."); |
| 367 | + return null; |
| 368 | + } |
| 369 | + |
| 370 | + return fullPath; |
| 371 | + } |
| 372 | + catch |
314 | 373 | { |
315 | | - this.EmitError(accessError); |
316 | | - return; |
| 374 | + return null; |
317 | 375 | } |
318 | | - |
319 | | - if (!Build.ReadFileSystem.File.Exists(explicitPath)) |
320 | | - this.EmitWarning($"Specified changelog config path '{ConfigPath}' not found."); |
321 | 376 | } |
322 | 377 |
|
323 | 378 | /// <summary> |
|
0 commit comments