- 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
This commit is contained in:
parent
bc9b0ce88f
commit
1da053fb67
|
|
@ -14,6 +14,3 @@ indent_size = 4
|
|||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.ps1]
|
||||
end_of_line = crlf
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -11,3 +11,4 @@ coverage.out
|
|||
Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 <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
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue