Compare commits

..

No commits in common. "b349586755b9630547b9f8dc325089b7dba4efde" and "05a61bba86e450d9dfc46972d6a542e3c885d640" have entirely different histories.

18 changed files with 121 additions and 985 deletions

View File

@ -2,48 +2,19 @@ kind: pipeline
type: docker
name: go-lib/util
steps:
- name: build
image: golang
commands:
- mkdir -p .build
- go install gotest.tools/gotestsum@v1.9.0
- go get ./...
- gotestsum --format testname --junitfile .build/unittests.xml -- -coverprofile=.build/coverage.txt ./...
- go install golang.org/x/vuln/cmd/govulncheck@latest
- govulncheck -v ./...
trigger:
event:
- push
- tag
ref:
- refs/tags/v*
steps:
- name: test
image: golang:1.25.8
commands:
- 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
- bash scripts/check-coverage.sh .build/coverage.out 80
- go install golang.org/x/vuln/cmd/govulncheck@latest
- 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:
event:
- tag
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:
event:
- tag
status:
- success
- push
- cron
- custom

View File

@ -1,16 +0,0 @@
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

17
.gitattributes vendored
View File

@ -1,17 +0,0 @@
* 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
*.bat text eol=crlf
*.cmd text eol=crlf

View File

@ -1,21 +0,0 @@
# 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)
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`)

View File

@ -1,40 +0,0 @@
#!/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)
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}')
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 [ -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
fi

14
.gitignore vendored
View File

@ -1,14 +0,0 @@
.build/
bin/
dist/
tmp/
coverage/
coverage.out
*.coverprofile
*.test
*.out
.DS_Store
Thumbs.db
.vscode/
.idea/

116
AGENTS.md
View File

@ -1,116 +0,0 @@
# 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.
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`.
## 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)
### 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.
- For Go projects, tests use `github.com/smartystreets/goconvey`.
- 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.
- Markdown files have no `markdownlint` errors or problems.
### 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. 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`.
- [ ] 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`).
- [ ] Markdown files have no `markdownlint` errors or problems.
- [ ] No SonarQube errors are present.
- [ ] No critical regressions found.

View File

@ -1,60 +1,14 @@
# Go utility library
<div style="text-align:left"><img src="https://www.yoorie.de/img/favicon_32.png"/></div>
![yoorie.de logo](https://www.yoorie.de/img/favicon_32.png)
# Go utility library
[![Build Status](https://drone.yoorie.de/api/badges/go-lib/util/status.svg)](https://drone.yoorie.de/go-lib/util)
## Project Description
## Documentation
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/<appname>`
- Windows: `%APPDATA%\\<appname>`
- `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"))
}
```
Is missed so far and will be created soon.
---
Copyright &copy; 2023 yoorie.de

View File

@ -1,63 +0,0 @@
# 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.
- For Go projects, tests use `github.com/smartystreets/goconvey`.
- 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.
- Markdown files have no `markdownlint` errors or problems.
## 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. 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.
## 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`).
- [ ] Markdown files have no `markdownlint` errors or problems.
- [ ] No SonarQube errors are present.
- [ ] No critical regressions found.

View File

@ -1,180 +0,0 @@
# 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/)

2
go.mod
View File

@ -3,6 +3,6 @@ module scm.yoorie.de/go-lib/util
go 1.16
require (
github.com/smartystreets/goconvey v1.6.4
github.com/stretchr/testify v1.8.2
golang.org/x/sys v0.6.0
)

30
go.sum
View File

@ -1,15 +1,19 @@
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=
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=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
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=

View File

