Skip to content
Merged
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
55 changes: 52 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ on:
branches:
- main
paths:
- build/docs/**
- build/**
- src/**
- global.json
- .config/dotnet-tools.json
- .github/actions/cache-restore/**
- docs/**
- package*.json
- markdownlint.json
Expand All @@ -23,7 +27,11 @@ on:
branches:
- main
paths:
- build/docs/**
- build/**
- src/**
- global.json
- .config/dotnet-tools.json
- .github/actions/cache-restore/**
- docs/**
- package*.json
- markdownlint.json
Expand Down Expand Up @@ -112,15 +120,39 @@ jobs:
key: node-${{ runner.os }}-${{ hashFiles('./package-lock.json') }}
restore-keys: node-${{ runner.os }}

- name: Verify documentation input selection
run: dotnet run --file docs/scripts/check-docs-inputs.cs

- name: Restore released documentation inputs
id: docs-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: artifacts/docs/cache
key: docs-inputs-v2-${{ runner.os }}-${{ hashFiles('docs/versions.json') }}
restore-keys: |
docs-inputs-v2-${{ runner.os }}-${{ hashFiles('docs/versions.json') }}-
docs-inputs-v2-${{ runner.os }}-

- name: '[Build Documentation]'
env:
GITHUB_TOKEN: ${{ github.token }}
shell: pwsh
run: dotnet run/docs.dll --target=BuildDocs

# New releases can add immutable inputs without changing the manifest.
# Save only when the verified archive set differs from the restored cache.
- name: Save released documentation inputs
if: hashFiles('artifacts/docs/cache/**/*.sha512') != '' && steps.docs-cache.outputs.cache-matched-key != format('docs-inputs-v2-{0}-{1}-{2}', runner.os, hashFiles('docs/versions.json'), hashFiles('artifacts/docs/cache/**/*.sha512'))
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: artifacts/docs/cache
key: docs-inputs-v2-${{ runner.os }}-${{ hashFiles('docs/versions.json') }}-${{ hashFiles('artifacts/docs/cache/**/*.sha512') }}

- name: '[HTMLProofer]'
uses: chabad360/htmlproofer@c2750eb7eb937599ac859517e7dd23a29f1b3ed7 # v2
with:
directory: ./artifacts/docs/preview
arguments: --ignore-urls /api/,/docs/,/5.12.0/ --allow-hash-href --allow-missing-href --assume-extension --disable-external --no-check_external_hash
arguments: --ignore-urls /api/,/docs/,/5.12.0/ --allow-hash-href --allow-missing-href --assume-extension .html --disable-external --no-check-external-hash
Comment thread
arturcic marked this conversation as resolved.

- name: '[Reviewdog Reporter]'
id: reporter
Expand Down Expand Up @@ -173,6 +205,16 @@ jobs:
key: node-${{ runner.os }}-${{ hashFiles('./package-lock.json') }}
restore-keys: node-${{ runner.os }}

- name: Restore released documentation inputs
id: docs-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: artifacts/docs/cache
key: docs-inputs-v2-${{ runner.os }}-${{ hashFiles('docs/versions.json') }}
restore-keys: |
docs-inputs-v2-${{ runner.os }}-${{ hashFiles('docs/versions.json') }}-
docs-inputs-v2-${{ runner.os }}-

- name: '[Build Schemas]'
shell: pwsh
run: dotnet run/docs.dll --target=GenerateSchemas
Expand All @@ -181,3 +223,10 @@ jobs:
if: github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch'
shell: pwsh
run: dotnet run/docs.dll --target=PublishDocs --force

