diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e3dfebdb..0193648a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,6 @@ permissions: contents: read packages: write -env: - BASE_IMAGE: mcr.microsoft.com/dotnet/nightly/runtime-deps:8.0-jammy-chiseled-aot - BASE_IMAGE_SINGLE: mcr.microsoft.com/dotnet/nightly/runtime-deps:8.0-jammy-chiseled - DOCKER_TAG: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') && 'latest;edge' || 'edge' }} - jobs: deploy: runs-on: ubuntu-latest @@ -70,24 +65,6 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Publish docs-builder - run: | - dotnet publish "src/docs-builder/docs-builder.csproj" \ - /t:PublishContainer \ - -p DebugType=none \ - -p ContainerUser=1001:1001 \ - -p ContainerBaseImage=${{ env.BASE_IMAGE }} \ - -p ContainerRegistry=ghcr.io \ - -p ContainerImageTags='"${{ env.DOCKER_TAG }};${{ steps.bootstrap.outputs.full-version }}"' \ - -p ContainerRepository=${{ github.repository }} - - name: Publish docs-generator - run: | - dotnet publish "src/docs-generator/docs-generator.csproj" \ - /t:PublishContainer \ - -p DebugType=none \ - -p ContainerUser=1001:1001 \ - -p ContainerBaseImage=${{ env.BASE_IMAGE_SINGLE }} \ - -p ContainerRegistry=ghcr.io \ - -p ContainerImageTags='"${{ env.DOCKER_TAG }};${{ steps.bootstrap.outputs.full-version }}"' \ - -p ContainerRepository=elastic/docs-generator \ + - name: Publish Containers + run: ./build.sh publishcontainers diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d34c4d9e5..7b38e5279 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -30,10 +30,13 @@ jobs: - name: Bootstrap Action Workspace id: bootstrap uses: ./.github/actions/bootstrap - - - name: build + + - name: Build run: ./build.sh - - - name: publish AOT - run: ./build.sh publish + + - name: Test + run: ./build.sh test + + - name: Publish AOT + run: ./build.sh publishbinaries diff --git a/build/BuildInformation.fs b/build/BuildInformation.fs new file mode 100644 index 000000000..6e61ff1b2 --- /dev/null +++ b/build/BuildInformation.fs @@ -0,0 +1,70 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +module BuildInformation + +open System +open System.IO +open System.Threading +open System.Xml.Linq +open System.Xml.XPath +open Fake.Core +open Proc.Fs +open Fake.Tools.Git + +type Paths = + static member Root = + let mutable dir = DirectoryInfo(".") + let mutable rooted = false + while not(rooted) && dir.GetFiles("*.sln").Length = 0 do + match dir.Parent with + | NonNull parent -> dir <- parent + | _ -> rooted <- true + Environment.CurrentDirectory <- dir.FullName + dir + + static member RelativePathToRoot path = Path.GetRelativePath(Paths.Root.FullName, path) + + static member ArtifactFolder = DirectoryInfo(Path.Combine(Paths.Root.FullName, ".artifacts")) + static member ArtifactPath t = DirectoryInfo(Path.Combine(Paths.ArtifactFolder.FullName, t)) + + static member private SrcFolder = DirectoryInfo(Path.Combine(Paths.Root.FullName, "src")) + static member SrcPath (t: string list) = DirectoryInfo(Path.Combine([Paths.SrcFolder.FullName] @ t |> List.toArray)) + + +type BuildConfiguration = + static member ValidateAssemblyName = false + static member GenerateApiChanges = false + +type Software = + static member Organization = "elastic" + static member Repository = "docs-builder" + static member GithubMoniker = $"%s{Software.Organization}/%s{Software.Repository}" + static member SignKey = "069ca2728db333c1" + + static let restore = + Lazy((fun _ -> exec { run "dotnet" "tool" "restore" }), LazyThreadSafetyMode.ExecutionAndPublication) + + static let versionInfo = + Lazy(fun _ -> + let sha = Information.getCurrentSHA1 "." + let output = exec { + binary "dotnet" + arguments "minver" "-p" "canary.0" "-m" "0.1" + find (fun l -> not(l.Error)) + } + SemVer.parse <| $"%s{output.Line}+%s{sha}" + , LazyThreadSafetyMode.ExecutionAndPublication) + + static member Version = restore.Value; versionInfo.Value + +type OS = + | OSX | Windows | Linux +with + static member Current = + match int Environment.OSVersion.Platform with + | 4 | 128 -> Linux + | 6 -> OSX + | _ -> Windows + diff --git a/build/CommandLine.fs b/build/CommandLine.fs new file mode 100644 index 000000000..7c74c1e44 --- /dev/null +++ b/build/CommandLine.fs @@ -0,0 +1,85 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +module CommandLine + +open Argu +open Microsoft.FSharp.Reflection +open System +open Bullseye + +type Build = + | [] Clean + | [] Version + | [] Compile + | [] Build + | [] Test + + | [] Format + + | [] Lint + | [] PristineCheck + | [] ValidateLicenses + + | [] Publish + | [] PublishBinaries + | [] PublishContainers + + | [] ReleaseNotes + | [] Release + + | [] Single_Target + | [] Token of string + | [] Skip_Dirty_Check +with + interface IArgParserTemplate with + member this.Usage = + match this with + // commands + | Clean -> "clean known output locations" + | Version -> "print version information" + | Build -> "Run build" + + | Test -> "runs a clean build and then runs all the tests " + | Release -> "runs build, tests, and create and validates the packages shy of publishing them" + | Publish -> "Publishes artifacts" + | Format -> "runs dotnet format" + + // steps + | Lint + | PristineCheck + | PublishBinaries + | PublishContainers + | ValidateLicenses + | ReleaseNotes + | Compile + + // flags + | Single_Target -> "Runs the provided sub command without running their dependencies" + | Token _ -> "Token to be used to authenticate with github" + | Skip_Dirty_Check -> "Skip the clean checkout check that guards the release/publish targets" + + member this.StepName = + match FSharpValue.GetUnionFields(this, typeof) with + | case, _ -> case.Name.ToLowerInvariant() + + static member Targets = + let cases = FSharpType.GetUnionCases(typeof) + seq { + for c in cases do + if c.GetFields().Length = 0 then + FSharpValue.MakeUnion(c, [| |]) :?> Build + } + + static member Ignore (_: Build) _ = () + + static member Step action (target: Build) parsed = + Targets.Target(target.StepName, Action(fun _ -> action(parsed))) + + static member Cmd (dependsOn: Build list) (composedOf: Build list) action (target: Build) (parsed: ParseResults) = + let singleTarget = parsed.TryGetResult Single_Target |> Option.isSome + let dependsOn = if singleTarget then [] else dependsOn + + let steps = dependsOn @ composedOf |> List.map _.StepName + Targets.Target(target.StepName, steps, Action(fun _ -> action parsed)) \ No newline at end of file diff --git a/build/Program.cs b/build/Program.cs index 43a95a98c..aaaf532e1 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -18,19 +18,6 @@ await "dotnet test --configuration Release --logger GitHubActions -- RunConfiguration.CollectSourceInformation=true"; }); -app.Add("publish", async (Cancel _) => -{ - var source = "src/docs-builder/docs-builder.csproj"; - await $""" - dotnet publish {source} - """; - - var generatorSource = "src/docs-generator/docs-generator.csproj"; - await $""" - dotnet publish {generatorSource} - """; -}); - // this is manual for now and quite hacky. // this ensures we download the actual LICENSE files in the repositories. // NOT the SPDX html from licenses.nuget.org diff --git a/build/Program.fs b/build/Program.fs new file mode 100644 index 000000000..744e51af7 --- /dev/null +++ b/build/Program.fs @@ -0,0 +1,40 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +module Program + +open Argu +open Bullseye +open ProcNet +open CommandLine + +[] +let main argv = + let argv = if argv.Length = 0 then ["build"] |> Array.ofList else argv + let parser = ArgumentParser.Create(programName = "./build.sh") + let parsed = + try + let parsed = parser.ParseCommandLine(inputs = argv, raiseOnUsage = true) + Some parsed + with e -> + printfn $"%s{e.Message}" + None + + match parsed with + | None -> 2 + | Some parsed -> + + let target = parsed.GetSubCommand().StepName + Targets.Setup parsed + + let swallowTypes = [typeof; typeof] + let shortErrorsFor = (fun e -> swallowTypes |> List.contains (e.GetType()) ) + + async { + do! Async.SwitchToThreadPool () + return! Targets.RunTargetsAndExitAsync([target], shortErrorsFor, fun _ -> ":") |> Async.AwaitTask + } |> Async.RunSynchronously + + 0 + diff --git a/build/Targets.fs b/build/Targets.fs new file mode 100644 index 000000000..2908e9001 --- /dev/null +++ b/build/Targets.fs @@ -0,0 +1,163 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +module Targets + +open Argu +open System.IO +open CommandLine +open Fake.Core +open Fake.IO +open Fake.Tools.Git +open Proc.Fs +open BuildInformation + +let private clean _ = + exec { run "dotnet" "clean" "-c" "release" } + let removeArtifacts folder = Shell.cleanDir (Paths.ArtifactPath folder).FullName + removeArtifacts "package" + removeArtifacts "release-notes" + removeArtifacts "tests" + removeArtifacts "docs-builder" + removeArtifacts "docs-generator" + +let private compile _ = exec { run "dotnet" "build" "-c" "release" } + +let private build _ = printfn "build" +let private release _ = printfn "release" +let private publish _ = printfn "publish" + +let private version _ = + let version = Software.Version + printfn $"Informational version: %s{version.AsString}" + printfn $"Semantic version: %s{version.NormalizeToShorter()}" + +let private format _ = exec { run "dotnet" "format" "--verbosity" "quiet" } + +let private lint _ = + match exec { exit_code_of "dotnet" "format" "--verify-no-changes" } with + | 0 -> printfn "There are no dotnet formatting violations, continuing the build." + | _ -> failwithf "There are dotnet formatting violations. Call `dotnet format` to fix or specify -c to ./build.sh to skip this check" + +let private pristineCheck (arguments:ParseResults) = + let skipCheck = arguments.TryGetResult Skip_Dirty_Check |> Option.isSome + match skipCheck, Information.isCleanWorkingCopy "." with + | true, _ -> printfn "Skip checking for clean working copy since -c is specified" + | _, true -> printfn "The checkout folder does not have pending changes, proceeding" + | _ -> failwithf "The checkout folder has pending changes, aborting. Specify -c to ./build.sh to skip this check" + + match skipCheck, (exec { exit_code_of "dotnet" "format" "--verify-no-changes" }) with + | true, _ -> printfn "Skip formatting checks since -c is specified" + | _, 0 -> printfn "There are no dotnet formatting violations, continuing the build." + | _ -> failwithf "There are dotnet formatting violations. Call `dotnet format` to fix or specify -c to ./build.sh to skip this check" + +let private publishBinaries _ = + exec { run "dotnet" "publish" "src/docs-builder/docs-builder.csproj" } + exec { run "dotnet" "publish" "src/docs-generator/docs-generator.csproj" } + +let private publishContainers _ = + + let createImage project = + let imageTag = match project with | "docs-builder" -> "jammy-chiseled-aot" | _ -> "jammy-chiseled" + let labels = + let exitCode = exec { exit_code_of "git" "describe" "--tags" "--exact-match" "HEAD" } + match exitCode with | 0 -> "edge;latest" | _ -> "edge" + let args = + ["publish"; $"src/%s{project}/%s{project}.csproj"] + @ [ + "/t:PublishContainer"; + "-p"; "DebugType=none"; + "-p"; $"ContainerBaseImage=mcr.microsoft.com/dotnet/nightly/runtime-deps:8.0-%s{imageTag}"; + "-p"; $"ContainerImageTags='\"%s{labels};%s{Software.Version.ToString()}\"'" + "-p"; $"ContainerRepository=%s{Software.GithubMoniker}" + ] + let registry = + match Environment.environVarOrNone "GITHUB_ACTION" with + | None -> [] + | Some _ -> [ + "-p"; "ContainerRegistry=ghcr.io" + "-p"; "ContainerUser=1001:1001"; + ] + exec { run "dotnet" (args @ registry) } + createImage "docs-builder" + createImage "docs-generator" + +let private runTests _ = + exec { + run "dotnet" ( + ["test"; "-c"; "release"; "--no-restore"; "--no-build"; "--logger"; "GitHubActions"] + @ ["--"; "RunConfiguration.CollectSourceInformation=true"] + ) + } + +let private validateLicenses _ = + let args = ["-u"; "-t"; "-i"; "docs-builder.sln"; "--use-project-assets-json" + "--forbidden-license-types"; "build/forbidden-license-types.json" + "--packages-filter"; "#System\..*#";] + exec { run "dotnet" (["dotnet-project-licenses"] @ args) } + +let private generateReleaseNotes (arguments:ParseResults) = + let currentVersion = Software.Version.NormalizeToShorter() + let releaseNotesPath = Paths.ArtifactPath "release-notes" + let output = + Paths.RelativePathToRoot <| Path.Combine(releaseNotesPath.FullName, $"release-notes-%s{currentVersion}.md") + let tokenArgs = + match arguments.TryGetResult Token with + | None -> [] + | Some token -> ["--token"; token;] + let releaseNotesArgs = + (Software.GithubMoniker.Split("/") |> Seq.toList) + @ ["--version"; currentVersion + "--label"; "enhancement"; "Features" + "--label"; "bug"; "Fixes" + "--label"; "documentation"; "Documentation" + ] @ tokenArgs + @ ["--output"; output] + + let args = ["release-notes"] @ releaseNotesArgs + exec { run "dotnet" args } + +let Setup (parsed:ParseResults) = + let wireCommandLine (t: Build) = + match t with + // commands + | Version -> Build.Step version + | Clean -> Build.Cmd [Version] [] clean + | Compile -> Build.Step compile + | Build -> + Build.Cmd + [Clean; Lint; Compile] [] build + + | Test -> Build.Cmd [Compile] [] runTests + + | Release -> + Build.Cmd + [PristineCheck; Build] + [ValidateLicenses; ReleaseNotes] + release + + | Publish -> + Build.Cmd + [] + [PublishBinaries; PublishContainers] + release + + | Format -> Build.Step format + + // steps + | Lint -> Build.Step lint + | PristineCheck -> Build.Step pristineCheck + | PublishBinaries -> Build.Step publishBinaries + | PublishContainers -> Build.Step publishContainers + | ValidateLicenses -> Build.Step validateLicenses + | ReleaseNotes -> Build.Step generateReleaseNotes + + // flags + | Single_Target + | Token _ + | Skip_Dirty_Check -> Build.Ignore + + for target in Build.Targets do + let setup = wireCommandLine target + setup target parsed \ No newline at end of file diff --git a/build/build.csproj b/build/build.csproj deleted file mode 100644 index ebb699896..000000000 --- a/build/build.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - net9.0 - Exe - $(NoWarn);NU1701 - false - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - diff --git a/build/build.fsproj b/build/build.fsproj new file mode 100644 index 000000000..fe8bb1fcf --- /dev/null +++ b/build/build.fsproj @@ -0,0 +1,34 @@ + + + net9.0 + Exe + $(NoWarn);NU1701 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs-builder.sln b/docs-builder.sln index bbe6e107e..f5845e4cc 100644 --- a/docs-builder.sln +++ b/docs-builder.sln @@ -21,8 +21,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BE6011CC-120 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs-builder", "src\docs-builder\docs-builder.csproj", "{01F05AD0-E0E0-401F-A7EC-905928E1E9F0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "build", "build\build.csproj", "{CCD5FBD6-B51F-431F-9B16-385D53464953}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = ".github", ".github\.github.csproj", "{1A8659C1-222A-4824-B562-ED8F88658C05}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{67B576EE-02FA-4F9B-94BC-3630BC09ECE5}" @@ -48,6 +46,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "publish-vercel", "publish-v actions\publish-vercel\action.yml = actions\publish-vercel\action.yml EndProjectSection EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "build", "build\build.fsproj", "{10857974-6CF1-42B5-B793-AAA988BD7348}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -66,10 +66,6 @@ Global {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Release|Any CPU.ActiveCfg = Release|Any CPU {01F05AD0-E0E0-401F-A7EC-905928E1E9F0}.Release|Any CPU.Build.0 = Release|Any CPU - {CCD5FBD6-B51F-431F-9B16-385D53464953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CCD5FBD6-B51F-431F-9B16-385D53464953}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CCD5FBD6-B51F-431F-9B16-385D53464953}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CCD5FBD6-B51F-431F-9B16-385D53464953}.Release|Any CPU.Build.0 = Release|Any CPU {1A8659C1-222A-4824-B562-ED8F88658C05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A8659C1-222A-4824-B562-ED8F88658C05}.Debug|Any CPU.Build.0 = Debug|Any CPU {1A8659C1-222A-4824-B562-ED8F88658C05}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -82,6 +78,10 @@ Global {61904527-9753-4379-B546-56B6A29073AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {61904527-9753-4379-B546-56B6A29073AC}.Release|Any CPU.ActiveCfg = Release|Any CPU {61904527-9753-4379-B546-56B6A29073AC}.Release|Any CPU.Build.0 = Release|Any CPU + {10857974-6CF1-42B5-B793-AAA988BD7348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10857974-6CF1-42B5-B793-AAA988BD7348}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {4D198E25-C211-41DC-9E84-B15E89BD7048} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A} diff --git a/dotnet-tools.json b/dotnet-tools.json index cee44a3ea..a73eaf3c3 100644 --- a/dotnet-tools.json +++ b/dotnet-tools.json @@ -22,6 +22,13 @@ "thirdlicense" ], "rollForward": false + }, + "release-notes": { + "version": "0.6.0", + "commands": [ + "release-notes" + ], + "rollForward": false } } } \ No newline at end of file