@ -11,23 +11,6 @@ import (
"golang.org/x/sys/windows"
)
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
)
// IsSuperUser returns true, if the current user is a super user
// A.K.A root, Administrator etc
func IsSuperUser() bool {
@ -37,21 +20,27 @@ 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 := allocateAdminGroupSid(&sid)
err := windows.AllocateAndInitializeSid(
&windows.SECURITY_NT_AUTHORITY,
2,
windows.SECURITY_BUILTIN_DOMAIN_RID,
windows.DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&sid)
if err != nil {
fatalf("SID Error: %s", err)
log.Fatalf("SID Error: %s", err)
return false
}
defer freeSid(sid)
defer windows.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 := tokenIsMember(token, sid)
member, err := token.IsMember(sid)
if err != nil {
fatalf("Token Membership Error: %s", err)
log.Fatalf("Token Membership Error: %s", err)
return false
}

View File

@ -1,35 +0,0 @@
//go:build windows
// +build windows
package util
import (
"errors"
"testing"
. "github.com/smartystreets/goconvey/convey"
"golang.org/x/sys/windows"
)
func TestIsSuperUserSidAllocationError(t *testing.T) {
Convey("IsSuperUser should return false when SID allocation fails", t, func() {
origAllocate := allocateAdminGroupSid
origFatalf := fatalf
defer func() {
allocateAdminGroupSid = origAllocate
fatalf = origFatalf
}()
allocateAdminGroupSid = func(_ **windows.SID) error {
return errors.New("forced sid allocation error")
}
fatalCalled := false
fatalf = func(_ string, _ ...interface{}) {
fatalCalled = true
}
So(IsSuperUser(), ShouldBeFalse)
So(fatalCalled, ShouldBeTrue)
})
}

View File

@ -1,28 +0,0 @@
#!/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

View File

@ -1,274 +0,0 @@
#!/usr/bin/env sh
set -eu
ROOT_PATH="${GO_GIT_ROOT:-$HOME/git}"
STANDARDS_REPO=""
STANDARDS_REPO_SET=0
EXCLUDE_NAME="project-standards"
CHECK_ONLY=0
WATCH=0
INTERVAL=60
usage() {
cat <<'EOF'
Usage:
reconcile-project-standards.sh [options]
Options:
--root <path> Root folder containing repositories
--standards-repo <path> Path to project-standards repository
--exclude <name> First-level directory name to skip (default: project-standards)
--check-only Check drift only, do not update files
--watch Continuously scan and reconcile
--interval <seconds> 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
}
while [ "$#" -gt 0 ]; do
case "$1" in
--root)
ROOT_PATH=$2
shift 2
;;
--standards-repo)
STANDARDS_REPO=$2
STANDARDS_REPO_SET=1
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 [ "$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
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
exit 1
fi
if [ ! -f "$DOD_TEMPLATE" ]; then
echo "DoD template not found: $DOD_TEMPLATE" >&2
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
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"
}
ensure_gitignore_entries_from_template() {
template_path=$1
gitignore_path=$2
missing=0
if [ ! -f "$gitignore_path" ]; then
if [ "$CHECK_ONLY" -eq 1 ]; then
printf "%s" "drift"
return 0
fi
: > "$gitignore_path"
fi
while IFS= read -r entry; do
entry=$(printf '%s' "$entry" | tr -d '\r')
[ -n "$entry" ] || continue
case "$entry" in
\#*)
continue
;;
esac
if tr -d '\r' < "$gitignore_path" | grep -Fqx "$entry"; 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"
else
printf "%s" "updated"
fi
}
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"
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")
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" ] || [ "$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" ] || [ "$gitattributes_state" = "drift" ] || [ "$editorconfig_state" = "drift" ] || [ "$precommit_state" = "drift" ] || [ "$hooks_readme_state" = "drift" ] || [ "$gitignore_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

View File

@ -34,6 +34,7 @@ func joiningSlash(elem []string) string {
}
func singleJoiningSlash(a, b string) string {
filepath.Join(a, b)
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {

View File

@ -1,69 +1,90 @@
package util
import (
"fmt"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/assert"
)
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 TestFileExist(t *testing.T) {
assert.True(t, FileExists("utils.go"))
}
func TestJoiningSlash(t *testing.T) {
Convey("JoiningSlash should combine URL-like segments safely", t, func() {
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, "")
})
func TestFileExistNot(t *testing.T) {
assert.True(t, !FileExists("Utils2.go"))
}
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 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 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 TestJoiningSlash2(t *testing.T) {
actual := JoiningSlash("http://my.tld", "bla", "blub")
assert.Equal(t, "http://my.tld/bla/blub", actual)
}
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) {
Convey("IsSuperUser should return a boolean without requiring elevated rights", t, func() {
result := IsSuperUser()
So(result, ShouldBeIn, []bool{true, false})
})
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.Skip(fmt.Sprintf("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.Skip(fmt.Sprintf("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.Skip(fmt.Sprintf("Skipping on OS %s", runtime.GOOS))
}
appFolder := GetGlobalConfigurationDirectory("myapp")
assert.Equal(t, "/etc/myapp", appFolder)
}