- name: Save released documentation inputs
if: hashFiles('artifacts/docs/cache/**/*.sha512') != '' && steps.docs-cache.outputs.cache-matched-key != format('docs-inputs-v2-{0}-{1}-{2}', runner.os, hashFiles('docs/versions.json'), hashFiles('artifacts/docs/cache/**/*.sha512'))
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: artifacts/docs/cache
key: docs-inputs-v2-${{ runner.os }}-${{ hashFiles('docs/versions.json') }}-${{ hashFiles('artifacts/docs/cache/**/*.sha512') }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,6 @@ new-cli/logs/

# Agent skills (name collides with the VS build-output [Rr]elease/ rule above)
!.agents/skills/release/

# Local agent planning notes
/.devagent/
4 changes: 3 additions & 1 deletion build/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="AngleSharp" Version="1.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.9.0" />
<PackageVersion Include="Cake.Compression" Version="0.4.0" />
<PackageVersion Include="Cake.Codecov" Version="6.0.0" />
<PackageVersion Include="Cake.Coverlet" Version="6.0.1" />
Expand All @@ -17,4 +19,4 @@
<PackageVersion Include="Cake.Wyam" Version="2.2.14" />
<PackageVersion Include="xunit.assert" Version="2.9.3" />
</ItemGroup>
</Project>
</Project>
3 changes: 1 addition & 2 deletions build/docs/BuildContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Cake.Wyam;
using Common.Utilities;
using Docs.Utilities;

Expand All @@ -8,5 +7,5 @@ public class BuildContext(ICakeContext context) : BuildContextBase(context)
{
public bool ForcePublish { get; set; }
public Credentials? Credentials { get; set; }
public WyamSettings? WyamSettings { get; set; }
public PreparedEdition[]? DocumentationInputs { get; set; }
}
21 changes: 0 additions & 21 deletions build/docs/BuildLifetime.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Cake.Wyam;
using Common.Lifetime;
using Common.Utilities;
using Docs.Utilities;
Expand All @@ -14,26 +13,6 @@ public override void Setup(BuildContext context, ISetupContext info)
context.Credentials = Credentials.GetCredentials(context);
context.ForcePublish = context.HasArgument("force");

context.WyamSettings = new WyamSettings
{
Recipe = "Docs",
Theme = "Samson",
OutputPath = context.MakeAbsolute(Paths.ArtifactsDocs.Combine("preview")),
RootPath = context.MakeAbsolute(Paths.Docs),
Settings = new Dictionary<string, object>
{
{ "BaseEditUrl", "https://github.com/gittools/GitVersion/tree/main/docs/input/" },
{ "SourceFiles", context.MakeAbsolute(Paths.Src) + "/**/{!bin,!obj,!packages,!*.Tests,!GitTools.*,}/**/*.cs" },
{ "Title", "GitVersion" },
{ "IncludeGlobalNamespace", false },
{ "IgnoreFolders", "**/mdsource" }
},
EnvironmentVariables = new Dictionary<string, string>
{
{ "DOTNET_ROLL_FORWARD", "Major" },
},
};

context.StartGroup("Build Setup");

LogBuildInformation(context);
Expand Down
11 changes: 2 additions & 9 deletions build/docs/Tasks/BuildDocs.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Cake.Wyam;
using Common.Utilities;
using Docs.Utilities;

Expand All @@ -7,6 +6,7 @@ namespace Docs.Tasks;
[TaskName(nameof(BuildDocs))]
[TaskDescription("Builds the docs to local path")]
[IsDependentOn(typeof(Clean))]
[IsDependentOn(typeof(PrepareDocsInputs))]
[IsDependentOn(typeof(ValidateMermaidDiagrams))]
public sealed class BuildDocs : FrostingTask<BuildContext>
{
Expand All @@ -18,12 +18,5 @@ public override bool ShouldRun(BuildContext context)
return shouldRun;
}

public override void Run(BuildContext context)
{
if (context.WyamSettings is not null)
{
context.StageMermaidRuntimeForWyam();
context.Wyam(context.WyamSettings);
}
}
public override void Run(BuildContext context) => VersionedDocs.Build(context);
}
11 changes: 11 additions & 0 deletions build/docs/Tasks/PrepareDocsInputs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Docs.Utilities;

namespace Docs.Tasks;

