Skip to content

Commit bbf698b

Browse files
authored
2026-04-08 Daily Audit (#9)
## Summary - refresh the dogfood .github/scripts copy to the released 1.0.4 set, including sync.py - tighten reusable workflow and template CI permissions so only the coverage publish job has contents: write - pin overlapping dev-tool dependencies in root and reference pyproject.toml to the same versions used by pre-commit hooks ## Audit notes - Open PR backlog on 2026-04-08: none - Latest verified Template CI main-branch run: 24152483904 (all jobs green; tests succeeded on ubuntu-latest, windows-latest, and macos-latest) - Latest verified Auto Release run: 24151035753 (success) - Self Update remains blocked for live validation in this session because authenticated workflow dispatch / PR automation access is unavailable here ## Local validation - .venv\\Scripts\\python.exe scripts\\qa.py - .venv\\Scripts\\python.exe .github\\scripts\\qa.py --skip lint --skip types --skip tests --skip security --skip spelling --skip package ## Remaining blockers - GitHub CLI auth in this environment is invalid, so any follow-up API-driven PR updates / manual workflow dispatches may require re-authentication outside this session
1 parent 9561c30 commit bbf698b

File tree

9 files changed

+163
-15
lines changed

9 files changed

+163
-15
lines changed

.github/scripts/.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
none
1+
v1.0.4

.github/scripts/qa.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""Local QA orchestrator. Discovers and runs all check_*.py scripts.
55
66
Usage:
7-
python scripts/qa.py [--fix] [--skip name ...]
7+
python path/to/qa.py [--fix] [--skip name ...]
88
"""
99

1010
from __future__ import annotations
@@ -18,7 +18,20 @@
1818
from pathlib import Path
1919

2020
SCRIPT_DIR = Path(__file__).resolve().parent
21-
PROJECT_ROOT = SCRIPT_DIR.parent
21+
22+
23+
def _find_project_root(start: Path | None = None) -> Path:
24+
"""Walk up from *start* (default SCRIPT_DIR) to find pyproject.toml."""
25+
d = start or SCRIPT_DIR
26+
while d != d.parent:
27+
if (d / "pyproject.toml").exists():
28+
return d
29+
d = d.parent
30+
print(f"Error: could not find pyproject.toml above {d}", file=sys.stderr)
31+
sys.exit(1)
32+
33+
34+
PROJECT_ROOT = _find_project_root()
2235

2336

2437
# ---------------------------------------------------------------------------

.github/scripts/setup.ps1

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,26 @@
33
$ErrorActionPreference = 'Stop'
44

55
$ScriptDir = Split-Path -Parent $PSCommandPath
6-
$ProjectRoot = if ($env:PROJECT_ROOT) { $env:PROJECT_ROOT } else { Split-Path -Parent $ScriptDir }
6+
7+
# Walk up from ScriptDir to find the repo root (where pyproject.toml lives).
8+
# This works whether the script is at scripts/ or .github/scripts/.
9+
if ($env:PROJECT_ROOT) {
10+
$ProjectRoot = $env:PROJECT_ROOT
11+
} else {
12+
$SearchDir = $ScriptDir
13+
$ProjectRoot = $null
14+
while ($SearchDir -and $SearchDir -ne (Split-Path -Parent $SearchDir)) {
15+
if (Test-Path (Join-Path $SearchDir 'pyproject.toml')) {
16+
$ProjectRoot = $SearchDir
17+
break
18+
}
19+
$SearchDir = Split-Path -Parent $SearchDir
20+
}
21+
if (-not $ProjectRoot) {
22+
Write-Error "Could not find pyproject.toml above $ScriptDir"
23+
exit 1
24+
}
25+
}
726

827
Write-Host "Project root: $ProjectRoot"
928
Set-Location $ProjectRoot

.github/scripts/setup.sh

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,25 @@
44
set -euo pipefail
55

66
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
7+
8+
# Walk up from SCRIPT_DIR to find the repo root (where pyproject.toml lives).
9+
# This works whether the script is at scripts/ or .github/scripts/.
10+
if [ -n "${PROJECT_ROOT:-}" ]; then
11+
: # Already set via env var
12+
else
13+
_dir="$SCRIPT_DIR"
14+
while [ "$_dir" != "/" ]; do
15+
if [ -f "$_dir/pyproject.toml" ]; then
16+
PROJECT_ROOT="$_dir"
17+
break
18+
fi
19+
_dir="$(dirname "$_dir")"
20+
done
21+
if [ -z "${PROJECT_ROOT:-}" ]; then
22+
echo "Error: could not find pyproject.toml above $SCRIPT_DIR" >&2
23+
exit 1
24+
fi
25+
fi
826

927
echo "Project root: ${PROJECT_ROOT}"
1028
cd "$PROJECT_ROOT"

.github/scripts/sync.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Sync template-managed files into a target repo using sync-manifest.json.
2+
3+
Usage:
4+
python sync.py TEMPLATE_ROOT REPO_ROOT
5+
6+
Reads sync-manifest.json from TEMPLATE_ROOT, copies files into REPO_ROOT using
7+
the mode specified for each mapping:
8+
9+
overwrite Full replacement.
10+
marker-preserve Replaces template-owned // #region sections while
11+
preserving repo-specific content.
12+
13+
Exit codes:
14+
0 Success (prints synced file count).
15+
1 Missing arguments or manifest not found.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
import json
21+
import re
22+
import sys
23+
from pathlib import Path
24+
25+
26+
def marker_preserve_copy(src: Path, dst: Path) -> None:
27+
"""Copy src to dst, preserving content outside template marker regions."""
28+
if not dst.exists():
29+
dst.parent.mkdir(parents=True, exist_ok=True)
30+
dst.write_text(src.read_text(), encoding="utf-8")
31+
return
32+
33+
src_text = src.read_text(encoding="utf-8")
34+
dst_text = dst.read_text(encoding="utf-8")
35+
36+
pattern = re.compile(
37+
r"(//\s*#region\s+Template:\s*\S+.*?\n)(.*?)(//\s*#endregion\s+Template:)",
38+
re.DOTALL,
39+
)
40+
41+
src_regions: dict[str, str] = {}
42+
for match in pattern.finditer(src_text):
43+
name_match = re.search(r"Template:\s*(\S+)", match.group(1))
44+
if name_match:
45+
src_regions[name_match.group(1)] = match.group(2)
46+
47+
def replace_region(match: re.Match[str]) -> str:
48+
name_match = re.search(r"Template:\s*(\S+)", match.group(1))
49+
if name_match and name_match.group(1) in src_regions:
50+
return match.group(1) + src_regions[name_match.group(1)] + match.group(3)
51+
return match.group(0)
52+
53+
dst.write_text(pattern.sub(replace_region, dst_text), encoding="utf-8")
54+
55+
56+
def sync(template_root: Path, repo_root: Path) -> int:
57+
"""Run the sync and return the number of files synced."""
58+
manifest_path = template_root / "sync-manifest.json"
59+
if not manifest_path.exists():
60+
print(f"Error: {manifest_path} not found", file=sys.stderr)
61+
sys.exit(1)
62+
63+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
64+
65+
changed: list[str] = []
66+
for mapping in manifest.get("files", []):
67+
src = template_root / mapping["src"]
68+
dst = repo_root / mapping["dest"]
69+
mode = mapping.get("mode", "overwrite")
70+
71+
if not src.exists():
72+
print(f" Skip (source missing): {mapping['src']}")
73+
continue
74+
75+
dst.parent.mkdir(parents=True, exist_ok=True)
76+
77+
if mode == "marker-preserve":
78+
marker_preserve_copy(src, dst)
79+
else:
80+
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
81+
82+
changed.append(mapping["dest"])
83+
print(f" Synced: {mapping['src']} -> {mapping['dest']} ({mode})")
84+
85+
print(f"\n{len(changed)} file(s) synced.")
86+
return len(changed)
87+
88+
89+
if __name__ == "__main__":
90+
if len(sys.argv) != 3:
91+
print(f"Usage: {sys.argv[0]} TEMPLATE_ROOT REPO_ROOT", file=sys.stderr)
92+
sys.exit(1)
93+
94+
sync(Path(sys.argv[1]), Path(sys.argv[2]))

.github/workflows/python-qa.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ on:
2121
default: true
2222

2323
permissions:
24-
contents: write
24+
contents: read
2525

2626
jobs:
2727
lint:
@@ -204,6 +204,8 @@ jobs:
204204
coverage-badge:
205205
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
206206
needs: [tests]
207+
permissions:
208+
contents: write
207209
runs-on: ubuntu-latest
208210
steps:
209211
- name: Download coverage badge data

.github/workflows/template-ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
branches: [main]
88

99
permissions:
10-
contents: write
10+
contents: read
1111

1212
concurrency:
1313
group: ${{ github.workflow }}-${{ github.ref }}
@@ -133,6 +133,8 @@ jobs:
133133
coverage-badge:
134134
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
135135
needs: [tests]
136+
permissions:
137+
contents: write
136138
runs-on: ubuntu-latest
137139
steps:
138140
- name: Download coverage badge data

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ license = "MIT"
1616
[project.optional-dependencies]
1717
dev = [
1818
"build",
19-
"codespell",
20-
"mypy>=1.16",
19+
"codespell==2.4.1",
20+
"mypy==1.16.0",
2121
"pip-audit",
2222
"pre-commit",
2323
"pytest",
2424
"pytest-cov",
25-
"ruff>=0.11",
25+
"ruff==0.11.12",
2626
"twine",
27-
"validate-pyproject",
27+
"validate-pyproject==0.24",
2828
]
2929

3030
[tool.ruff]

reference/pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ license = "MIT"
1414
[project.optional-dependencies]
1515
dev = [
1616
"build",
17-
"codespell",
18-
"mypy>=1.16",
17+
"codespell==2.4.1",
18+
"mypy==1.16.0",
1919
"pip-audit",
2020
"pre-commit",
2121
"pytest",
2222
"pytest-cov",
23-
"ruff>=0.11",
23+
"ruff==0.11.12",
2424
"twine",
25-
"validate-pyproject",
25+
"validate-pyproject==0.24",
2626
]
2727

2828
# [project.scripts]

0 commit comments

Comments
 (0)