Skip to content
Closed
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
44 changes: 43 additions & 1 deletion docs/docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ In this example, files in the `articles` directory uses `docs` as the base URL:
The `redirect_url` metadata is a simple way to create redirects in your documentation. This metadata can be added to a Markdown file in your project, and it will be used to redirect users to a new URL when they try to access the original URL:

---
redirect_url: [new URL]
redirect_url: [new URL]
---

Replace [new URL] with the URL that you want to redirect users to. You can use any valid URL, including relative URLs or external URLs.
Expand Down Expand Up @@ -116,3 +116,45 @@ Where:
- [`changefreq`](https://www.sitemaps.org/protocol.html#changefreqdef) determines how frequently the page is likely to change. Valid values are `always`, `hourly`, `daily`, `weekly`, `monthly`, `yearly`, `never`. Default to `daily`.
- [`priority`](https://www.sitemaps.org/protocol.html#priority) is the priority of this URL relative to other URLs on your site. Valid values range from 0.0 to 1.0. Default to `0.5`
- `fileOptions` is a per file config of the above options. The key is the file glob pattern and value is the sitemap options.

## Exit Codes

Docfx returns different exit codes to indicate the result of the operation. This is useful for CI/CD pipelines to determine if the build was successful.

| Exit Code | Name | Description |
|-----------|------|-------------|
| 0 | Success | Build completed successfully with no errors. |
| 1 | SuccessWithWarnings | Build completed but with warnings (only with `--strict` flag). |
| 2 | BuildError | Documentation build failed (invalid markdown, broken refs, etc.). |
| 3 | ConfigError | Invalid `docfx.json` or configuration error. |
| 4 | InputError | Source files not found or invalid paths. |
| 5 | MetadataError | .NET API metadata extraction failed. |
| 6 | TemplateError | Template preprocessing or rendering failed. |
| 130 | UserCancelled | Operation was cancelled by user (Ctrl+C). |
| 255 | UnhandledException | An unexpected fatal error occurred. |

### Exit Code Options

The following command-line options control exit code behavior:

- `--strict`: Return exit code `1` when warnings are present. By default, warnings do not affect the exit code (returns `0`).
- `--legacy-exit-codes`: Use the legacy exit code behavior for backward compatibility:
- `0` for success (with or without warnings)
- `-1` for any error

### Examples

Check for warnings in a CI pipeline:

```bash
docfx build --strict
if [ $? -eq 1 ]; then
echo "Build succeeded but has warnings"
fi
```

Use legacy exit codes for backward compatibility with existing scripts:

```bash
docfx build --legacy-exit-codes
```
48 changes: 48 additions & 0 deletions src/Docfx.Common/Loggers/Logger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ public static class Logger
{
public const int WarningThrottling = 10000;
public static bool HasError { get; private set; }
public static bool HasConfigError { get; private set; }
public static bool HasBuildError { get; private set; }
public static bool HasMetadataError { get; private set; }
public static bool HasTemplateError { get; private set; }
public static bool HasInputError { get; private set; }
public static int WarningCount => _warningCount;
public static int ErrorCount => _errorCount;

Expand Down Expand Up @@ -98,12 +103,50 @@ private static void Log(LogItem item)
{
HasError = true;
Interlocked.Increment(ref _errorCount);
CategorizeError(item.Code);
}

Debug.WriteLine(item.Message);
_syncListener.WriteLine(item);
}

private static void CategorizeError(string code)
{
if (string.IsNullOrEmpty(code))
{
HasBuildError = true;
return;
}

// Categorize based on error code patterns from ErrorCodes class
if (code.StartsWith("Config", StringComparison.OrdinalIgnoreCase) ||
code == ErrorCodes.Build.ViolateSchema)
{
HasConfigError = true;
}
else if (code.StartsWith("Metadata", StringComparison.OrdinalIgnoreCase))
{
HasMetadataError = true;
}
else if (code.StartsWith("Template", StringComparison.OrdinalIgnoreCase) ||
code == ErrorCodes.Template.ApplyTemplatePreprocessorError ||
code == ErrorCodes.Template.ApplyTemplateRendererError)
{
HasTemplateError = true;
}
else if (code == ErrorCodes.Build.InvalidInputFile ||
code == ErrorCodes.Build.InvalidRelativePath ||
code.StartsWith("FileNotFound", StringComparison.OrdinalIgnoreCase) ||
code.StartsWith("InvalidPath", StringComparison.OrdinalIgnoreCase))
{
HasInputError = true;
}
else
{
HasBuildError = true;
}
}

public static void Log(LogLevel level, string message, string phase = null, string file = null, string line = null, string code = null)
{
Log(new LogItem
Expand Down Expand Up @@ -190,6 +233,11 @@ public static void ResetCount()
_warningCount = 0;
_errorCount = 0;
HasError = false;
HasConfigError = false;
HasBuildError = false;
HasMetadataError = false;
HasTemplateError = false;
HasInputError = false;
}

class LogItem : ILogItem
Expand Down
6 changes: 4 additions & 2 deletions src/docfx/Models/CommandHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,24 @@ public static int Run(Action run)

public static int Run(LogOptions options, Action run)
{
ExitCodeHelper.Reset();
SetupLogger(options);

run();

CleanupLogger();
return Logger.HasError ? -1 : 0;
return ExitCodeHelper.DetermineExitCode(options);
}

public static async Task<int> RunAsync(LogOptions options, Func<Task> run)
{
ExitCodeHelper.Reset();
SetupLogger(options);

await run();

CleanupLogger();
return Logger.HasError ? -1 : 0;
return ExitCodeHelper.DetermineExitCode(options);
}

public static bool IsTcpPortAlreadyUsed(string? host, int? port)
Expand Down
70 changes: 70 additions & 0 deletions src/docfx/Models/ExitCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Docfx;

/// <summary>
/// Exit codes returned by docfx CLI commands.
/// </summary>
/// <remarks>
/// Use <c>--legacy-exit-codes</c> flag to revert to the old behavior (0 for success, -1 for any error).
/// Use <c>--strict</c> flag to return exit code 1 when warnings are present.
/// </remarks>
public enum ExitCode
{
/// <summary>
/// Build completed successfully with no errors.
/// When <c>--strict</c> is not specified, this is also returned when warnings are present.
/// </summary>
Success = 0,

/// <summary>
/// Build completed successfully but with warnings.
/// Only returned when <c>--strict</c> flag is specified.
/// </summary>
SuccessWithWarnings = 1,

/// <summary>
/// Documentation build failed due to errors such as invalid markdown,
/// broken references, invalid YAML files, or TOC errors.
/// </summary>
BuildError = 2,

/// <summary>
/// Configuration error occurred, such as invalid docfx.json,
/// missing required fields, or schema validation failures.
/// </summary>
ConfigError = 3,

/// <summary>
/// Input error occurred, such as source files not found,
/// invalid paths, or inaccessible files.
/// </summary>
InputError = 4,

/// <summary>
/// .NET API metadata extraction failed.
/// </summary>
MetadataError = 5,

/// <summary>
/// Template preprocessing or rendering failed.
/// </summary>
TemplateError = 6,

/// <summary>
/// Operation was cancelled by user (Ctrl+C / SIGINT).
/// Follows Unix convention: 128 + signal number (SIGINT = 2).
/// </summary>
UserCancelled = 130,

/// <summary>
/// An unexpected fatal error occurred.
/// </summary>
UnhandledException = 255,

/// <summary>
/// Legacy exit code for any error (used with <c>--legacy-exit-codes</c> flag).
/// </summary>
LegacyError = -1,
}
134 changes: 134 additions & 0 deletions src/docfx/Models/ExitCodeHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Docfx.Common;

namespace Docfx;

/// <summary>
/// Helper class for determining the appropriate exit code based on Logger state and options.
/// </summary>
internal static class ExitCodeHelper
{
/// <summary>
/// Gets or sets whether the operation was cancelled by the user (Ctrl+C).
/// </summary>
public static volatile bool IsCancelled;

/// <summary>
/// Determines the appropriate exit code based on the current Logger state and options.
/// </summary>
/// <param name="options">The log options containing strict and legacy flags.</param>
/// <returns>The exit code as an integer.</returns>
public static int DetermineExitCode(LogOptions options)
{
return DetermineExitCode(options?.Strict ?? false, options?.LegacyExitCodes ?? false);
}

/// <summary>
/// Determines the appropriate exit code based on the current Logger state and options.
/// </summary>
/// <param name="strict">If true, return exit code 1 when warnings are present.</param>
/// <param name="legacyExitCodes">If true, use legacy exit codes (0 for success, -1 for any error).</param>
/// <returns>The exit code as an integer.</returns>
public static int DetermineExitCode(bool strict = false, bool legacyExitCodes = false)
{
// Check for user cancellation first
if (IsCancelled)
{
return legacyExitCodes ? (int)ExitCode.LegacyError : (int)ExitCode.UserCancelled;
}

// Check for errors
if (Logger.HasError)
{
if (legacyExitCodes)
{
return (int)ExitCode.LegacyError;
}

// Return the most specific error category
return (int)GetErrorCategory();
}

// Check for warnings with strict mode
if (strict && Logger.WarningCount > 0)
{
return legacyExitCodes ? (int)ExitCode.Success : (int)ExitCode.SuccessWithWarnings;
}

return (int)ExitCode.Success;
}

/// <summary>
/// Gets the most appropriate error category based on Logger state.
/// Priority: Config > Input > Metadata > Template > Build
/// </summary>
private static ExitCode GetErrorCategory()
{
// Configuration errors take highest priority as they prevent any work from starting
if (Logger.HasConfigError)
{
return ExitCode.ConfigError;
}

// Input errors (file not found, etc.) are next
if (Logger.HasInputError)
{
return ExitCode.InputError;
}

// Metadata extraction errors
if (Logger.HasMetadataError)
{
return ExitCode.MetadataError;
}

// Template errors
if (Logger.HasTemplateError)
{
return ExitCode.TemplateError;
}

// Default to build error for all other errors
return ExitCode.BuildError;
}

/// <summary>
/// Maps an exception to the appropriate exit code.
/// </summary>
/// <param name="exception">The exception that occurred.</param>
/// <param name="legacyExitCodes">If true, use legacy exit codes.</param>
/// <returns>The exit code as an integer.</returns>
public static int GetExitCodeForException(Exception exception, bool legacyExitCodes = false)
{
if (legacyExitCodes)
{
return (int)ExitCode.LegacyError;
}

// Unwrap AggregateException to get the actual exception type
if (exception is AggregateException ae && ae.InnerExceptions.Count == 1)
{
exception = ae.InnerExceptions[0];
}

return exception switch
{
OperationCanceledException => (int)ExitCode.UserCancelled,
FileNotFoundException => (int)ExitCode.InputError,
DirectoryNotFoundException => (int)ExitCode.InputError,
System.Text.Json.JsonException => (int)ExitCode.ConfigError,
Newtonsoft.Json.JsonException => (int)ExitCode.ConfigError,
_ => (int)ExitCode.UnhandledException,
};
}

/// <summary>
/// Resets the cancellation state. Should be called at the start of each command.
/// </summary>
public static void Reset()
{
IsCancelled = false;
}
}
8 changes: 8 additions & 0 deletions src/docfx/Models/LogOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@ internal class LogOptions : CommandSettings
[Description("Treats warnings as errors")]
[CommandOption("--warningsAsErrors")]
public bool WarningsAsErrors { get; set; }

[Description("Return exit code 1 if warnings are present (default: warnings return 0)")]
[CommandOption("--strict")]
public bool Strict { get; set; }

[Description("Use legacy exit codes (0 for success, -1 for any error) for backward compatibility")]
[CommandOption("--legacy-exit-codes")]
public bool LegacyExitCodes { get; set; }
}
Loading
Loading