[TaskName(nameof(PrepareDocsInputs))]
[TaskDescription("Resolve documentation trains and cache their release inputs")]
public sealed class PrepareDocsInputs : FrostingTask<BuildContext>
{
public override void Run(BuildContext context) => context.DocumentationInputs = new DocsInputs(
context.Environment.WorkingDirectory.FullPath, message => context.Information(message)).Prepare().GetAwaiter().GetResult();
}
21 changes: 2 additions & 19 deletions build/docs/Tasks/PreviewDocs.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
using Cake.Wyam;
using Common.Utilities;
using Docs.Utilities;

namespace Docs.Tasks;

[TaskName(nameof(PreviewDocs))]
[TaskDescription("Run a local server with docs in preview")]
[IsDependentOn(typeof(Clean))]
[IsDependentOn(typeof(ValidateMermaidDiagrams))]
[IsDependentOn(typeof(BuildDocs))]
public sealed class PreviewDocs : FrostingTask<BuildContext>
{
public override bool ShouldRun(BuildContext context)
Expand All @@ -18,20 +16,5 @@ public override bool ShouldRun(BuildContext context)
return shouldRun;
}

public override void Run(BuildContext context)
{
if (context.WyamSettings is not null)
{
var schemaTargetDir = Paths.ArtifactsDocs.Combine("preview").Combine("schemas");
context.EnsureDirectoryExists(schemaTargetDir);
context.CopyDirectory(Paths.Schemas, schemaTargetDir);
context.StageMermaidRuntimeForWyam();

context.WyamSettings.Preview = true;
context.WyamSettings.Watch = true;
context.WyamSettings.NoClean = true;
context.WyamSettings.Settings.Add("Host", "gittools.github.io");
context.Wyam(context.WyamSettings);
}
}
public override void Run(BuildContext context) => VersionedDocs.Preview(context);
}
19 changes: 9 additions & 10 deletions build/docs/Tasks/PublishDocs.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Cake.Git;
using Cake.Npx;
using Cake.Wyam;
using Common.Utilities;
using Docs.Utilities;

Expand All @@ -22,8 +20,7 @@ public override bool ShouldRun(BuildContext context)

[TaskName(nameof(PublishDocsInternal))]
[TaskDescription("Published the docs changes to docs specific branch")]
[IsDependentOn(typeof(Clean))]
[IsDependentOn(typeof(ValidateMermaidDiagrams))]
[IsDependentOn(typeof(BuildDocs))]
public sealed class PublishDocsInternal : FrostingTask<BuildContext>
{
public override bool ShouldRun(BuildContext context)
Expand Down Expand Up @@ -78,14 +75,16 @@ private static void PublishDocumentation(BuildContext context)
BranchName = publishBranchName
});

if (context.WyamSettings is not null)
foreach (var directory in System.IO.Directory.GetDirectories(publishFolder.FullPath).Where(directory => System.IO.Path.GetFileName(directory) != ".git"))
{
context.WyamSettings.OutputPath = publishFolder;
context.WyamSettings.NoClean = true;
context.StageMermaidRuntimeForWyam();
context.Wyam(context.WyamSettings);
context.Npx("prettier", arguments: "--write **/*.html", configureSettings: settings => settings.WorkingDirectory = publishFolder);
System.IO.Directory.Delete(directory, true);
}
foreach (var file in System.IO.Directory.GetFiles(publishFolder.FullPath).Where(file => System.IO.Path.GetFileName(file) is not ("CNAME" or ".nojekyll")))
{
System.IO.File.Delete(file);
}

DocsInputs.CopyTree(context.MakeAbsolute(Paths.ArtifactsDocs.Combine("preview")).FullPath, publishFolder.FullPath);

var schemaTargetDir = publishFolder.Combine("schemas");
context.EnsureDirectoryExists(schemaTargetDir);
Expand Down
103 changes: 103 additions & 0 deletions build/docs/Utilities/DocsApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Path = System.IO.Path;

namespace Docs.Utilities;

