diff --git a/.editorconfig b/.editorconfig index 578ce1883..65d0ce884 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,7 +15,7 @@ max_line_length = 160 indent_style = space indent_size = 4 -[*.{md,markdown,json,js,yml,csproj,fsproj,targets,targets,props}] +[*.{md,markdown,json,js,yml,yaml,csproj,fsproj,targets,targets,props}] indent_style = space indent_size = 2 diff --git a/.github/actions/bootstrap/action.yml b/.github/actions/bootstrap/action.yml index 0643fd9c9..e4ac144f7 100644 --- a/.github/actions/bootstrap/action.yml +++ b/.github/actions/bootstrap/action.yml @@ -16,7 +16,11 @@ runs: # Ensure we fetch all tags - shell: bash run: | - git fetch --prune --unshallow --tags + if [ -f .git/shallow ]; then + git fetch --prune --unshallow --tags + else + git fetch --prune --tags + fi git tag --list - name: Git config diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..6937b3651 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +dotnet husky run --group pre-commit diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 000000000..3af48fa0a --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +dotnet husky run --group pre-push diff --git a/.husky/task-runner.json b/.husky/task-runner.json new file mode 100644 index 000000000..8ed012c84 --- /dev/null +++ b/.husky/task-runner.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://alirezanet.github.io/Husky.Net/schema.json", + "variables": [ + { + "name": "push-files", + "command": "bash", + "args": [ + "-c", + "git diff --name-only --diff-filter=d @{upstream}..HEAD 2>/dev/null || git diff --name-only --diff-filter=d origin/main..HEAD 2>/dev/null || git diff --name-only --diff-filter=d HEAD~1..HEAD" + ] + } + ], + "tasks": [ + { + "name": "dotnet-lint", + "group": "pre-push", + "command": "bash", + "args": ["./build.sh", "lint", "${push-files}"], + "include": ["**/*.cs", "**/*.fs"] + }, + { + "name": "prettier", + "group": "pre-commit", + "pathMode": "absolute", + "cwd": "src/Elastic.Documentation.Site", + "command": "npx", + "args": ["prettier", "--write", "${staged}"], + "include": [ + "src/Elastic.Documentation.Site/**/*.ts", + "src/Elastic.Documentation.Site/**/*.tsx", + "src/Elastic.Documentation.Site/**/*.js", + "src/Elastic.Documentation.Site/**/*.jsx", + "src/Elastic.Documentation.Site/**/*.css", + "src/Elastic.Documentation.Site/**/*.json" + ] + }, + { + "name": "typescript-check", + "group": "pre-commit", + "cwd": "src/Elastic.Documentation.Site", + "command": "npx", + "args": ["tsc", "--noEmit"], + "include": [ + "src/Elastic.Documentation.Site/**/*.ts", + "src/Elastic.Documentation.Site/**/*.tsx" + ], + "filteringRule": "staged" + }, + { + "name": "eslint", + "group": "pre-commit", + "pathMode": "absolute", + "cwd": "src/Elastic.Documentation.Site", + "command": "npx", + "args": ["eslint", "--fix", "${staged}"], + "include": [ + "src/Elastic.Documentation.Site/**/*.ts", + "src/Elastic.Documentation.Site/**/*.tsx", + "src/Elastic.Documentation.Site/**/*.js", + "src/Elastic.Documentation.Site/**/*.jsx", + "src/Elastic.Documentation.Site/**/*.mjs" + ] + } + ] +} diff --git a/CONTRIBUTING.MD b/CONTRIBUTING.md similarity index 58% rename from CONTRIBUTING.MD rename to CONTRIBUTING.md index f07195b31..e97503f1a 100644 --- a/CONTRIBUTING.MD +++ b/CONTRIBUTING.md @@ -56,6 +56,77 @@ based on the labels of the pull requests merged into the `main` branch. See the [release-drafter configuration](./.github/release-drafter.yml) for more details. +## Git Hooks with Husky.Net + +This repository uses [Husky.Net](https://alirezanet.github.io/Husky.Net/) to automatically format and validate code before commits and pushes. + +### What Gets Checked + +**Pre-commit hooks** (run on staged files): +- **prettier** - Formats TypeScript, JavaScript, CSS, and JSON files in `src/Elastic.Documentation.Site/` +- **typescript-check** - Type checks TypeScript files with `tsc --noEmit` (only if TS files are staged) +- **eslint** - Lints and fixes JavaScript/TypeScript files + +**Pre-push hooks** (run on files being pushed): +- **dotnet-lint** - Lints C# and F# files using `./build.sh lint` (runs `dotnet format --verify-no-changes`) + +### Installation + +Husky.Net is included as a dotnet tool. After cloning the repository, restore the tools: + +```bash +dotnet tool restore +``` + +Then install the git hooks: + +```bash +dotnet husky install +``` + +That's it! The hooks will now run automatically on every commit and push. + +### Usage + +Once installed, hooks run automatically when you commit or push: + +```bash +git add . +git commit -m "your message" # Pre-commit hooks run here +git push # Pre-push hooks run here +``` + +**Note:** If hooks modify files (prettier, eslint), the commit will fail so you can review the changes. Simply stage the changes and commit again: + +```bash +git add -u +git commit -m "your message" +``` + +If the **dotnet-lint** hook fails during push, you need to fix the linting errors and commit the fixes before pushing again. + +### Manual Execution + +You can test hooks without committing or pushing: + +```bash +# Run all pre-commit tasks +dotnet husky run --group pre-commit + +# Run all pre-push tasks +dotnet husky run --group pre-push + +# Test individual tasks +dotnet husky run --name prettier +dotnet husky run --name typescript-check +dotnet husky run --name eslint +dotnet husky run --name dotnet-lint +``` + +### Configuration + +Hook configuration is defined in `.husky/task-runner.json`. See the [Husky.Net documentation](https://alirezanet.github.io/Husky.Net/guide/task-runner.html) for more details. + ## Creating a New Release To create a new release trigger the [release](https://github.com/elastic/docs-builder/actions/workflows/release.yml) workflow on the `main` branch. diff --git a/build/CommandLine.fs b/build/CommandLine.fs index 94b13bade..56770ec31 100644 --- a/build/CommandLine.fs +++ b/build/CommandLine.fs @@ -15,6 +15,22 @@ type TestSuite = All | Unit | Integration match FSharpValue.GetUnionFields(this, typeof) with | case, _ -> case.Name.ToLowerInvariant() +type FormatArgs = + | [] Include of string list + with + interface IArgParserTemplate with + member this.Usage = + match this with + | Include _ -> "Specify files to include in format, passed to dotnet format --include" + +type LintArgs = + | [] Include of string list + with + interface IArgParserTemplate with + member this.Usage = + match this with + | Include _ -> "Specify files to include in lint check, passed to dotnet format --include" + type Build = | [] Clean | [] Version @@ -25,10 +41,10 @@ type Build = | [] Unit_Test | [] Integrate - | [] Format + | [] Format of ParseResults | [] Watch - | [] Lint + | [] Lint of ParseResults | [] PristineCheck | [] ValidateLicenses @@ -59,12 +75,12 @@ with | Release -> "runs build, tests, and create and validates the packages shy of publishing them" | Publish -> "Publishes artifacts" - | Format -> "runs dotnet format" + | Format _ -> "runs dotnet format" | Watch -> "runs dotnet watch to continuous build code/templates and web assets on the fly" // steps - | Lint + | Lint _ -> "runs dotnet format --verify-no-changes" | PristineCheck | PublishBinaries | PublishContainers @@ -87,10 +103,20 @@ with let cases = FSharpType.GetUnionCases(typeof) seq { for c in cases do - if c.GetFields().Length = 0 then + match c.GetFields().Length with + | 0 -> match FSharpValue.MakeUnion(c, [| |]) with | NonNull u -> u :?> Build | _ -> failwithf $"%s{c.Name} can not be cast to Build enum" + | _ when c.Name = "Format" -> + // Format has sub-arguments, create empty instance + let emptyFormatArgs = ArgumentParser.Create().Parse([||]) + Format emptyFormatArgs + | _ when c.Name = "Lint" -> + // Lint has sub-arguments, create empty instance + let emptyLintArgs = ArgumentParser.Create().Parse([||]) + Lint emptyLintArgs + | _ -> () } static member Ignore (_: Build) _ = () diff --git a/build/Targets.fs b/build/Targets.fs index f5bef2d3f..c9e074e65 100644 --- a/build/Targets.fs +++ b/build/Targets.fs @@ -31,13 +31,22 @@ let private version _ = printfn $"Informational version: %s{version.AsString}" printfn $"Semantic version: %s{version.Normalize()}" -let private format _ = exec { run "dotnet" "format" "--verbosity" "quiet" } +let private format (formatArgs: ParseResults) = + let includeFiles = formatArgs.TryGetResult FormatArgs.Include |> Option.defaultValue [] + let includeArgs = + if includeFiles.IsEmpty then [] + else ["--include"] @ includeFiles + exec { run "dotnet" (["format"; "--verbosity"; "quiet"] @ includeArgs) } let private watch _ = exec { run "dotnet" "watch" "--project" "src/tooling/docs-builder" "--configuration" "debug" "--" "serve" } -let private lint _ = +let private lint (lintArgs: ParseResults) = + let includeFiles = lintArgs.TryGetResult LintArgs.Include |> Option.defaultValue [] + let includeArgs = + if includeFiles.IsEmpty then [] + else ["--include"] @ includeFiles match exec { - exit_code_of "dotnet" "format" "--verify-no-changes" + exit_code_of "dotnet" (["format"; "--verify-no-changes"] @ includeArgs) } 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" @@ -148,6 +157,8 @@ let private validateLicenses _ = exec { run "dotnet" (["dotnet-project-licenses"] @ args) } let Setup (parsed:ParseResults) = + let emptyLintArgs = ArgumentParser.Create().Parse([||]) + let wireCommandLine (t: Build) = match t with // commands @@ -156,7 +167,7 @@ let Setup (parsed:ParseResults) = | Compile -> Build.Step compile | Build -> Build.Cmd - [Clean; Lint; Compile] [] build + [Clean; Lint emptyLintArgs; Compile] [] build | Test -> Build.Cmd [Compile] [] <| runTests TestSuite.All | Unit_Test -> Build.Cmd [Compile] [] <| runTests TestSuite.Unit @@ -174,11 +185,11 @@ let Setup (parsed:ParseResults) = [PublishBinaries; PublishContainers] release - | Format -> Build.Step format + | Format formatArgs -> Build.Step (fun _ -> format formatArgs) | Watch -> Build.Step watch // steps - | Lint -> Build.Step lint + | Lint lintArgs -> Build.Step (fun _ -> lint lintArgs) | PristineCheck -> Build.Step pristineCheck | PublishBinaries -> Build.Step publishBinaries | PublishContainers -> Build.Step publishContainers diff --git a/dotnet-tools.json b/dotnet-tools.json index a73eaf3c3..24246ef59 100644 --- a/dotnet-tools.json +++ b/dotnet-tools.json @@ -2,6 +2,13 @@ "version": 1, "isRoot": true, "tools": { + "husky": { + "version": "0.7.1", + "commands": [ + "husky" + ], + "rollForward": true + }, "minver-cli": { "version": "5.0.0", "commands": [ @@ -31,4 +38,4 @@ "rollForward": false } } -} \ No newline at end of file +}