From bc9b0ce88f11d6db0acd3dcf937a38197b148d62 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 13:48:46 +0200 Subject: [PATCH] Add configuration files and scripts for project standards enforcement - Introduced .editorconfig for consistent coding styles. - Added .gitattributes to manage line endings and text attributes. - Created README.md for Git hooks usage and configuration. - Implemented pre-commit hook to enforce executable permissions and LF line endings for shell scripts. - Updated .gitignore to include additional build artifacts and directories. - Enhanced AGENTS.md and Definition of Done documentation with testing standards. - Modified reconciliation scripts to include new configuration files and templates. --- .editorconfig | 19 ++++++ .gitattributes | 18 ++++++ .githooks/README.md | 16 +++++ .githooks/pre-commit | 25 ++++++++ .gitignore | 12 ++++ AGENTS.md | 10 ++++ docs/DEFINITION_OF_DONE.md | 2 + scripts/Reconcile-ProjectStandards.ps1 | 82 +++++++++++++++++++++++--- scripts/reconcile-project-standards.sh | 81 +++++++++++++++++++++---- 9 files changed, 244 insertions(+), 21 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .githooks/README.md create mode 100644 .githooks/pre-commit mode change 100644 => 100755 scripts/reconcile-project-standards.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b9dc5ac --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.go] +indent_style = tab +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + +[*.ps1] +end_of_line = crlf diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0b57dc4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,18 @@ +* text=auto + +# Ensure LF for shell scripts and common source/docs files +*.sh text eol=lf +*.bash text eol=lf +*.zsh text eol=lf +*.go text eol=lf +*.mod text eol=lf +*.sum text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +Makefile text eol=lf + +# Keep native Windows script formats +*.ps1 text eol=crlf +*.bat text eol=crlf +*.cmd text eol=crlf diff --git a/.githooks/README.md b/.githooks/README.md new file mode 100644 index 0000000..3d2190a --- /dev/null +++ b/.githooks/README.md @@ -0,0 +1,16 @@ +# Git Hooks + +This repository standard uses a project-local hooks directory: + +- `.githooks/pre-commit` + +Activate it once per repository: + +```sh +git config core.hooksPath .githooks +``` + +The pre-commit hook validates for staged `.sh` files: + +- executable bit in Git index (`100755`) +- LF line endings (no CRLF) diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..a8652d4 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,25 @@ +#!/usr/bin/env sh +set -eu + +failed=0 +cr=$(printf '\r') + +staged_shell_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.sh$' || true) + +for file in $staged_shell_files; do + mode=$(git ls-files --stage -- "$file" | awk '{print $1}') + if [ "$mode" != "100755" ]; then + echo "ERROR: $file is not executable in Git index. Run: git add --chmod=+x $file" >&2 + failed=1 + fi + + if git show ":$file" | grep -q "$cr"; then + echo "ERROR: $file contains CRLF in staged content. Use LF line endings." >&2 + failed=1 + fi +done + +if [ "$failed" -ne 0 ]; then + echo "Pre-commit check failed." >&2 + exit 1 +fi diff --git a/.gitignore b/.gitignore index 30bcfa4..4b00f8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,13 @@ .build/ +bin/ +dist/ +tmp/ +coverage/ +coverage.out +*.coverprofile +*.test +*.out +.DS_Store +Thumbs.db +.vscode/ +.idea/ diff --git a/AGENTS.md b/AGENTS.md index da4e36e..92be300 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,11 +20,17 @@ All project documentation files must be stored under `docs/`, except `README.md` 2. Keep tests deterministic and fast. 3. Prefer table-driven tests where they improve readability. 4. Run relevant tests locally before finishing changes. +5. For Go projects, use `github.com/smartystreets/goconvey` as the standard test library. ## Build Artifacts and Reports 1. Builder logs and generated reports must be created under `.build/`. 2. The `.build/` directory must be excluded from version control via `.gitignore`. +## Git and Script Standards +1. Shell scripts (`*.sh`) must use LF line endings. +2. Shell scripts committed to the repository must be executable in Git index (mode `100755`). +3. When adding a new shell script, set execute permissions before commit: `git add --chmod=+x path/to/script.sh`. + ## Definition of Done (DoD) ### Purpose @@ -35,6 +41,8 @@ The Definition of Done defines the minimum quality bar for every completed chang - Every code change is covered by tests where applicable. - New functionality includes new tests. - Bug fixes include at least one regression test. +- For Go projects, tests use `github.com/smartystreets/goconvey`. +- Automated test coverage is at least 80%. 1. Functional documentation - Implemented functionality is documented. @@ -61,6 +69,8 @@ The Definition of Done defines the minimum quality bar for every completed chang ### Review Checklist (Quick) - [ ] Change is implemented and meets acceptance criteria. - [ ] Tests were added/updated and pass. +- [ ] Go tests use `github.com/smartystreets/goconvey`. +- [ ] Automated test coverage is at least 80%. - [ ] Functionality is documented. - [ ] Documentation is in English. - [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`). diff --git a/docs/DEFINITION_OF_DONE.md b/docs/DEFINITION_OF_DONE.md index e425c44..884a425 100644 --- a/docs/DEFINITION_OF_DONE.md +++ b/docs/DEFINITION_OF_DONE.md @@ -10,6 +10,7 @@ This Definition of Done defines the minimum quality bar for every completed chan - Every code change is covered by tests where applicable. - New functionality includes new tests. - Bug fixes include at least one regression test. +- For Go projects, tests use `github.com/smartystreets/goconvey`. - Automated test coverage is at least 80%. 1. Functional documentation @@ -39,6 +40,7 @@ This Definition of Done defines the minimum quality bar for every completed chan - [ ] Change is implemented and meets acceptance criteria. - [ ] Tests were added/updated and pass. +- [ ] Go tests use `github.com/smartystreets/goconvey`. - [ ] Automated test coverage is at least 80%. - [ ] Functionality is documented. - [ ] Documentation is in English. diff --git a/scripts/Reconcile-ProjectStandards.ps1 b/scripts/Reconcile-ProjectStandards.ps1 index afce0ec..ad39809 100644 --- a/scripts/Reconcile-ProjectStandards.ps1 +++ b/scripts/Reconcile-ProjectStandards.ps1 @@ -13,6 +13,11 @@ $ErrorActionPreference = 'Stop' $agentsTemplate = Join-Path $StandardsRepoPath 'templates/AGENTS.base.md' $dodTemplate = Join-Path $StandardsRepoPath 'templates/DEFINITION_OF_DONE.base.md' +$gitIgnoreTemplate = Join-Path $StandardsRepoPath 'templates/.gitignore.base' +$gitAttributesTemplate = Join-Path $StandardsRepoPath 'templates/.gitattributes.base' +$editorConfigTemplate = Join-Path $StandardsRepoPath 'templates/.editorconfig.base' +$preCommitHookTemplate = Join-Path $StandardsRepoPath 'templates/pre-commit.base.sh' +$hooksReadmeTemplate = Join-Path $StandardsRepoPath 'templates/.githooks.README.base.md' if (-not (Test-Path -Path $agentsTemplate -PathType Leaf)) { throw "AGENTS template not found: $agentsTemplate" @@ -22,6 +27,26 @@ if (-not (Test-Path -Path $dodTemplate -PathType Leaf)) { throw "DoD template not found: $dodTemplate" } +if (-not (Test-Path -Path $gitIgnoreTemplate -PathType Leaf)) { + throw "gitignore template not found: $gitIgnoreTemplate" +} + +if (-not (Test-Path -Path $gitAttributesTemplate -PathType Leaf)) { + throw "gitattributes template not found: $gitAttributesTemplate" +} + +if (-not (Test-Path -Path $editorConfigTemplate -PathType Leaf)) { + throw "editorconfig template not found: $editorConfigTemplate" +} + +if (-not (Test-Path -Path $preCommitHookTemplate -PathType Leaf)) { + throw "pre-commit hook template not found: $preCommitHookTemplate" +} + +if (-not (Test-Path -Path $hooksReadmeTemplate -PathType Leaf)) { + throw "hooks readme template not found: $hooksReadmeTemplate" +} + if ($IntervalSeconds -lt 5) { throw 'IntervalSeconds must be >= 5.' } @@ -63,23 +88,41 @@ function Ensure-FileFromTemplate { return 'updated' } -function Ensure-GitIgnoreBuildEntry { +function Ensure-GitIgnoreEntriesFromTemplate { param( + [string]$TemplatePath, [string]$GitIgnorePath, [switch]$OnlyCheck ) + $requiredEntries = Get-Content -Path $TemplatePath | Where-Object { + $_.Trim() -and -not $_.Trim().StartsWith('#') + } + if (-not (Test-Path -Path $GitIgnorePath -PathType Leaf)) { if ($OnlyCheck) { return 'drift' } - Set-Content -Path $GitIgnorePath -Value '.build/' - return 'updated' + New-Item -ItemType File -Path $GitIgnorePath | Out-Null } $lines = Get-Content -Path $GitIgnorePath - if ($lines -contains '.build/') { + + $missingEntry = $false + foreach ($entry in $requiredEntries) { + if ($lines -contains $entry) { + continue + } + + $missingEntry = $true + if (-not $OnlyCheck) { + Add-Content -Path $GitIgnorePath -Value $entry + $lines += $entry + } + } + + if (-not $missingEntry) { return 'ok' } @@ -87,7 +130,6 @@ function Ensure-GitIgnoreBuildEntry { return 'drift' } - Add-Content -Path $GitIgnorePath -Value '.build/' return 'updated' } @@ -110,20 +152,44 @@ function Invoke-ReconcileOnce { $agentsTarget = Join-Path $repoPath 'AGENTS.md' $dodTarget = Join-Path (Join-Path $repoPath 'docs') 'DEFINITION_OF_DONE.md' $gitIgnoreTarget = Join-Path $repoPath '.gitignore' + $gitAttributesTarget = Join-Path $repoPath '.gitattributes' + $editorConfigTarget = Join-Path $repoPath '.editorconfig' + $preCommitHookTarget = Join-Path (Join-Path $repoPath '.githooks') 'pre-commit' + $hooksReadmeTarget = Join-Path (Join-Path $repoPath '.githooks') 'README.md' $summary.scanned++ $agentsState = Ensure-FileFromTemplate -Template $agentsTemplate -Target $agentsTarget -OnlyCheck:$OnlyCheck $dodState = Ensure-FileFromTemplate -Template $dodTemplate -Target $dodTarget -OnlyCheck:$OnlyCheck - $gitIgnoreState = Ensure-GitIgnoreBuildEntry -GitIgnorePath $gitIgnoreTarget -OnlyCheck:$OnlyCheck + $gitAttributesState = Ensure-FileFromTemplate -Template $gitAttributesTemplate -Target $gitAttributesTarget -OnlyCheck:$OnlyCheck + $editorConfigState = Ensure-FileFromTemplate -Template $editorConfigTemplate -Target $editorConfigTarget -OnlyCheck:$OnlyCheck + $preCommitHookState = Ensure-FileFromTemplate -Template $preCommitHookTemplate -Target $preCommitHookTarget -OnlyCheck:$OnlyCheck + $hooksReadmeState = Ensure-FileFromTemplate -Template $hooksReadmeTemplate -Target $hooksReadmeTarget -OnlyCheck:$OnlyCheck + $gitIgnoreState = Ensure-GitIgnoreEntriesFromTemplate -TemplatePath $gitIgnoreTemplate -GitIgnorePath $gitIgnoreTarget -OnlyCheck:$OnlyCheck - if ($agentsState -eq 'updated' -or $dodState -eq 'updated' -or $gitIgnoreState -eq 'updated') { + if ( + $agentsState -eq 'updated' -or + $dodState -eq 'updated' -or + $gitAttributesState -eq 'updated' -or + $editorConfigState -eq 'updated' -or + $preCommitHookState -eq 'updated' -or + $hooksReadmeState -eq 'updated' -or + $gitIgnoreState -eq 'updated' + ) { $summary.updated++ Write-Host "UPDATED: $repoPath" continue } - if ($agentsState -eq 'drift' -or $dodState -eq 'drift' -or $gitIgnoreState -eq 'drift') { + if ( + $agentsState -eq 'drift' -or + $dodState -eq 'drift' -or + $gitAttributesState -eq 'drift' -or + $editorConfigState -eq 'drift' -or + $preCommitHookState -eq 'drift' -or + $hooksReadmeState -eq 'drift' -or + $gitIgnoreState -eq 'drift' + ) { $summary.drift++ Write-Host "DRIFT: $repoPath" continue diff --git a/scripts/reconcile-project-standards.sh b/scripts/reconcile-project-standards.sh old mode 100644 new mode 100755 index 3c93b13..68fa9a9 --- a/scripts/reconcile-project-standards.sh +++ b/scripts/reconcile-project-standards.sh @@ -69,6 +69,11 @@ fi AGENTS_TEMPLATE="$STANDARDS_REPO/templates/AGENTS.base.md" DOD_TEMPLATE="$STANDARDS_REPO/templates/DEFINITION_OF_DONE.base.md" +GITIGNORE_TEMPLATE="$STANDARDS_REPO/templates/.gitignore.base" +GITATTRIBUTES_TEMPLATE="$STANDARDS_REPO/templates/.gitattributes.base" +EDITORCONFIG_TEMPLATE="$STANDARDS_REPO/templates/.editorconfig.base" +PRECOMMIT_TEMPLATE="$STANDARDS_REPO/templates/pre-commit.base.sh" +HOOKS_README_TEMPLATE="$STANDARDS_REPO/templates/.githooks.README.base.md" if [ ! -f "$AGENTS_TEMPLATE" ]; then echo "AGENTS template not found: $AGENTS_TEMPLATE" >&2 @@ -80,6 +85,31 @@ if [ ! -f "$DOD_TEMPLATE" ]; then exit 1 fi +if [ ! -f "$GITIGNORE_TEMPLATE" ]; then + echo "gitignore template not found: $GITIGNORE_TEMPLATE" >&2 + exit 1 +fi + +if [ ! -f "$GITATTRIBUTES_TEMPLATE" ]; then + echo "gitattributes template not found: $GITATTRIBUTES_TEMPLATE" >&2 + exit 1 +fi + +if [ ! -f "$EDITORCONFIG_TEMPLATE" ]; then + echo "editorconfig template not found: $EDITORCONFIG_TEMPLATE" >&2 + exit 1 +fi + +if [ ! -f "$PRECOMMIT_TEMPLATE" ]; then + echo "pre-commit hook template not found: $PRECOMMIT_TEMPLATE" >&2 + exit 1 +fi + +if [ ! -f "$HOOKS_README_TEMPLATE" ]; then + echo "hooks readme template not found: $HOOKS_README_TEMPLATE" >&2 + exit 1 +fi + hash_or_missing() { path=$1 if [ ! -f "$path" ]; then @@ -116,8 +146,11 @@ ensure_file() { printf "%s" "updated" } -ensure_gitignore_build_entry() { - gitignore_path=$1 +ensure_gitignore_entries_from_template() { + template_path=$1 + gitignore_path=$2 + + missing=0 if [ ! -f "$gitignore_path" ]; then if [ "$CHECK_ONLY" -eq 1 ]; then @@ -125,23 +158,37 @@ ensure_gitignore_build_entry() { return 0 fi - printf '%s\n' '.build/' > "$gitignore_path" - printf "%s" "updated" - return 0 + : > "$gitignore_path" fi - if grep -Fqx '.build/' "$gitignore_path"; then + while IFS= read -r entry; do + [ -n "$entry" ] || continue + case "$entry" in + \#*) + continue + ;; + esac + + if grep -Fqx "$entry" "$gitignore_path"; then + continue + fi + + missing=1 + if [ "$CHECK_ONLY" -ne 1 ]; then + printf '%s\n' "$entry" >> "$gitignore_path" + fi + done < "$template_path" + + if [ "$missing" -eq 0 ]; then printf "%s" "ok" return 0 fi if [ "$CHECK_ONLY" -eq 1 ]; then printf "%s" "drift" - return 0 + else + printf "%s" "updated" fi - - printf '\n%s\n' '.build/' >> "$gitignore_path" - printf "%s" "updated" } run_once() { @@ -159,19 +206,27 @@ run_once() { agents_target="$repo/AGENTS.md" dod_target="$repo/docs/DEFINITION_OF_DONE.md" + gitattributes_target="$repo/.gitattributes" + editorconfig_target="$repo/.editorconfig" + precommit_target="$repo/.githooks/pre-commit" + hooks_readme_target="$repo/.githooks/README.md" gitignore_target="$repo/.gitignore" agents_state=$(ensure_file "$AGENTS_TEMPLATE" "$agents_target") dod_state=$(ensure_file "$DOD_TEMPLATE" "$dod_target") - gitignore_state=$(ensure_gitignore_build_entry "$gitignore_target") + gitattributes_state=$(ensure_file "$GITATTRIBUTES_TEMPLATE" "$gitattributes_target") + editorconfig_state=$(ensure_file "$EDITORCONFIG_TEMPLATE" "$editorconfig_target") + precommit_state=$(ensure_file "$PRECOMMIT_TEMPLATE" "$precommit_target") + hooks_readme_state=$(ensure_file "$HOOKS_README_TEMPLATE" "$hooks_readme_target") + gitignore_state=$(ensure_gitignore_entries_from_template "$GITIGNORE_TEMPLATE" "$gitignore_target") - if [ "$agents_state" = "updated" ] || [ "$dod_state" = "updated" ] || [ "$gitignore_state" = "updated" ]; then + if [ "$agents_state" = "updated" ] || [ "$dod_state" = "updated" ] || [ "$gitattributes_state" = "updated" ] || [ "$editorconfig_state" = "updated" ] || [ "$precommit_state" = "updated" ] || [ "$hooks_readme_state" = "updated" ] || [ "$gitignore_state" = "updated" ]; then updated=$((updated + 1)) echo "UPDATED: $repo" continue fi - if [ "$agents_state" = "drift" ] || [ "$dod_state" = "drift" ] || [ "$gitignore_state" = "drift" ]; then + if [ "$agents_state" = "drift" ] || [ "$dod_state" = "drift" ] || [ "$gitattributes_state" = "drift" ] || [ "$editorconfig_state" = "drift" ] || [ "$precommit_state" = "drift" ] || [ "$hooks_readme_state" = "drift" ] || [ "$gitignore_state" = "drift" ]; then drift=$((drift + 1)) echo "DRIFT: $repo" continue