public static class DocsApi
{
// Published binaries define the API. Tagged source supplies comments missing from the packages.
public static void Prepare(string source, string assemblies)
{
var references = Directory.GetFiles(assemblies, "*.dll")
.Select(p => MetadataReference.CreateFromFile(p)).ToArray();
var inventory = new List<string>();
var routes = new List<string>();
foreach (var assembly in references.Where(r => Path.GetFileName(r.FilePath!).StartsWith("GitVersion", StringComparison.OrdinalIgnoreCase)))
{
var name = Path.GetFileNameWithoutExtension(assembly.FilePath!);
var metadata = CSharpCompilation.Create("Inventory", references: references);
var symbol = metadata.GetAssemblyOrModuleSymbol(assembly) as IAssemblySymbol
?? throw new InvalidOperationException($"Could not inspect {name}.");
var exported = Types(symbol.GlobalNamespace).Where(Public).ToArray();
inventory.AddRange(exported.Select(t => t.GetDocumentationCommentId()!).Where(id => id is not null));
routes.AddRange(exported.Select(t => t.ContainingNamespace.ToDisplayString() + "/" + t.MetadataName.Replace('`', '_') + "/index.html"));
SupplementComments(source, assemblies, references, assembly, name);
}
File.WriteAllLines(Path.Combine(assemblies, "public-types.txt"), inventory.Order(StringComparer.Ordinal));
File.WriteAllLines(Path.Combine(assemblies, "public-type-routes.txt"), routes.Order(StringComparer.Ordinal));
}

private static void SupplementComments(string source, string assemblies, PortableExecutableReference[] references, PortableExecutableReference assembly, string name)
{
var project = Path.Combine(source, "src", name == "gitversion" ? "GitVersion.App" : name);
if (!Directory.Exists(project))
{
return;
}

var trees = Directory.GetFiles(project, "*.cs", SearchOption.AllDirectories)
.Where(p => !Path.GetRelativePath(project, p).Split(Path.DirectorySeparatorChar).Any(s => s is "bin" or "obj" or "Templates"))
.Select(p => CSharpSyntaxTree.ParseText(File.ReadAllText(p), new CSharpParseOptions(documentationMode: DocumentationMode.Diagnose), p)).ToArray();
var compilation = CSharpCompilation.Create(name, trees, references.Where(r => r != assembly),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var path = Path.Combine(assemblies, name + ".xml");
var document = File.Exists(path) ? XDocument.Load(path) : new XDocument(new XElement("doc", new XElement("assembly", new XElement("name", name)), new XElement("members")));
var members = document.Root!.Element("members")!;
var existing = members.Elements("member").Select(m => (string?)m.Attribute("name")).ToHashSet();
foreach (var tree in trees)
{
AddTreeComments(compilation.GetSemanticModel(tree), tree, members, existing);
}

document.Save(path);
}

private static void AddTreeComments(SemanticModel model, SyntaxTree tree, XElement members, HashSet<string?> existing)
{
foreach (var node in tree.GetRoot().DescendantNodes().Where(n => n is MemberDeclarationSyntax or VariableDeclaratorSyntax))
{
var declared = model.GetDeclaredSymbol(node);
var id = declared?.GetDocumentationCommentId();
if (id is null || existing.Contains(id))
{
continue;
}

var xml = declared!.GetDocumentationCommentXml();
if (string.IsNullOrWhiteSpace(xml))
{
continue;
}

try { members.Add(XElement.Parse(xml)); existing.Add(id); }
catch (System.Xml.XmlException e) { throw new InvalidOperationException($"Invalid API comment for {id} in {tree.FilePath}", e); }
}

}

private static IEnumerable<INamedTypeSymbol> Types(INamespaceOrTypeSymbol parent)
{
foreach (var member in parent.GetMembers())
{
if (member is INamedTypeSymbol type)
{
yield return type;
}

if (member is INamespaceOrTypeSymbol container)
{
foreach (var child in Types(container))
{
yield return child;
}
}
}
}

private static bool Public(INamedTypeSymbol type) => type.DeclaredAccessibility == Accessibility.Public
&& !type.Name.Contains('<') && type.ContainingNamespace.ToDisplayString().StartsWith("GitVersion", StringComparison.Ordinal)
&& (type.ContainingType is null || Public(type.ContainingType));
}
Loading
Loading