Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f4302fb
feat: add complete template infrastructure with audit fixes
NWarila Apr 7, 2026
ef83ed0
fix: resolve CI failures from initial run
NWarila Apr 7, 2026
528720b
fix: resolve remaining CI failures (ruff, mypy, setuptools)
NWarila Apr 7, 2026
a6aaf83
fix: resolve ruff format, mypy type-arg, and test import failures
NWarila Apr 7, 2026
e5c7e5b
fix: resolve test failures (argv isolation, missing dev deps)
NWarila Apr 7, 2026
9261a6c
fix: resolve remaining test failures (coverage, twine)
NWarila Apr 7, 2026
4310deb
fix: skip entry-point smoke test when command not on PATH
NWarila Apr 7, 2026
fd2e2f4
fix: use ASCII dashes in qa.py summary table for Windows compat
NWarila Apr 7, 2026
9331c21
chore: harden python template audit baseline
NWarila Apr 7, 2026
b490f6d
fix: make setup.sh dev extra detection portable
NWarila Apr 7, 2026
d36c834
chore: standardize gitignore and gitattributes to org baseline
NWarila Apr 7, 2026
d789059
docs: document gitignore/gitattributes org standard in PLAN.md and RE…
NWarila Apr 7, 2026
9cfb89a
security: add top-level least-privilege permissions to template-ci.yml
NWarila Apr 7, 2026
301f4cb
security: default reusable workflow consumers to contents read
NWarila Apr 7, 2026
865325e
refactor: move composite action from actions/ to .github/actions/
NWarila Apr 8, 2026
2cd0bf6
chore: refresh action version pins (setup-python v6.2.0, setup-uv v8.…
NWarila Apr 8, 2026
2e8ed94
feat: add self-dogfooding via released scripts
NWarila Apr 8, 2026
355647e
fix: resolve CI failures in dogfood setup
NWarila Apr 8, 2026
3cee83b
docs: align README with current template architecture
NWarila Apr 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Normalize tracked text files to LF for stable diffs across platforms.
* text=auto eol=lf

# Keep markdown readable in diffs and consistently normalized.
*.md text eol=lf diff=markdown

# Keep Python and shell automation normalized for reliable review and execution.
*.py text eol=lf
*.sh text eol=lf
*.ps1 text eol=lf

# Keep structured config normalized so tooling behaves consistently.
*.yml text eol=lf
*.yaml text eol=lf
*.toml text eol=lf
*.json text eol=lf
*.jsonc text eol=lf

# Keep ignore and attribute files themselves normalized for portability.
.gitignore text eol=lf
.gitattributes text eol=lf

# Add project-specific binary types below.
# *.png binary
# *.pdf binary
# *.docx binary
53 changes: 53 additions & 0 deletions .github/actions/setup-python/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Setup Python environment
description: >
Install a Python interpreter, create a venv via scripts/setup.sh or
scripts/setup.ps1, and add the venv to PATH so every subsequent step
uses the same isolated environment that local development does.

inputs:
python-version:
description: Python version to install
required: false
default: "3.12"

runs:
using: composite
steps:
- name: Install Python ${{ inputs.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ inputs.python-version }}
cache: pip
cache-dependency-path: pyproject.toml

- name: Install uv when uv.lock is present
if: ${{ hashFiles('uv.lock') != '' }}
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0

- name: Create venv and install package (Linux/macOS)
if: runner.os != 'Windows'
shell: bash
run: bash .github/scripts/setup.sh
env:
PROJECT_ROOT: ${{ github.workspace }}

- name: Create venv and install package (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: .\.github\scripts\setup.ps1
env:
PROJECT_ROOT: ${{ github.workspace }}

- name: Activate venv for subsequent steps (Linux/macOS)
if: runner.os != 'Windows'
shell: bash
run: |
echo "${GITHUB_WORKSPACE}/.venv/bin" >> "$GITHUB_PATH"
echo "VIRTUAL_ENV=${GITHUB_WORKSPACE}/.venv" >> "$GITHUB_ENV"

- name: Activate venv for subsequent steps (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
"$env:GITHUB_WORKSPACE\.venv\Scripts" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8
"VIRTUAL_ENV=$env:GITHUB_WORKSPACE\.venv" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly

- package-ecosystem: pip
directory: /
schedule:
interval: weekly
1 change: 1 addition & 0 deletions .github/scripts/.version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
none
61 changes: 61 additions & 0 deletions .github/scripts/check_lint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
# Managed by nwarila/python-template — do not edit manually.
# Source: https://github.com/nwarila/python-template

from __future__ import annotations

import argparse
import os
import subprocess
import sys
import tomllib
from pathlib import Path
from typing import Any


def _load_pyproject() -> dict[str, Any]:
path = Path("pyproject.toml")
if not path.exists():
return {}
with open(path, "rb") as f:
return tomllib.load(f)


def _tool(name: str) -> str:
exe_dir = Path(sys.executable).resolve().parent
candidates = [exe_dir / name, exe_dir / f"{name}.exe"]
for candidate in candidates:
if candidate.exists():
return str(candidate)
return name


def _run(cmd: list[str], label: str) -> int:
print(f"\n--- {label} ---")
result = subprocess.run(cmd)
if result.returncode != 0 and os.environ.get("GITHUB_ACTIONS") == "true":
print(f"::error::{label} failed with exit code {result.returncode}")
return result.returncode


def main() -> int:
parser = argparse.ArgumentParser(description="Run ruff lint and format checks.")
parser.add_argument("--fix", action="store_true", help="Auto-fix lint issues and reformat")
parser.add_argument("--paths", nargs="+", help="Override source paths to check")
args = parser.parse_args()

pyproject = _load_pyproject()
paths = args.paths or pyproject.get("tool", {}).get("ruff", {}).get("src", ["src"])

if args.fix:
rc1 = _run([_tool("ruff"), "check", "--fix", *paths], "Ruff Fix")
rc2 = _run([_tool("ruff"), "format", *paths], "Ruff Format")
else:
rc1 = _run([_tool("ruff"), "check", *paths], "Ruff Check")
rc2 = _run([_tool("ruff"), "format", "--check", *paths], "Ruff Format Check")

return 1 if (rc1 or rc2) else 0


if __name__ == "__main__":
sys.exit(main())
97 changes: 97 additions & 0 deletions .github/scripts/check_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
# Managed by nwarila/python-template — do not edit manually.
# Source: https://github.com/nwarila/python-template

from __future__ import annotations

import argparse
import glob
import os
import shutil
import subprocess
import sys
import tomllib
from pathlib import Path
from typing import Any


def _load_pyproject() -> dict[str, Any]:
path = Path("pyproject.toml")
if not path.exists():
return {}
with open(path, "rb") as f:
return tomllib.load(f)


def _tool(name: str) -> str:
exe_dir = Path(sys.executable).resolve().parent
candidates = [exe_dir / name, exe_dir / f"{name}.exe"]
for candidate in candidates:
if candidate.exists():
return str(candidate)
return name


def _run(cmd: list[str], label: str) -> int:
print(f"\n--- {label} ---")
result = subprocess.run(cmd)
if result.returncode != 0 and os.environ.get("GITHUB_ACTIONS") == "true":
print(f"::error::{label} failed with exit code {result.returncode}")
return result.returncode


def _cleanup() -> None:
shutil.rmtree("dist", ignore_errors=True)
for egg_dir in glob.glob("*.egg-info"):
shutil.rmtree(egg_dir, ignore_errors=True)
for egg_dir in glob.glob("src/*.egg-info"):
shutil.rmtree(egg_dir, ignore_errors=True)


def main() -> int:
argparse.ArgumentParser(description="Validate package build, metadata, and entry points.").parse_args()

pyproject = _load_pyproject()

if "build-system" not in pyproject:
print("No [build-system] found, skipping package check")
return 0

entry_points = pyproject.get("project", {}).get("scripts", {})

try:
rc = _run([_tool("validate-pyproject"), "pyproject.toml"], "Validate pyproject.toml")
if rc != 0:
return rc

rc = _run([sys.executable, "-m", "build"], "Build sdist+wheel")
if rc != 0:
return rc

dist_files = glob.glob("dist/*")
if not dist_files:
is_ci = os.environ.get("GITHUB_ACTIONS") == "true"
print("::error::No dist files produced" if is_ci else "ERROR: No dist files produced")
return 1

rc = _run([_tool("twine"), "check", "--strict", *dist_files], "Twine Check")
if rc != 0:
return rc

for name in entry_points:
tool_path = shutil.which(name) or _tool(name)
if shutil.which(name) is None and tool_path == name:
print(f" Entry point '{name}' not found on PATH, skipping smoke test")
continue
rc = _run([tool_path, "--help"], f"Entry point: {name} --help")
if rc != 0:
return rc

finally:
_cleanup()

return 0


if __name__ == "__main__":
sys.exit(main())
38 changes: 38 additions & 0 deletions .github/scripts/check_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
# Managed by nwarila/python-template — do not edit manually.
# Source: https://github.com/nwarila/python-template

from __future__ import annotations

import argparse
import os
import subprocess
import sys
from pathlib import Path


def _tool(name: str) -> str:
exe_dir = Path(sys.executable).resolve().parent
candidates = [exe_dir / name, exe_dir / f"{name}.exe"]
for candidate in candidates:
if candidate.exists():
return str(candidate)
return name


def _run(cmd: list[str], label: str) -> int:
print(f"\n--- {label} ---")
result = subprocess.run(cmd)
if result.returncode != 0 and os.environ.get("GITHUB_ACTIONS") == "true":
print(f"::error::{label} failed with exit code {result.returncode}")
return result.returncode


def main() -> int:
argparse.ArgumentParser(description="Run pip-audit for dependency vulnerability scanning.").parse_args()

return _run([_tool("pip-audit"), "--skip-editable"], "Pip-Audit")


if __name__ == "__main__":
sys.exit(main())
44 changes: 44 additions & 0 deletions .github/scripts/check_spelling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
# Managed by nwarila/python-template — do not edit manually.
# Source: https://github.com/nwarila/python-template

from __future__ import annotations

import argparse
import os
import subprocess
import sys
from pathlib import Path


def _tool(name: str) -> str:
exe_dir = Path(sys.executable).resolve().parent
candidates = [exe_dir / name, exe_dir / f"{name}.exe"]
for candidate in candidates:
if candidate.exists():
return str(candidate)
return name


def _run(cmd: list[str], label: str) -> int:
print(f"\n--- {label} ---")
result = subprocess.run(cmd)
if result.returncode != 0 and os.environ.get("GITHUB_ACTIONS") == "true":
print(f"::error::{label} failed with exit code {result.returncode}")
return result.returncode


def main() -> int:
parser = argparse.ArgumentParser(description="Run codespell for typo detection.")
parser.add_argument("--fix", action="store_true", help="Auto-fix spelling mistakes")
args = parser.parse_args()

cmd = [_tool("codespell")]
if args.fix:
cmd.append("--write-changes")

return _run(cmd, "Codespell")


if __name__ == "__main__":
sys.exit(main())
Loading
Loading