From e69fd33931aed767932da17850feb4fe08b6eba8 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 13:07:14 +0200 Subject: [PATCH 01/17] Add AGENTS and Definition of Done documentation; implement project standards reconciliation scripts --- AGENTS.md | 63 +++++++++ docs/DEFINITION_OF_DONE.md | 44 +++++++ scripts/Reconcile-ProjectStandards.ps1 | 121 ++++++++++++++++++ scripts/reconcile-project-standards.sh | 170 +++++++++++++++++++++++++ utils_test.go | 16 ++- 5 files changed, 410 insertions(+), 4 deletions(-) create mode 100644 AGENTS.md create mode 100644 docs/DEFINITION_OF_DONE.md create mode 100644 scripts/Reconcile-ProjectStandards.ps1 create mode 100644 scripts/reconcile-project-standards.sh diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1aaa2e8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,63 @@ +# AGENTS.md + +## Purpose +This file defines the default working agreement for AI coding agents and contributors in this repository. + +## Documentation Language +Project language is English. All documentation, issues, pull requests, commit messages, and code comments should be written in English. +All newly created documentation in this project must be written in English. +All project documentation files must be stored under `docs/`, except `README.md` and `AGENTS.md`. + +## Working Principles +1. Keep changes minimal and focused on the requested task. +2. Preserve existing public APIs unless a breaking change is explicitly requested. +3. Prefer clear, maintainable code over clever shortcuts. +4. Do not modify unrelated files. +5. Never add secrets, credentials, or tokens to the repository. + +## Testing Expectations +1. Add or update tests for behavior changes. +2. Keep tests deterministic and fast. +3. Prefer table-driven tests where they improve readability. +4. Run relevant tests locally before finishing changes. + +## Definition of Done (DoD) + +### Purpose +The Definition of Done defines the minimum quality bar for every completed change in this repository. + +### Mandatory Criteria +1. Tests +- Every code change is covered by tests where applicable. +- New functionality includes new tests. +- Bug fixes include at least one regression test. + +1. Functional documentation +- Implemented functionality is documented. +- Public API-relevant changes are reflected in README and/or docs. + +1. Documentation standards +- Documentation is written in English. +- Documentation files are placed under `docs/`. +- Exceptions: `README.md` and `AGENTS.md` remain at repository root. + +### Technical Completion Criteria +1. Build and test status +- The project builds successfully. +- Relevant test commands run successfully. + +1. No unresolved critical issues +- No new blocking errors are introduced. +- Known non-blocking warnings are acceptable only if unrelated to the change or documented. + +1. Documentation structure +- Links to moved or newly added docs are valid. +- Documentation structure remains consistent with project rules. + +### Review Checklist (Quick) +- [ ] Change is implemented and meets acceptance criteria. +- [ ] Tests were added/updated and pass. +- [ ] Functionality is documented. +- [ ] Documentation is in English. +- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`). +- [ ] No critical regressions found. diff --git a/docs/DEFINITION_OF_DONE.md b/docs/DEFINITION_OF_DONE.md new file mode 100644 index 0000000..c254e82 --- /dev/null +++ b/docs/DEFINITION_OF_DONE.md @@ -0,0 +1,44 @@ +# Definition of Done (DoD) + +## Purpose + +This Definition of Done defines the minimum quality bar for every completed change in this repository. + +## Mandatory Criteria + +1. Tests +- Every code change is covered by tests where applicable. +- New functionality includes new tests. +- Bug fixes include at least one regression test. + +1. Functional documentation +- Implemented functionality is documented. +- Public API-relevant changes are reflected in README and/or docs. + +1. Documentation standards +- Documentation is written in English. +- Documentation files are placed under `docs/`. +- Exceptions: `README.md` and `AGENTS.md` remain at repository root. + +## Technical Completion Criteria + +1. Build and test status +- The project builds successfully. +- Relevant test commands run successfully. + +1. No unresolved critical issues +- No new blocking errors are introduced. +- Known non-blocking warnings are acceptable only if unrelated to the change or documented. + +1. Documentation links and structure +- Links to moved or newly added docs are valid. +- Documentation structure remains consistent with project rules. + +## Review Checklist (Quick) + +- [ ] Change is implemented and meets acceptance criteria. +- [ ] Tests were added/updated and pass. +- [ ] Functionality is documented. +- [ ] Documentation is in English. +- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`). +- [ ] No critical regressions found. diff --git a/scripts/Reconcile-ProjectStandards.ps1 b/scripts/Reconcile-ProjectStandards.ps1 new file mode 100644 index 0000000..63ee8f9 --- /dev/null +++ b/scripts/Reconcile-ProjectStandards.ps1 @@ -0,0 +1,121 @@ +[CmdletBinding()] +param( + [string]$RootPath = "C:\Users\stefan\git", + [string]$StandardsRepoPath = "C:\Users\stefan\git\project-standards", + [string[]]$Exclude = @('project-standards'), + [switch]$CheckOnly, + [switch]$Watch, + [int]$IntervalSeconds = 60 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$agentsTemplate = Join-Path $StandardsRepoPath 'templates/AGENTS.base.md' +$dodTemplate = Join-Path $StandardsRepoPath 'templates/DEFINITION_OF_DONE.base.md' + +if (-not (Test-Path -Path $agentsTemplate -PathType Leaf)) { + throw "AGENTS template not found: $agentsTemplate" +} + +if (-not (Test-Path -Path $dodTemplate -PathType Leaf)) { + throw "DoD template not found: $dodTemplate" +} + +if ($IntervalSeconds -lt 5) { + throw 'IntervalSeconds must be >= 5.' +} + +function Get-ContentHashOrMissing { + param([string]$Path) + + if (-not (Test-Path -Path $Path -PathType Leaf)) { + return '__MISSING__' + } + + return (Get-FileHash -Path $Path -Algorithm SHA256).Hash +} + +function Ensure-FileFromTemplate { + param( + [string]$Template, + [string]$Target, + [switch]$OnlyCheck + ) + + $templateHash = Get-ContentHashOrMissing -Path $Template + $targetHash = Get-ContentHashOrMissing -Path $Target + + if ($templateHash -eq $targetHash) { + return 'ok' + } + + if ($OnlyCheck) { + return 'drift' + } + + $parent = Split-Path -Parent $Target + if (-not (Test-Path -Path $parent -PathType Container)) { + New-Item -ItemType Directory -Path $parent | Out-Null + } + + Copy-Item -Path $Template -Destination $Target -Force + return 'updated' +} + +function Invoke-ReconcileOnce { + param([switch]$OnlyCheck) + + $resolvedRoot = (Resolve-Path -Path $RootPath -ErrorAction Stop).Path + $repos = Get-ChildItem -Path $resolvedRoot -Directory | Where-Object { + $Exclude -notcontains $_.Name + } + + $summary = [ordered]@{ + scanned = 0 + updated = 0 + drift = 0 + } + + foreach ($repo in $repos) { + $repoPath = $repo.FullName + $agentsTarget = Join-Path $repoPath 'AGENTS.md' + $dodTarget = Join-Path (Join-Path $repoPath 'docs') 'DEFINITION_OF_DONE.md' + + $summary.scanned++ + + $agentsState = Ensure-FileFromTemplate -Template $agentsTemplate -Target $agentsTarget -OnlyCheck:$OnlyCheck + $dodState = Ensure-FileFromTemplate -Template $dodTemplate -Target $dodTarget -OnlyCheck:$OnlyCheck + + if ($agentsState -eq 'updated' -or $dodState -eq 'updated') { + $summary.updated++ + Write-Host "UPDATED: $repoPath" + continue + } + + if ($agentsState -eq 'drift' -or $dodState -eq 'drift') { + $summary.drift++ + Write-Host "DRIFT: $repoPath" + continue + } + + Write-Host "OK: $repoPath" + } + + Write-Host "Summary -> scanned=$($summary.scanned), updated=$($summary.updated), drift=$($summary.drift)" + return $summary +} + +if ($Watch) { + while ($true) { + $now = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + Write-Host "[$now] Reconciling project standards..." + [void](Invoke-ReconcileOnce -OnlyCheck:$CheckOnly) + Start-Sleep -Seconds $IntervalSeconds + } +} + +$result = Invoke-ReconcileOnce -OnlyCheck:$CheckOnly +if ($CheckOnly -and $result.drift -gt 0) { + exit 1 +} diff --git a/scripts/reconcile-project-standards.sh b/scripts/reconcile-project-standards.sh new file mode 100644 index 0000000..0d280e6 --- /dev/null +++ b/scripts/reconcile-project-standards.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env sh +set -eu + +ROOT_PATH="/c/Users/stefan/git" +STANDARDS_REPO="/c/Users/stefan/git/project-standards" +EXCLUDE_NAME="project-standards" +CHECK_ONLY=0 +WATCH=0 +INTERVAL=60 + +usage() { + cat <<'EOF' +Usage: + reconcile-project-standards.sh [options] + +Options: + --root Root folder containing repositories + --standards-repo Path to project-standards repository + --exclude First-level directory name to skip (default: project-standards) + --check-only Check drift only, do not update files + --watch Continuously scan and reconcile + --interval Watch interval in seconds (default: 60) + -h, --help Show this help +EOF +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --root) + ROOT_PATH=$2 + shift 2 + ;; + --standards-repo) + STANDARDS_REPO=$2 + shift 2 + ;; + --exclude) + EXCLUDE_NAME=$2 + shift 2 + ;; + --check-only) + CHECK_ONLY=1 + shift + ;; + --watch) + WATCH=1 + shift + ;; + --interval) + INTERVAL=$2 + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [ "$INTERVAL" -lt 5 ]; then + echo "interval must be >= 5" >&2 + exit 1 +fi + +AGENTS_TEMPLATE="$STANDARDS_REPO/templates/AGENTS.base.md" +DOD_TEMPLATE="$STANDARDS_REPO/templates/DEFINITION_OF_DONE.base.md" + +if [ ! -f "$AGENTS_TEMPLATE" ]; then + echo "AGENTS template not found: $AGENTS_TEMPLATE" >&2 + exit 1 +fi + +if [ ! -f "$DOD_TEMPLATE" ]; then + echo "DoD template not found: $DOD_TEMPLATE" >&2 + exit 1 +fi + +hash_or_missing() { + path=$1 + if [ ! -f "$path" ]; then + printf "%s" "__MISSING__" + return 0 + fi + + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$path" | awk '{print $1}' + else + shasum -a 256 "$path" | awk '{print $1}' + fi +} + +ensure_file() { + template=$1 + target=$2 + + template_hash=$(hash_or_missing "$template") + target_hash=$(hash_or_missing "$target") + + if [ "$template_hash" = "$target_hash" ]; then + printf "%s" "ok" + return 0 + fi + + if [ "$CHECK_ONLY" -eq 1 ]; then + printf "%s" "drift" + return 0 + fi + + mkdir -p "$(dirname "$target")" + cp "$template" "$target" + printf "%s" "updated" +} + +run_once() { + scanned=0 + updated=0 + drift=0 + + for repo in "$ROOT_PATH"/*; do + [ -d "$repo" ] || continue + + name=$(basename "$repo") + [ "$name" = "$EXCLUDE_NAME" ] && continue + + scanned=$((scanned + 1)) + + agents_target="$repo/AGENTS.md" + dod_target="$repo/docs/DEFINITION_OF_DONE.md" + + agents_state=$(ensure_file "$AGENTS_TEMPLATE" "$agents_target") + dod_state=$(ensure_file "$DOD_TEMPLATE" "$dod_target") + + if [ "$agents_state" = "updated" ] || [ "$dod_state" = "updated" ]; then + updated=$((updated + 1)) + echo "UPDATED: $repo" + continue + fi + + if [ "$agents_state" = "drift" ] || [ "$dod_state" = "drift" ]; then + drift=$((drift + 1)) + echo "DRIFT: $repo" + continue + fi + + echo "OK: $repo" + done + + echo "Summary -> scanned=$scanned, updated=$updated, drift=$drift" + + if [ "$CHECK_ONLY" -eq 1 ] && [ "$drift" -gt 0 ]; then + return 1 + fi + + return 0 +} + +if [ "$WATCH" -eq 1 ]; then + while :; do + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Reconciling project standards..." + run_once || true + sleep "$INTERVAL" + done +else + run_once +fi diff --git a/utils_test.go b/utils_test.go index 5003eea..05960f3 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,7 +1,6 @@ package util import ( - "fmt" "os" "os/user" "path/filepath" @@ -67,7 +66,7 @@ func TestIsSuperUser(t *testing.T) { func TestGlobalConfigurationDirectoryWindows(t *testing.T) { if runtime.GOOS != "windows" { - t.Skip(fmt.Sprintf("Skipping on OS %s", runtime.GOOS)) + t.Skipf("Skipping on OS %s", runtime.GOOS) } appFolder := GetGlobalConfigurationDirectory("myapp") assert.Equal(t, filepath.Join(os.Getenv("APPDATA"), "myapp"), appFolder) @@ -75,7 +74,7 @@ func TestGlobalConfigurationDirectoryWindows(t *testing.T) { func TestGlobalConfigurationDirectoryLinux(t *testing.T) { if runtime.GOOS != "linux" { - t.Skip(fmt.Sprintf("Skipping on OS %s", runtime.GOOS)) + t.Skipf("Skipping on OS %s", runtime.GOOS) } appFolder := GetGlobalConfigurationDirectory("myapp") assert.Equal(t, "/etc/myapp", appFolder) @@ -83,8 +82,17 @@ func TestGlobalConfigurationDirectoryLinux(t *testing.T) { func TestGlobalConfigurationDirectoryMacOS(t *testing.T) { if runtime.GOOS != "darwin" { - t.Skip(fmt.Sprintf("Skipping on OS %s", runtime.GOOS)) + t.Skipf("Skipping on OS %s", runtime.GOOS) } appFolder := GetGlobalConfigurationDirectory("myapp") assert.Equal(t, "/etc/myapp", appFolder) } + +func TestMkDir(t *testing.T) { + path := "c:/tmp/bla/blub" + if !FileExists(path) { + err := os.MkdirAll(path, os.ModeDir) + assert.Nil(t, err) + assert.True(t, FileExists(path)) + } +} From a24fffa0e6167a2b4340028643d86635965faaf5 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 13:31:23 +0200 Subject: [PATCH 02/17] Add .gitignore, enhance AGENTS and Definition of Done documentation, update README with project details, and improve test coverage for Windows-specific functionality --- .gitignore | 1 + AGENTS.md | 2 + README.md | 52 +++++++++++++++- coverage | 1 + docs/DEFINITION_OF_DONE.md | 2 + go.mod | 2 +- go.sum | 30 ++++----- os_windows.go | 17 ++++-- os_windows_test.go | 47 ++++++++++++++ utils_test.go | 121 +++++++++++++------------------------ 10 files changed, 171 insertions(+), 104 deletions(-) create mode 100644 .gitignore create mode 100644 coverage create mode 100644 os_windows_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30bcfa4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.build/ diff --git a/AGENTS.md b/AGENTS.md index 1aaa2e8..6d49371 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,7 @@ 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. +- Minimum code coverage is 80% (statements, measured with `go test -cover`). 1. Functional documentation - Implemented functionality is documented. @@ -57,6 +58,7 @@ 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. +- [ ] Code 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/README.md b/README.md index 2142931..7077afc 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,57 @@ [![Build Status](https://drone.yoorie.de/api/badges/go-lib/util/status.svg)](https://drone.yoorie.de/go-lib/util) -## Documentation +## Project Description -Is missed so far and will be created soon. +This repository provides a small, cross-platform utility package for Go projects. +It focuses on common helpers that are often reimplemented in multiple services, +such as file checks, safe path joining for URL-like strings, and OS-specific +configuration directory handling. + +The package is intentionally lightweight and easy to reuse in CLI tools, +daemons, and backend services. + +## Included Utilities + +- `FileExists(fileName string) bool` + - Returns whether a file exists on disk. +- `JoiningSlash(elem ...string) string` + - Joins path segments with exactly one slash between elements. +- `GetGlobalConfigurationDirectory(appname string) string` + - Returns an operating-system-specific global configuration directory. + - Linux and macOS: `/etc/` + - Windows: `%APPDATA%\\` +- `GetGlobalConfigurationFile(appname string, file string) string` + - Builds a full path to a config file inside the global config directory. +- `IsSuperUser() bool` + - Detects whether the current process runs with elevated privileges. + +## Installation + +```bash +go get scm.yoorie.de/go-lib/util +``` + +## Example + +```go +package main + +import ( + "fmt" + + "scm.yoorie.de/go-lib/util" +) + +func main() { + if util.FileExists("config.yaml") { + fmt.Println("config file found") + } + + fmt.Println(util.JoiningSlash("/api", "v1", "users")) + fmt.Println(util.GetGlobalConfigurationFile("myapp", "config.yaml")) +} +``` --- Copyright © 2023 yoorie.de diff --git a/coverage b/coverage new file mode 100644 index 0000000..5f02b11 --- /dev/null +++ b/coverage @@ -0,0 +1 @@ +mode: set diff --git a/docs/DEFINITION_OF_DONE.md b/docs/DEFINITION_OF_DONE.md index c254e82..9d85002 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. +- Minimum code coverage is 80% (statements, measured with `go test -cover`). 1. Functional documentation - Implemented functionality is documented. @@ -38,6 +39,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. +- [ ] Code 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/go.mod b/go.mod index 1b98bc3..0285bb6 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module scm.yoorie.de/go-lib/util go 1.16 require ( - github.com/stretchr/testify v1.8.2 + github.com/smartystreets/goconvey v1.6.4 golang.org/x/sys v0.6.0 ) diff --git a/go.sum b/go.sum index 3b079bb..7d950f9 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,15 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= diff --git a/os_windows.go b/os_windows.go index ea7e3ce..677ecdf 100644 --- a/os_windows.go +++ b/os_windows.go @@ -11,6 +11,13 @@ import ( "golang.org/x/sys/windows" ) +var ( + allocateAndInitializeSid = windows.AllocateAndInitializeSid + freeSid = windows.FreeSid + tokenIsMember = func(token windows.Token, sid *windows.SID) (bool, error) { return token.IsMember(sid) } + fatalf = log.Fatalf +) + // IsSuperUser returns true, if the current user is a super user // A.K.A root, Administrator etc func IsSuperUser() bool { @@ -20,7 +27,7 @@ func IsSuperUser() bool { // official windows documentation. The Go API for this is a // direct wrap around the official C++ API. // See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership - err := windows.AllocateAndInitializeSid( + err := allocateAndInitializeSid( &windows.SECURITY_NT_AUTHORITY, 2, windows.SECURITY_BUILTIN_DOMAIN_RID, @@ -28,19 +35,19 @@ func IsSuperUser() bool { 0, 0, 0, 0, 0, 0, &sid) if err != nil { - log.Fatalf("SID Error: %s", err) + fatalf("SID Error: %s", err) return false } - defer windows.FreeSid(sid) + defer freeSid(sid) // This appears to cast a null pointer so I'm not sure why this // works, but this guy says it does and it Works for Me™: // https://github.com/golang/go/issues/28804#issuecomment-438838144 token := windows.Token(0) - member, err := token.IsMember(sid) + member, err := tokenIsMember(token, sid) if err != nil { - log.Fatalf("Token Membership Error: %s", err) + fatalf("Token Membership Error: %s", err) return false } diff --git a/os_windows_test.go b/os_windows_test.go new file mode 100644 index 0000000..f33a65a --- /dev/null +++ b/os_windows_test.go @@ -0,0 +1,47 @@ +//go:build windows +// +build windows + +package util + +import ( + "errors" + "testing" + + . "github.com/smartystreets/goconvey/convey" + "golang.org/x/sys/windows" +) + +func TestIsSuperUser_SidAllocationError(t *testing.T) { + Convey("IsSuperUser should return false when SID allocation fails", t, func() { + origAllocate := allocateAndInitializeSid + origFatalf := fatalf + defer func() { + allocateAndInitializeSid = origAllocate + fatalf = origFatalf + }() + + allocateAndInitializeSid = func( + authority *windows.SidIdentifierAuthority, + subAuthorityCount byte, + subAuthority0 uint32, + subAuthority1 uint32, + subAuthority2 uint32, + subAuthority3 uint32, + subAuthority4 uint32, + subAuthority5 uint32, + subAuthority6 uint32, + subAuthority7 uint32, + sid **windows.SID, + ) error { + return errors.New("forced sid allocation error") + } + + fatalCalled := false + fatalf = func(format string, v ...interface{}) { + fatalCalled = true + } + + So(IsSuperUser(), ShouldBeFalse) + So(fatalCalled, ShouldBeTrue) + }) +} diff --git a/utils_test.go b/utils_test.go index 05960f3..1ba39da 100644 --- a/utils_test.go +++ b/utils_test.go @@ -2,97 +2,60 @@ package util import ( "os" - "os/user" "path/filepath" - "runtime" - "strings" "testing" - "github.com/stretchr/testify/assert" + . "github.com/smartystreets/goconvey/convey" ) -func TestFileExist(t *testing.T) { - assert.True(t, FileExists("utils.go")) +func TestFileExists(t *testing.T) { + Convey("FileExists should report existing and missing files", t, func() { + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "exists.txt") + + err := os.WriteFile(tmpFile, []byte("ok"), 0o600) + So(err, ShouldBeNil) + + So(FileExists(tmpFile), ShouldBeTrue) + So(FileExists(filepath.Join(tmpDir, "missing.txt")), ShouldBeFalse) + }) } -func TestFileExistNot(t *testing.T) { - assert.True(t, !FileExists("Utils2.go")) +func TestJoiningSlash(t *testing.T) { + Convey("JoiningSlash should combine URL-like segments safely", t, func() { + So(JoiningSlash("http://my.tld/docs/", "bla/", "blub/"), ShouldEqual, "http://my.tld/docs/bla/blub/") + So(JoiningSlash("http://my.tld", "bla", "blub"), ShouldEqual, "http://my.tld/bla/blub") + So(JoiningSlash("http://my.tld/", "bla", "blub"), ShouldEqual, "http://my.tld/bla/blub") + So(JoiningSlash("http://my.tld", "bla/", "blub"), ShouldEqual, "http://my.tld/bla/blub") + So(JoiningSlash("http://my.tld/docs", "bla/", "blub"), ShouldEqual, "http://my.tld/docs/bla/blub") + So(JoiningSlash("http://my.tld/docs/", "bla/", "blub"), ShouldEqual, "http://my.tld/docs/bla/blub") + So(JoiningSlash("", "api", "v1"), ShouldEqual, "api/v1") + So(JoiningSlash("", "", ""), ShouldEqual, "") + }) } -func TestJoiningSlash1(t *testing.T) { - actual := JoiningSlash("http://my.tld/docs/", "bla/", "blub/") - expected := "http://my.tld/docs/bla/blub/" - assert.Equal(t, expected, actual) +func TestSingleJoiningSlash(t *testing.T) { + Convey("singleJoiningSlash should handle slash edge cases", t, func() { + So(singleJoiningSlash("a/", "/b"), ShouldEqual, "a/b") + So(singleJoiningSlash("a", "b"), ShouldEqual, "a/b") + So(singleJoiningSlash("a/", "b"), ShouldEqual, "a/b") + So(singleJoiningSlash("a", "/b"), ShouldEqual, "a/b") + }) } -func TestJoiningSlash2(t *testing.T) { - actual := JoiningSlash("http://my.tld", "bla", "blub") - assert.Equal(t, "http://my.tld/bla/blub", actual) +func TestGetGlobalConfiguration(t *testing.T) { + Convey("GetGlobalConfigurationFile should create the expected path", t, func() { + appName := "myapp" + fileName := "config.yaml" + + dir := GetGlobalConfigurationDirectory(appName) + So(GetGlobalConfigurationFile(appName, fileName), ShouldEqual, filepath.Join(dir, fileName)) + }) } -func TestJoiningSlash3(t *testing.T) { - actual := JoiningSlash("http://my.tld/", "bla", "blub") - assert.Equal(t, "http://my.tld/bla/blub", actual) -} -func TestJoiningSlash4(t *testing.T) { - actual := JoiningSlash("http://my.tld", "bla/", "blub") - assert.Equal(t, "http://my.tld/bla/blub", actual) -} -func TestJoiningSlash5(t *testing.T) { - actual := JoiningSlash("http://my.tld/docs", "bla/", "blub") - expected := "http://my.tld/docs/bla/blub" - assert.Equal(t, expected, actual) -} - -func TestJoiningSlash6(t *testing.T) { - actual := JoiningSlash("http://my.tld/docs/", "bla/", "blub") - expected := "http://my.tld/docs/bla/blub" - assert.Equal(t, expected, actual) -} - -/* -Can run only as admin within windows or linux -e.g. sudo TESTASSUDO=yes /usr/local/go/bin/go test -timeout 30s -run ^TestIsSuperUser$ -*/ func TestIsSuperUser(t *testing.T) { - if !strings.EqualFold(os.Getenv("TESTASSUDO"), "yes") { - t.Skip("Skipping in normal tests") - } - cuser, err := user.Current() - assert.Nil(t, err) - assert.NotNil(t, cuser) - assert.True(t, IsSuperUser()) -} - -func TestGlobalConfigurationDirectoryWindows(t *testing.T) { - if runtime.GOOS != "windows" { - t.Skipf("Skipping on OS %s", runtime.GOOS) - } - appFolder := GetGlobalConfigurationDirectory("myapp") - assert.Equal(t, filepath.Join(os.Getenv("APPDATA"), "myapp"), appFolder) -} - -func TestGlobalConfigurationDirectoryLinux(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skipf("Skipping on OS %s", runtime.GOOS) - } - appFolder := GetGlobalConfigurationDirectory("myapp") - assert.Equal(t, "/etc/myapp", appFolder) -} - -func TestGlobalConfigurationDirectoryMacOS(t *testing.T) { - if runtime.GOOS != "darwin" { - t.Skipf("Skipping on OS %s", runtime.GOOS) - } - appFolder := GetGlobalConfigurationDirectory("myapp") - assert.Equal(t, "/etc/myapp", appFolder) -} - -func TestMkDir(t *testing.T) { - path := "c:/tmp/bla/blub" - if !FileExists(path) { - err := os.MkdirAll(path, os.ModeDir) - assert.Nil(t, err) - assert.True(t, FileExists(path)) - } + Convey("IsSuperUser should return a boolean without requiring elevated rights", t, func() { + result := IsSuperUser() + So(result, ShouldBeIn, []bool{true, false}) + }) } From 77ef79d8583475b5506f9e6890f28787ce534823 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 13:31:30 +0200 Subject: [PATCH 03/17] Fix formatting of variable declarations in os_windows.go --- os_windows.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/os_windows.go b/os_windows.go index 677ecdf..4943a2e 100644 --- a/os_windows.go +++ b/os_windows.go @@ -13,9 +13,9 @@ import ( var ( allocateAndInitializeSid = windows.AllocateAndInitializeSid - freeSid = windows.FreeSid - tokenIsMember = func(token windows.Token, sid *windows.SID) (bool, error) { return token.IsMember(sid) } - fatalf = log.Fatalf + freeSid = windows.FreeSid + tokenIsMember = func(token windows.Token, sid *windows.SID) (bool, error) { return token.IsMember(sid) } + fatalf = log.Fatalf ) // IsSuperUser returns true, if the current user is a super user From bfb4a30ce8b35fe592c5fb314b9179ab6cd18253 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 13:36:43 +0200 Subject: [PATCH 04/17] - add build artifacts/report rule (.build) to AGENTS - set DoD minimum automated test coverage to 80% - extend reconcile scripts (ps1/sh) to ensure .gitignore contains .build/ - sync repository standards files to latest template --- AGENTS.md | 6 +++-- docs/DEFINITION_OF_DONE.md | 4 +-- scripts/Reconcile-ProjectStandards.ps1 | 34 ++++++++++++++++++++++++-- scripts/reconcile-project-standards.sh | 34 ++++++++++++++++++++++++-- 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6d49371..da4e36e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,10 @@ All project documentation files must be stored under `docs/`, except `README.md` 3. Prefer table-driven tests where they improve readability. 4. Run relevant tests locally before finishing changes. +## 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`. + ## Definition of Done (DoD) ### Purpose @@ -31,7 +35,6 @@ 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. -- Minimum code coverage is 80% (statements, measured with `go test -cover`). 1. Functional documentation - Implemented functionality is documented. @@ -58,7 +61,6 @@ 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. -- [ ] Code 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 9d85002..e425c44 100644 --- a/docs/DEFINITION_OF_DONE.md +++ b/docs/DEFINITION_OF_DONE.md @@ -10,7 +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. -- Minimum code coverage is 80% (statements, measured with `go test -cover`). +- Automated test coverage is at least 80%. 1. Functional documentation - Implemented functionality is documented. @@ -39,7 +39,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. -- [ ] Code coverage is at least 80%. +- [ ] 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/scripts/Reconcile-ProjectStandards.ps1 b/scripts/Reconcile-ProjectStandards.ps1 index 63ee8f9..afce0ec 100644 --- a/scripts/Reconcile-ProjectStandards.ps1 +++ b/scripts/Reconcile-ProjectStandards.ps1 @@ -63,6 +63,34 @@ function Ensure-FileFromTemplate { return 'updated' } +function Ensure-GitIgnoreBuildEntry { + param( + [string]$GitIgnorePath, + [switch]$OnlyCheck + ) + + if (-not (Test-Path -Path $GitIgnorePath -PathType Leaf)) { + if ($OnlyCheck) { + return 'drift' + } + + Set-Content -Path $GitIgnorePath -Value '.build/' + return 'updated' + } + + $lines = Get-Content -Path $GitIgnorePath + if ($lines -contains '.build/') { + return 'ok' + } + + if ($OnlyCheck) { + return 'drift' + } + + Add-Content -Path $GitIgnorePath -Value '.build/' + return 'updated' +} + function Invoke-ReconcileOnce { param([switch]$OnlyCheck) @@ -81,19 +109,21 @@ function Invoke-ReconcileOnce { $repoPath = $repo.FullName $agentsTarget = Join-Path $repoPath 'AGENTS.md' $dodTarget = Join-Path (Join-Path $repoPath 'docs') 'DEFINITION_OF_DONE.md' + $gitIgnoreTarget = Join-Path $repoPath '.gitignore' $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 - if ($agentsState -eq 'updated' -or $dodState -eq 'updated') { + if ($agentsState -eq 'updated' -or $dodState -eq 'updated' -or $gitIgnoreState -eq 'updated') { $summary.updated++ Write-Host "UPDATED: $repoPath" continue } - if ($agentsState -eq 'drift' -or $dodState -eq 'drift') { + if ($agentsState -eq 'drift' -or $dodState -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 index 0d280e6..3c93b13 100644 --- a/scripts/reconcile-project-standards.sh +++ b/scripts/reconcile-project-standards.sh @@ -116,6 +116,34 @@ ensure_file() { printf "%s" "updated" } +ensure_gitignore_build_entry() { + gitignore_path=$1 + + if [ ! -f "$gitignore_path" ]; then + if [ "$CHECK_ONLY" -eq 1 ]; then + printf "%s" "drift" + return 0 + fi + + printf '%s\n' '.build/' > "$gitignore_path" + printf "%s" "updated" + return 0 + fi + + if grep -Fqx '.build/' "$gitignore_path"; then + printf "%s" "ok" + return 0 + fi + + if [ "$CHECK_ONLY" -eq 1 ]; then + printf "%s" "drift" + return 0 + fi + + printf '\n%s\n' '.build/' >> "$gitignore_path" + printf "%s" "updated" +} + run_once() { scanned=0 updated=0 @@ -131,17 +159,19 @@ run_once() { agents_target="$repo/AGENTS.md" dod_target="$repo/docs/DEFINITION_OF_DONE.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") - if [ "$agents_state" = "updated" ] || [ "$dod_state" = "updated" ]; then + if [ "$agents_state" = "updated" ] || [ "$dod_state" = "updated" ] || [ "$gitignore_state" = "updated" ]; then updated=$((updated + 1)) echo "UPDATED: $repo" continue fi - if [ "$agents_state" = "drift" ] || [ "$dod_state" = "drift" ]; then + if [ "$agents_state" = "drift" ] || [ "$dod_state" = "drift" ] || [ "$gitignore_state" = "drift" ]; then drift=$((drift + 1)) echo "DRIFT: $repo" continue From bc9b0ce88f11d6db0acd3dcf937a38197b148d62 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 13:48:46 +0200 Subject: [PATCH 05/17] 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 From 1da053fb67bb4e8934e943745de8bfd267fca7e3 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 14:15:29 +0200 Subject: [PATCH 06/17] - apply latest AGENTS and DoD templates - replace PowerShell reconcile script with shell-based reconcile workflow - add .gitattributes, .editorconfig, and project-local git hooks - configure .githooks as core.hooksPath - refresh .gitignore entries from the shared standard --- .editorconfig | 3 - .gitattributes | 1 - .gitignore | 1 + AGENTS.md | 7 + scripts/Reconcile-ProjectStandards.ps1 | 217 ------------------------- scripts/reconcile-project-standards.sh | 25 ++- 6 files changed, 30 insertions(+), 224 deletions(-) delete mode 100644 scripts/Reconcile-ProjectStandards.ps1 diff --git a/.editorconfig b/.editorconfig index b9dc5ac..fb18251 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,6 +14,3 @@ indent_size = 4 [*.md] trim_trailing_whitespace = false - -[*.ps1] -end_of_line = crlf diff --git a/.gitattributes b/.gitattributes index 0b57dc4..826bbe8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,6 +13,5 @@ Makefile text eol=lf # Keep native Windows script formats -*.ps1 text eol=crlf *.bat text eol=crlf *.cmd text eol=crlf diff --git a/.gitignore b/.gitignore index 4b00f8d..d22f48a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ coverage.out Thumbs.db .vscode/ .idea/ + diff --git a/AGENTS.md b/AGENTS.md index 92be300..32da315 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,13 @@ All project documentation files must be stored under `docs/`, except `README.md` 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`. +## Git Bash Execution Defaults +1. Repository maintenance scripts are executed with Git Bash shell on Windows. +2. Default repository root is `~/git`. +3. The repository root can be overridden via `GO_GIT_ROOT`. +4. The standards repository defaults to `$ROOT_PATH/project-standards`. +5. The standards repository path can be overridden via `GO_PROJECT_STANDARDS`. + ## Definition of Done (DoD) ### Purpose diff --git a/scripts/Reconcile-ProjectStandards.ps1 b/scripts/Reconcile-ProjectStandards.ps1 deleted file mode 100644 index ad39809..0000000 --- a/scripts/Reconcile-ProjectStandards.ps1 +++ /dev/null @@ -1,217 +0,0 @@ -[CmdletBinding()] -param( - [string]$RootPath = "C:\Users\stefan\git", - [string]$StandardsRepoPath = "C:\Users\stefan\git\project-standards", - [string[]]$Exclude = @('project-standards'), - [switch]$CheckOnly, - [switch]$Watch, - [int]$IntervalSeconds = 60 -) - -Set-StrictMode -Version Latest -$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" -} - -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.' -} - -function Get-ContentHashOrMissing { - param([string]$Path) - - if (-not (Test-Path -Path $Path -PathType Leaf)) { - return '__MISSING__' - } - - return (Get-FileHash -Path $Path -Algorithm SHA256).Hash -} - -function Ensure-FileFromTemplate { - param( - [string]$Template, - [string]$Target, - [switch]$OnlyCheck - ) - - $templateHash = Get-ContentHashOrMissing -Path $Template - $targetHash = Get-ContentHashOrMissing -Path $Target - - if ($templateHash -eq $targetHash) { - return 'ok' - } - - if ($OnlyCheck) { - return 'drift' - } - - $parent = Split-Path -Parent $Target - if (-not (Test-Path -Path $parent -PathType Container)) { - New-Item -ItemType Directory -Path $parent | Out-Null - } - - Copy-Item -Path $Template -Destination $Target -Force - return 'updated' -} - -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' - } - - New-Item -ItemType File -Path $GitIgnorePath | Out-Null - } - - $lines = Get-Content -Path $GitIgnorePath - - $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' - } - - if ($OnlyCheck) { - return 'drift' - } - - return 'updated' -} - -function Invoke-ReconcileOnce { - param([switch]$OnlyCheck) - - $resolvedRoot = (Resolve-Path -Path $RootPath -ErrorAction Stop).Path - $repos = Get-ChildItem -Path $resolvedRoot -Directory | Where-Object { - $Exclude -notcontains $_.Name - } - - $summary = [ordered]@{ - scanned = 0 - updated = 0 - drift = 0 - } - - foreach ($repo in $repos) { - $repoPath = $repo.FullName - $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 - $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 - $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 - $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 - } - - Write-Host "OK: $repoPath" - } - - Write-Host "Summary -> scanned=$($summary.scanned), updated=$($summary.updated), drift=$($summary.drift)" - return $summary -} - -if ($Watch) { - while ($true) { - $now = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' - Write-Host "[$now] Reconciling project standards..." - [void](Invoke-ReconcileOnce -OnlyCheck:$CheckOnly) - Start-Sleep -Seconds $IntervalSeconds - } -} - -$result = Invoke-ReconcileOnce -OnlyCheck:$CheckOnly -if ($CheckOnly -and $result.drift -gt 0) { - exit 1 -} diff --git a/scripts/reconcile-project-standards.sh b/scripts/reconcile-project-standards.sh index 68fa9a9..aa8cf9c 100755 --- a/scripts/reconcile-project-standards.sh +++ b/scripts/reconcile-project-standards.sh @@ -1,8 +1,9 @@ #!/usr/bin/env sh set -eu -ROOT_PATH="/c/Users/stefan/git" -STANDARDS_REPO="/c/Users/stefan/git/project-standards" +ROOT_PATH="${GO_GIT_ROOT:-$HOME/git}" +STANDARDS_REPO="" +STANDARDS_REPO_SET=0 EXCLUDE_NAME="project-standards" CHECK_ONLY=0 WATCH=0 @@ -21,6 +22,14 @@ Options: --watch Continuously scan and reconcile --interval Watch interval in seconds (default: 60) -h, --help Show this help + +Environment: + GO_GIT_ROOT Overrides default root path (default: ~/git) + GO_PROJECT_STANDARDS Overrides standards repository path + +Default resolution: + ROOT_PATH defaults to GO_GIT_ROOT or ~/git. + STANDARDS_REPO defaults to GO_PROJECT_STANDARDS or $ROOT_PATH/project-standards. EOF } @@ -32,6 +41,7 @@ while [ "$#" -gt 0 ]; do ;; --standards-repo) STANDARDS_REPO=$2 + STANDARDS_REPO_SET=1 shift 2 ;; --exclude) @@ -62,6 +72,14 @@ while [ "$#" -gt 0 ]; do esac done +if [ "$STANDARDS_REPO_SET" -ne 1 ]; then + if [ -n "${GO_PROJECT_STANDARDS:-}" ]; then + STANDARDS_REPO="$GO_PROJECT_STANDARDS" + else + STANDARDS_REPO="$ROOT_PATH/project-standards" + fi +fi + if [ "$INTERVAL" -lt 5 ]; then echo "interval must be >= 5" >&2 exit 1 @@ -162,6 +180,7 @@ ensure_gitignore_entries_from_template() { fi while IFS= read -r entry; do + entry=$(printf '%s' "$entry" | tr -d '\r') [ -n "$entry" ] || continue case "$entry" in \#*) @@ -169,7 +188,7 @@ ensure_gitignore_entries_from_template() { ;; esac - if grep -Fqx "$entry" "$gitignore_path"; then + if tr -d '\r' < "$gitignore_path" | grep -Fqx "$entry"; then continue fi From 8ace13074f51e651f4f717c9929c5f5d95251914 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 14:54:31 +0200 Subject: [PATCH 07/17] Update CI pipeline to use Go 1.25.8, enhance Definition of Done with SonarQube checks, refactor SID allocation in Windows functions, and improve test coverage for JoiningSlash function. --- .drone.yml | 2 +- AGENTS.md | 4 ++++ docs/DEFINITION_OF_DONE.md | 4 ++++ os_windows.go | 18 +++++++++++------- os_windows_test.go | 20 ++++---------------- utils.go | 1 - utils_test.go | 20 ++++++++++++++------ 7 files changed, 38 insertions(+), 31 deletions(-) diff --git a/.drone.yml b/.drone.yml index 0cf4b3a..39db88c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,7 +4,7 @@ name: go-lib/util steps: - name: test - image: golang:1.18 + image: golang:1.25.8 commands: - go get ./... - go test ./... diff --git a/AGENTS.md b/AGENTS.md index 32da315..32fd1c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,6 +69,9 @@ The Definition of Done defines the minimum quality bar for every completed chang - No new blocking errors are introduced. - Known non-blocking warnings are acceptable only if unrelated to the change or documented. +1. SonarQube status +- No SonarQube errors are present. + 1. Documentation structure - Links to moved or newly added docs are valid. - Documentation structure remains consistent with project rules. @@ -81,4 +84,5 @@ The Definition of Done defines the minimum quality bar for every completed chang - [ ] Functionality is documented. - [ ] Documentation is in English. - [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`). +- [ ] No SonarQube errors are present. - [ ] No critical regressions found. diff --git a/docs/DEFINITION_OF_DONE.md b/docs/DEFINITION_OF_DONE.md index 884a425..a47d72f 100644 --- a/docs/DEFINITION_OF_DONE.md +++ b/docs/DEFINITION_OF_DONE.md @@ -32,6 +32,9 @@ This Definition of Done defines the minimum quality bar for every completed chan - No new blocking errors are introduced. - Known non-blocking warnings are acceptable only if unrelated to the change or documented. +1. SonarQube status +- No SonarQube errors are present. + 1. Documentation links and structure - Links to moved or newly added docs are valid. - Documentation structure remains consistent with project rules. @@ -45,4 +48,5 @@ This Definition of Done defines the minimum quality bar for every completed chan - [ ] Functionality is documented. - [ ] Documentation is in English. - [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`). +- [ ] No SonarQube errors are present. - [ ] No critical regressions found. diff --git a/os_windows.go b/os_windows.go index 4943a2e..7383ac8 100644 --- a/os_windows.go +++ b/os_windows.go @@ -13,6 +13,16 @@ import ( var ( allocateAndInitializeSid = windows.AllocateAndInitializeSid + allocateAdminGroupSid = func(sid **windows.SID) error { + return allocateAndInitializeSid( + &windows.SECURITY_NT_AUTHORITY, + 2, + windows.SECURITY_BUILTIN_DOMAIN_RID, + windows.DOMAIN_ALIAS_RID_ADMINS, + 0, 0, 0, 0, 0, 0, + sid, + ) + } freeSid = windows.FreeSid tokenIsMember = func(token windows.Token, sid *windows.SID) (bool, error) { return token.IsMember(sid) } fatalf = log.Fatalf @@ -27,13 +37,7 @@ func IsSuperUser() bool { // official windows documentation. The Go API for this is a // direct wrap around the official C++ API. // See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership - err := allocateAndInitializeSid( - &windows.SECURITY_NT_AUTHORITY, - 2, - windows.SECURITY_BUILTIN_DOMAIN_RID, - windows.DOMAIN_ALIAS_RID_ADMINS, - 0, 0, 0, 0, 0, 0, - &sid) + err := allocateAdminGroupSid(&sid) if err != nil { fatalf("SID Error: %s", err) return false diff --git a/os_windows_test.go b/os_windows_test.go index f33a65a..6415230 100644 --- a/os_windows_test.go +++ b/os_windows_test.go @@ -13,31 +13,19 @@ import ( func TestIsSuperUser_SidAllocationError(t *testing.T) { Convey("IsSuperUser should return false when SID allocation fails", t, func() { - origAllocate := allocateAndInitializeSid + origAllocate := allocateAdminGroupSid origFatalf := fatalf defer func() { - allocateAndInitializeSid = origAllocate + allocateAdminGroupSid = origAllocate fatalf = origFatalf }() - allocateAndInitializeSid = func( - authority *windows.SidIdentifierAuthority, - subAuthorityCount byte, - subAuthority0 uint32, - subAuthority1 uint32, - subAuthority2 uint32, - subAuthority3 uint32, - subAuthority4 uint32, - subAuthority5 uint32, - subAuthority6 uint32, - subAuthority7 uint32, - sid **windows.SID, - ) error { + allocateAdminGroupSid = func(_ **windows.SID) error { return errors.New("forced sid allocation error") } fatalCalled := false - fatalf = func(format string, v ...interface{}) { + fatalf = func(_ string, _ ...interface{}) { fatalCalled = true } diff --git a/utils.go b/utils.go index c607d78..0601455 100644 --- a/utils.go +++ b/utils.go @@ -34,7 +34,6 @@ func joiningSlash(elem []string) string { } func singleJoiningSlash(a, b string) string { - filepath.Join(a, b) aslash := strings.HasSuffix(a, "/") bslash := strings.HasPrefix(b, "/") switch { diff --git a/utils_test.go b/utils_test.go index 1ba39da..cd06eb8 100644 --- a/utils_test.go +++ b/utils_test.go @@ -23,12 +23,20 @@ func TestFileExists(t *testing.T) { func TestJoiningSlash(t *testing.T) { Convey("JoiningSlash should combine URL-like segments safely", t, func() { - So(JoiningSlash("http://my.tld/docs/", "bla/", "blub/"), ShouldEqual, "http://my.tld/docs/bla/blub/") - So(JoiningSlash("http://my.tld", "bla", "blub"), ShouldEqual, "http://my.tld/bla/blub") - So(JoiningSlash("http://my.tld/", "bla", "blub"), ShouldEqual, "http://my.tld/bla/blub") - So(JoiningSlash("http://my.tld", "bla/", "blub"), ShouldEqual, "http://my.tld/bla/blub") - So(JoiningSlash("http://my.tld/docs", "bla/", "blub"), ShouldEqual, "http://my.tld/docs/bla/blub") - So(JoiningSlash("http://my.tld/docs/", "bla/", "blub"), ShouldEqual, "http://my.tld/docs/bla/blub") + const ( + baseURL = "http://my.tld" + docsURL = "http://my.tld/docs" + expectedRoot = "http://my.tld/bla/blub" + expectedDocs = "http://my.tld/docs/bla/blub" + expectedDocsS = "http://my.tld/docs/bla/blub/" + ) + + So(JoiningSlash(docsURL+"/", "bla/", "blub/"), ShouldEqual, expectedDocsS) + So(JoiningSlash(baseURL, "bla", "blub"), ShouldEqual, expectedRoot) + So(JoiningSlash(baseURL+"/", "bla", "blub"), ShouldEqual, expectedRoot) + So(JoiningSlash(baseURL, "bla/", "blub"), ShouldEqual, expectedRoot) + So(JoiningSlash(docsURL, "bla/", "blub"), ShouldEqual, expectedDocs) + So(JoiningSlash(docsURL+"/", "bla/", "blub"), ShouldEqual, expectedDocs) So(JoiningSlash("", "api", "v1"), ShouldEqual, "api/v1") So(JoiningSlash("", "", ""), ShouldEqual, "") }) From 2ec2fcd2ab232c76be7e15daa85c19d226fdde0b Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 14:54:46 +0200 Subject: [PATCH 08/17] Fix formatting of variable declarations in os_windows.go --- os_windows.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/os_windows.go b/os_windows.go index 7383ac8..a50c2a7 100644 --- a/os_windows.go +++ b/os_windows.go @@ -23,9 +23,9 @@ var ( sid, ) } - freeSid = windows.FreeSid - tokenIsMember = func(token windows.Token, sid *windows.SID) (bool, error) { return token.IsMember(sid) } - fatalf = log.Fatalf + freeSid = windows.FreeSid + tokenIsMember = func(token windows.Token, sid *windows.SID) (bool, error) { return token.IsMember(sid) } + fatalf = log.Fatalf ) // IsSuperUser returns true, if the current user is a super user From 6236717fdb9f622e73eba123653ed1043b1d8b66 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 15:01:08 +0200 Subject: [PATCH 09/17] Refactor documentation for clarity and consistency, update test function naming convention --- AGENTS.md | 46 +++++++++++++++++++++++++++++--------- README.md | 20 ++++++++--------- docs/DEFINITION_OF_DONE.md | 13 +++++++++-- os_windows_test.go | 2 +- 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 32fd1c9..6de3336 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,14 +1,20 @@ # AGENTS.md ## Purpose -This file defines the default working agreement for AI coding agents and contributors in this repository. + +This file defines the default working agreement for AI coding agents +and contributors in this repository. ## Documentation Language -Project language is English. All documentation, issues, pull requests, commit messages, and code comments should be written in English. + +Project language is English. All documentation, issues, pull requests, +commit messages, and code comments should be written in English. All newly created documentation in this project must be written in English. -All project documentation files must be stored under `docs/`, except `README.md` and `AGENTS.md`. +All project documentation files must be stored under `docs/`, +except `README.md` and `AGENTS.md`. ## Working Principles + 1. Keep changes minimal and focused on the requested task. 2. Preserve existing public APIs unless a breaking change is explicitly requested. 3. Prefer clear, maintainable code over clever shortcuts. @@ -16,22 +22,29 @@ All project documentation files must be stored under `docs/`, except `README.md` 5. Never add secrets, credentials, or tokens to the repository. ## Testing Expectations -1. Add or update tests for behavior changes. + +1. Add or update tests for behaviour changes. 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. +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`. +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`. ## Git Bash Execution Defaults + 1. Repository maintenance scripts are executed with Git Bash shell on Windows. 2. Default repository root is `~/git`. 3. The repository root can be overridden via `GO_GIT_ROOT`. @@ -40,11 +53,15 @@ All project documentation files must be stored under `docs/`, except `README.md` ## Definition of Done (DoD) -### Purpose -The Definition of Done defines the minimum quality bar for every completed change in this repository. +### DoD Purpose + +The Definition of Done defines the minimum quality bar +for every completed change in this repository. ### Mandatory Criteria + 1. Tests + - Every code change is covered by tests where applicable. - New functionality includes new tests. - Bug fixes include at least one regression test. @@ -52,31 +69,40 @@ The Definition of Done defines the minimum quality bar for every completed chang - Automated test coverage is at least 80%. 1. Functional documentation + - Implemented functionality is documented. - Public API-relevant changes are reflected in README and/or docs. 1. Documentation standards + - Documentation is written in English. - Documentation files are placed under `docs/`. - Exceptions: `README.md` and `AGENTS.md` remain at repository root. ### Technical Completion Criteria + 1. Build and test status + - The project builds successfully. - Relevant test commands run successfully. 1. No unresolved critical issues + - No new blocking errors are introduced. -- Known non-blocking warnings are acceptable only if unrelated to the change or documented. +- Known non-blocking warnings are acceptable only if unrelated + to the change or documented. 1. SonarQube status + - No SonarQube errors are present. 1. Documentation structure + - Links to moved or newly added docs are valid. - Documentation structure remains consistent with project rules. ### Review Checklist (Quick) + - [ ] Change is implemented and meets acceptance criteria. - [ ] Tests were added/updated and pass. - [ ] Go tests use `github.com/smartystreets/goconvey`. diff --git a/README.md b/README.md index 7077afc..1756147 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -
- # Go utility library +![yoorie.de logo](https://www.yoorie.de/img/favicon_32.png) + [![Build Status](https://drone.yoorie.de/api/badges/go-lib/util/status.svg)](https://drone.yoorie.de/go-lib/util) ## Project Description @@ -41,22 +41,20 @@ go get scm.yoorie.de/go-lib/util package main import ( - "fmt" + "fmt" - "scm.yoorie.de/go-lib/util" + "scm.yoorie.de/go-lib/util" ) func main() { - if util.FileExists("config.yaml") { - fmt.Println("config file found") - } + if util.FileExists("config.yaml") { + fmt.Println("config file found") + } - fmt.Println(util.JoiningSlash("/api", "v1", "users")) - fmt.Println(util.GetGlobalConfigurationFile("myapp", "config.yaml")) + fmt.Println(util.JoiningSlash("/api", "v1", "users")) + fmt.Println(util.GetGlobalConfigurationFile("myapp", "config.yaml")) } ``` --- Copyright © 2023 yoorie.de - - diff --git a/docs/DEFINITION_OF_DONE.md b/docs/DEFINITION_OF_DONE.md index a47d72f..1bdb9ee 100644 --- a/docs/DEFINITION_OF_DONE.md +++ b/docs/DEFINITION_OF_DONE.md @@ -2,11 +2,13 @@ ## Purpose -This Definition of Done defines the minimum quality bar for every completed change in this repository. +This Definition of Done defines the minimum quality bar +for every completed change in this repository. ## Mandatory Criteria 1. Tests + - Every code change is covered by tests where applicable. - New functionality includes new tests. - Bug fixes include at least one regression test. @@ -14,10 +16,12 @@ This Definition of Done defines the minimum quality bar for every completed chan - Automated test coverage is at least 80%. 1. Functional documentation + - Implemented functionality is documented. - Public API-relevant changes are reflected in README and/or docs. 1. Documentation standards + - Documentation is written in English. - Documentation files are placed under `docs/`. - Exceptions: `README.md` and `AGENTS.md` remain at repository root. @@ -25,17 +29,22 @@ This Definition of Done defines the minimum quality bar for every completed chan ## Technical Completion Criteria 1. Build and test status + - The project builds successfully. - Relevant test commands run successfully. 1. No unresolved critical issues + - No new blocking errors are introduced. -- Known non-blocking warnings are acceptable only if unrelated to the change or documented. +- Known non-blocking warnings are acceptable only if unrelated + to the change or documented. 1. SonarQube status + - No SonarQube errors are present. 1. Documentation links and structure + - Links to moved or newly added docs are valid. - Documentation structure remains consistent with project rules. diff --git a/os_windows_test.go b/os_windows_test.go index 6415230..34085ee 100644 --- a/os_windows_test.go +++ b/os_windows_test.go @@ -11,7 +11,7 @@ import ( "golang.org/x/sys/windows" ) -func TestIsSuperUser_SidAllocationError(t *testing.T) { +func TestIsSuperUserSidAllocationError(t *testing.T) { Convey("IsSuperUser should return false when SID allocation fails", t, func() { origAllocate := allocateAdminGroupSid origFatalf := fatalf From d01242ae7c101e062dd9d5506cba485ea777213b Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 15:14:33 +0200 Subject: [PATCH 10/17] docs(dod): enforce markdownlint rule for markdown files --- .githooks/README.md | 5 +++++ .githooks/pre-commit | 15 +++++++++++++++ AGENTS.md | 16 +++++++++------- docs/DEFINITION_OF_DONE.md | 6 ++++-- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/.githooks/README.md b/.githooks/README.md index 3d2190a..ad95056 100644 --- a/.githooks/README.md +++ b/.githooks/README.md @@ -14,3 +14,8 @@ The pre-commit hook validates for staged `.sh` files: - executable bit in Git index (`100755`) - LF line endings (no CRLF) + +The pre-commit hook also validates staged `.md` files with `markdownlint`: + +- no `markdownlint` errors or problems +- requires `markdownlint` CLI in PATH (for example via `npm install --global markdownlint-cli`) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index a8652d4..df1a085 100644 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -5,6 +5,7 @@ failed=0 cr=$(printf '\r') staged_shell_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.sh$' || true) +staged_markdown_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.md$' || true) for file in $staged_shell_files; do mode=$(git ls-files --stage -- "$file" | awk '{print $1}') @@ -19,6 +20,20 @@ for file in $staged_shell_files; do fi done +if [ -n "$staged_markdown_files" ]; then + if ! command -v markdownlint >/dev/null 2>&1; then + echo "ERROR: markdownlint is required to validate staged Markdown files (.md)." >&2 + echo "Install with npm: npm install --global markdownlint-cli" >&2 + failed=1 + else + # Validate the staged markdown files currently present in the working tree. + # This keeps the hook simple and fast for standard project usage. + if ! markdownlint $staged_markdown_files; then + failed=1 + fi + fi +fi + if [ "$failed" -ne 0 ]; then echo "Pre-commit check failed." >&2 exit 1 diff --git a/AGENTS.md b/AGENTS.md index 6de3336..b98e294 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,12 +23,12 @@ except `README.md` and `AGENTS.md`. ## Testing Expectations -1. Add or update tests for behaviour changes. +1. Add or update tests for behavior changes. 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. + as the standard test library. ## Build Artifacts and Reports @@ -39,9 +39,9 @@ except `README.md` and `AGENTS.md`. 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`. + 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`. ## Git Bash Execution Defaults @@ -78,6 +78,7 @@ for every completed change in this repository. - Documentation is written in English. - Documentation files are placed under `docs/`. - Exceptions: `README.md` and `AGENTS.md` remain at repository root. +- Markdown files have no `markdownlint` errors or problems. ### Technical Completion Criteria @@ -89,8 +90,8 @@ for every completed change in this repository. 1. No unresolved critical issues - No new blocking errors are introduced. -- Known non-blocking warnings are acceptable only if unrelated - to the change or documented. +- Known non-blocking warnings are acceptable + only if unrelated to the change or documented. 1. SonarQube status @@ -110,5 +111,6 @@ for every completed change in this repository. - [ ] Functionality is documented. - [ ] Documentation is in English. - [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`). +- [ ] Markdown files have no `markdownlint` errors or problems. - [ ] No SonarQube errors are present. - [ ] No critical regressions found. diff --git a/docs/DEFINITION_OF_DONE.md b/docs/DEFINITION_OF_DONE.md index 1bdb9ee..bc0471b 100644 --- a/docs/DEFINITION_OF_DONE.md +++ b/docs/DEFINITION_OF_DONE.md @@ -25,6 +25,7 @@ for every completed change in this repository. - Documentation is written in English. - Documentation files are placed under `docs/`. - Exceptions: `README.md` and `AGENTS.md` remain at repository root. +- Markdown files have no `markdownlint` errors or problems. ## Technical Completion Criteria @@ -36,8 +37,8 @@ for every completed change in this repository. 1. No unresolved critical issues - No new blocking errors are introduced. -- Known non-blocking warnings are acceptable only if unrelated - to the change or documented. +- Known non-blocking warnings are acceptable + only if unrelated to the change or documented. 1. SonarQube status @@ -57,5 +58,6 @@ for every completed change in this repository. - [ ] Functionality is documented. - [ ] Documentation is in English. - [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`). +- [ ] Markdown files have no `markdownlint` errors or problems. - [ ] No SonarQube errors are present. - [ ] No critical regressions found. From 6ca2c4d25ffb66b3fda37024a3c7097d0e0b8ba2 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 15:37:35 +0200 Subject: [PATCH 11/17] ci: improve drone test checks and coverage reporting --- .drone.yml | 7 +++++-- coverage | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) delete mode 100644 coverage diff --git a/.drone.yml b/.drone.yml index 39db88c..172d154 100644 --- a/.drone.yml +++ b/.drone.yml @@ -7,6 +7,9 @@ steps: image: golang:1.25.8 commands: - go get ./... - - go test ./... + - go vet ./... + - mkdir -p .build + - go test -v -coverprofile=.build/coverage.out ./... + - go tool cover -func=.build/coverage.out | tee .build/coverage.txt | awk '/^total:/ { gsub("%", "", $3); if ($3 + 0 < 80) { printf("Coverage %.1f%% is below 80%%\n", $3); exit 1 } }' - go install golang.org/x/vuln/cmd/govulncheck@latest - - govulncheck -v -json ./... > vulncheck.json \ No newline at end of file + - govulncheck -json ./... > vulncheck.json \ No newline at end of file diff --git a/coverage b/coverage deleted file mode 100644 index 5f02b11..0000000 --- a/coverage +++ /dev/null @@ -1 +0,0 @@ -mode: set From 0e150f0ba9ce50ef8cadc811d964127b5cab6111 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 15:45:18 +0200 Subject: [PATCH 12/17] ci: fix coverage flag parsing for Linux shell compatibility --- .drone.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index 172d154..589eb3b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -9,7 +9,7 @@ steps: - go get ./... - go vet ./... - mkdir -p .build - - go test -v -coverprofile=.build/coverage.out ./... - - go tool cover -func=.build/coverage.out | tee .build/coverage.txt | awk '/^total:/ { gsub("%", "", $3); if ($3 + 0 < 80) { printf("Coverage %.1f%% is below 80%%\n", $3); exit 1 } }' + - go test -v -coverprofile .build/coverage.out ./... + - go tool cover -func .build/coverage.out | tee .build/coverage.txt | awk '/^total:/ { gsub("%", "", $3); if ($3 + 0 < 80) { printf("Coverage %.1f%% is below 80%%\n", $3); exit 1 } }' - go install golang.org/x/vuln/cmd/govulncheck@latest - govulncheck -json ./... > vulncheck.json \ No newline at end of file From 2e267ac6dd71dafd8f0e0410c2d878ffebc45091 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 15:48:10 +0200 Subject: [PATCH 13/17] ci: move coverage validation to separate script for better readability --- .drone.yml | 3 ++- scripts/check-coverage.sh | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100755 scripts/check-coverage.sh diff --git a/.drone.yml b/.drone.yml index 589eb3b..40faf19 100644 --- a/.drone.yml +++ b/.drone.yml @@ -10,6 +10,7 @@ steps: - go vet ./... - mkdir -p .build - go test -v -coverprofile .build/coverage.out ./... - - go tool cover -func .build/coverage.out | tee .build/coverage.txt | awk '/^total:/ { gsub("%", "", $3); if ($3 + 0 < 80) { printf("Coverage %.1f%% is below 80%%\n", $3); exit 1 } }' + - go tool cover -func .build/coverage.out | tee .build/coverage.txt + - bash scripts/check-coverage.sh .build/coverage.out 80 - go install golang.org/x/vuln/cmd/govulncheck@latest - govulncheck -json ./... > vulncheck.json \ No newline at end of file diff --git a/scripts/check-coverage.sh b/scripts/check-coverage.sh new file mode 100755 index 0000000..be38fc7 --- /dev/null +++ b/scripts/check-coverage.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Check test coverage against minimum threshold + +set -e + +COVERAGE_FILE="${1:-.build/coverage.out}" +MIN_COVERAGE="${2:-80}" + +if [ ! -f "$COVERAGE_FILE" ]; then + echo "Error: Coverage file not found: $COVERAGE_FILE" + exit 1 +fi + +# Extract coverage percentage using awk +COVERAGE=$(go tool cover -func "$COVERAGE_FILE" | awk '/^total:/ { match($0, /[0-9.]+%/); print substr($0, RSTART, RLENGTH-1) }') + +echo "Total coverage: ${COVERAGE}%" + +# Compare as integers (remove decimals for simpler comparison) +COVERAGE_INT=${COVERAGE%.*} + +if [ "$COVERAGE_INT" -lt "$MIN_COVERAGE" ]; then + echo "Coverage ${COVERAGE}% is below minimum ${MIN_COVERAGE}%" + exit 1 +fi + +echo "Coverage check passed" +exit 0 From 91c2a313f20fcdee98db2f56e1f7df28e1f67b31 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 16:04:02 +0200 Subject: [PATCH 14/17] ci: add tag-based release pipeline with coverage and source archive --- .drone.yml | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 40faf19..82ef31d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2,6 +2,11 @@ kind: pipeline type: docker name: go-lib/util +trigger: + event: tag + ref: + - refs/tags/v* + steps: - name: test image: golang:1.25.8 @@ -13,4 +18,24 @@ steps: - go tool cover -func .build/coverage.out | tee .build/coverage.txt - bash scripts/check-coverage.sh .build/coverage.out 80 - go install golang.org/x/vuln/cmd/govulncheck@latest - - govulncheck -json ./... > vulncheck.json \ No newline at end of file + - govulncheck -json ./... > vulncheck.json + +- name: package + image: golang:1.25.8 + commands: + - tar czf .build/sources.tar.gz --exclude=.build --exclude=.git --exclude=.drone.yml . + when: + status: success + +- name: release + image: plugins/gitea-release + settings: + api_key: + from_secret: gitea_token + files: + - .build/coverage.txt + - .build/sources.tar.gz + title: ${DRONE_TAG} + note: "Release ${DRONE_TAG}\n\nCoverage report: coverage.txt" + when: + status: success \ No newline at end of file From 0ebb212b659bc1d572323bc29d3f005daffb5604 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 16:27:41 +0200 Subject: [PATCH 15/17] docs: add release process documentation --- docs/RELEASING.md | 180 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/RELEASING.md diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..b7571dd --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,180 @@ +# Releasing go-lib/util + +This document describes the process for creating a release of the +`go-lib/util` library. + +## Overview + +Releases in this project are managed via **Git tags**. When you push a +tag matching the pattern `v*` (e.g., `v0.5.0`), the Drone CI/CD +pipeline automatically: + +1. Runs all quality checks (tests, coverage, vet, vulnerability scan) +2. Creates a source archive (`sources.tar.gz`) +3. Publishes a release to Gitea with both artifacts + +## Release Types + +This library follows **semantic versioning**: `MAJOR.MINOR.PATCH` (e.g., `v1.2.3`) + +- `v1.0.0` - Major release (breaking API changes) +- `v1.1.0` - Minor release (new features, backward compatible) +- `v1.0.1` - Patch release (bug fixes only) + +## Prerequisites + +Before creating a release, ensure: + +1. **All changes committed**: `git status` shows clean working tree +2. **Tests passing**: Run `go test ./...` locally +3. **Coverage OK**: Coverage must be ≥ 80% +4. **Dependencies updated**: Run `go mod tidy` +5. **CHANGELOG.md updated** (optional but recommended) + +## Creating a Release + +### Step 1: Prepare Release Commit (Optional) + +Update version references if needed (README, docs, etc.): + +```bash +# Edit any version references +vim README.md +git add README.md +git commit -m "docs: prepare v0.5.0 release" +``` + +### Step 2: Create the Git Tag + +```bash +# Create annotated tag with release notes +git tag -a v0.5.0 -m "Release v0.5.0: Description of changes" + +# Or simple tag (not recommended) +# git tag v0.5.0 +``` + +### Step 3: Push the Tag + +```bash +# Push tag to remote +git push origin v0.5.0 + +# Or push all tags at once +# git push --tags +``` + +### Step 4: Monitor the Pipeline + +1. Navigate to your Drone instance (usually `https://drone.example.com`) +2. Watch the pipeline run through: + + - ✓ Test & coverage checks + - ✓ Code quality (vet, vulnerability scan) + - ✓ Create source archive + - ✓ Publish to Gitea release + +### Step 5: Verify Release in Gitea + +1. Go to your repository on Gitea +2. Click "Releases" section +3. Verify the new release includes: + - Release title: `v0.5.0` + - Attached artifacts: + - `coverage.txt` - Test coverage report + - `sources.tar.gz` - Full source code snapshot + +## Using the Released Version + +End users can install your library via: + +```bash +# Latest version +go get scm.yoorie.de/go-lib/util + +# Specific version +go get scm.yoorie.de/go-lib/util@v0.5.0 + +# Latest patch of a minor version +go get scm.yoorie.de/go-lib/util@v0.5 +``` + +## Tag Naming Convention + +- **Release tags**: `v0.5.0` (pushed to trigger Drone pipeline) +- **Pre-release tags** (optional): `v0.5.0-rc1`, `v0.5.0-beta1` +- **Internal tags** (if any): Not recommended; use branches instead + +Only tags matching `v*` trigger the release pipeline. + +## Rollback / Deleting a Release + +If a release has issues: + +```bash +# Delete local tag +git tag -d v0.5.0 + +# Delete remote tag +git push origin :refs/tags/v0.5.0 + +# Then push a fixed release with the same or new tag +git tag -a v0.5.0-fixed -m "Fixed release" +git push origin v0.5.0-fixed +``` + +## Troubleshooting + +### Pipeline Failed + +Check Drone logs: + +1. Go to Drone UI +2. Click on failed pipeline +3. Expand step details +4. Review error messages + +Common issues: + +- **Coverage below 80%**: Ensure tests cover new code +- **Tests failing**: Run locally: `go test -v ./...` +- **Vet errors**: Run: `go vet ./...` + +### Release Not Appearing in Gitea + +1. Verify `gitea_token` secret is set in Drone +2. Check Drone pipeline output for release step +3. Ensure tag matches pattern `v*` + +### Tag Already Exists + +If you pushed a tag and need to update it: + +```bash +# Force delete and recreate (dangerous - use with caution) +git tag -d v0.5.0 +git push origin :refs/tags/v0.5.0 +git tag -a v0.5.0 -m "Updated release notes" +git push origin v0.5.0 +``` + +## Release Checklist + +- [ ] All code changes reviewed and merged +- [ ] Tests pass locally: `go test -v ./...` +- [ ] Coverage ≥ 80%: Test and check coverage +- [ ] Code quality OK: `go vet ./...` +- [ ] No vulnerabilities: `govulncheck ./...` +- [ ] Dependencies tidy: `go mod tidy` +- [ ] CHANGELOG updated (if maintained) +- [ ] Version references updated (if any) +- [ ] Git tag created: `git tag -a vX.Y.Z -m "message"` +- [ ] Tag pushed: `git push origin vX.Y.Z` +- [ ] Release visible in Gitea +- [ ] Coverage artifact downloaded and verified + +## Further Reading + +- [Semantic Versioning](https://semver.org/) +- [Go Modules](https://golang.org/doc/modules) +- [Drone CI Documentation](https://docs.drone.io/) From 5e8660ebd19ebbf05425b748eb2d05346d667bc3 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 16:30:12 +0200 Subject: [PATCH 16/17] fix: correct drone.yml yaml schema - use arrays for event and status --- .drone.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index 82ef31d..2eb2c19 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3,7 +3,8 @@ type: docker name: go-lib/util trigger: - event: tag + event: + - tag ref: - refs/tags/v* @@ -25,7 +26,8 @@ steps: commands: - tar czf .build/sources.tar.gz --exclude=.build --exclude=.git --exclude=.drone.yml . when: - status: success + status: + - success - name: release image: plugins/gitea-release @@ -38,4 +40,5 @@ steps: title: ${DRONE_TAG} note: "Release ${DRONE_TAG}\n\nCoverage report: coverage.txt" when: - status: success \ No newline at end of file + status: + - success \ No newline at end of file From 109d62efa3ab2f3107cb3d3e782ddb623917a65b Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 29 Mar 2026 16:34:14 +0200 Subject: [PATCH 17/17] ci: run tests on all commits, release only on tags --- .drone.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.drone.yml b/.drone.yml index 2eb2c19..47c7f19 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,6 +4,7 @@ name: go-lib/util trigger: event: + - push - tag ref: - refs/tags/v* @@ -26,6 +27,8 @@ steps: commands: - tar czf .build/sources.tar.gz --exclude=.build --exclude=.git --exclude=.drone.yml . when: + event: + - tag status: - success @@ -40,5 +43,7 @@ steps: title: ${DRONE_TAG} note: "Release ${DRONE_TAG}\n\nCoverage report: coverage.txt" when: + event: + - tag status: - success \ No newline at end of file