diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9269504 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# EditorConfig: https://editorconfig.org +# A trusted VS Code EditorConfig extension can enforce these on save, +# converting CRLF to LF in any file opened in the editor. +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +[Makefile] +indent_style = tab + +[*.{groovy,gradle}] +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ee6db88 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Enforce LF endings for tracked text files to prevent CRLF regressions. +* text=auto eol=lf diff --git a/.github/workflows/bootstrap-checks.yml b/.github/workflows/bootstrap-checks.yml index 60f72bf..1658c11 100644 --- a/.github/workflows/bootstrap-checks.yml +++ b/.github/workflows/bootstrap-checks.yml @@ -13,5 +13,54 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install Linux setup prerequisites + run: | + sudo apt-get update + sudo apt-get install -y python3-yaml + - name: Run bootstrap checks run: bash bin/run-checks.sh + + - name: Smoke test Python setup engine + run: | + mkdir -p tmp-ci-python-workspace + cd tmp-ci-python-workspace + python3 ../bin/ide-workspace-setup.py + python3 ../bin/ide-workspace-setup.py --apply + test -f .vscode/settings.json + test -f .vscode/extensions.json + test -f cspell.config.yaml + test -f vscode-project-words.txt + + - name: Smoke test shell wrapper + run: | + mkdir -p tmp-ci-shell-workspace + cd tmp-ci-shell-workspace + bash ../bin/ide-workspace-setup.sh + + validate-windows-bootstrap: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate Windows bootstrap prerequisites + shell: pwsh + run: .\bin\bootstrap-windows-dev.ps1 -ValidateOnly + + - name: Smoke test PowerShell wrapper + shell: pwsh + run: | + New-Item -ItemType Directory -Path tmp-ci-windows-workspace | Out-Null + Push-Location tmp-ci-windows-workspace + try { + & ..\bin\ide-workspace-setup.ps1 + & ..\bin\ide-workspace-setup.ps1 -Apply + if (-not (Test-Path .vscode\settings.json)) { throw 'Missing .vscode/settings.json' } + if (-not (Test-Path .vscode\extensions.json)) { throw 'Missing .vscode/extensions.json' } + if (-not (Test-Path cspell.config.yaml)) { throw 'Missing cspell.config.yaml' } + if (-not (Test-Path vscode-project-words.txt)) { throw 'Missing vscode-project-words.txt' } + } + finally { + Pop-Location + } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b257c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/zzz-* +.vscode/ +/local_ide_settings.yml diff --git a/README.md b/README.md index 5a10f6f..15662b6 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ Shared linting, check, and IDE bootstrap assets for this and other repositories. +This repository exists to help the team implement coding practices they have +agreed on. Content is added here only after team agreement, typically through +the PR review process. + This repository is designed to be consumed as a git submodule. Consumer repositories add it as a top-level `code_checking/` directory and keep local wrapper scripts and `.github/workflows` in their own tree. @@ -10,7 +14,7 @@ keep local wrapper scripts and `.github/workflows` in their own tree. - Provide reusable, generic check scripts across repositories - Keep check behavior consistent for CLI, pre-commit, and GitHub Actions -- Provide IDE baseline guidance and profile overlays +- Provide IDE baseline guidance and a single YAML customization input - Reduce duplicate checker logic and drift between repositories ## Non-Goals @@ -25,6 +29,15 @@ keep local wrapper scripts and `.github/workflows` in their own tree. - Visibility: Public - Primary maintainer group: DAOS-DO/Developers +## Local Scratch Files + +- Repository-local scratch files should use a `zzz-` prefix. +- Scratch files are for temporary planning notes, draft commit messages, and + similar local work in progress. +- Scratch files must not be committed to the repository. +- Root-level `zzz-*` files are ignored through the tracked `.gitignore` so + `git status` stays focused on commit candidates. + ## Checker Behavior Policy - Checks are non-mutating by default. @@ -36,6 +49,12 @@ keep local wrapper scripts and `.github/workflows` in their own tree. Add this repository as a top-level submodule in a consumer repository: +- Commands in this README that reference `code_checking/` assume the submodule + directory is named `code_checking`. +- If your submodule uses a different directory name, replace that path prefix + in commands. +- Run submodule-integration commands from the consumer repository root. + ```bash git submodule add https://github.com/daos-do/code-checking code_checking git submodule update --init --recursive @@ -57,17 +76,30 @@ Consumer repositories keep: Shared scripts in this repository are invoked by those local wrappers and workflows. -## Initial Consumer Targets +Maintenance planning notes are documented in +[docs/maintenance.md](docs/maintenance.md). + +## IDE Customization + +- Run commands from the base directory of the consumer repository clone. +- Use your chosen submodule directory name as the path prefix for scripts and + reference files in this repository. +- If you want local overrides, keep an optional user-maintained file at + `./local_ide_settings.yml` in the directory where setup is run. +- `./local_ide_settings.yml` is intentionally untracked and belongs in + `.gitignore`; it should not be committed. +- Run the bootstrap script for your platform first, then run + `ide-workspace-setup` in dry-run mode before `--apply`. +- If you are developing this repository directly rather than using it as a + submodule, drop the `code_checking/` path prefix from commands. -- `ansible-lab` -- `system-pipeline-lib` +Detailed usage is in [docs/usage.md](docs/usage.md). -## Planned Content Areas +Recommended VS Code extensions and platform-specific tool requirements are +documented in [docs/vscode-extensions.md](docs/vscode-extensions.md). -- Shell and PowerShell check runners -- Check script library (ansible-lint, yamllint, shellcheck, markdownlint, - groovylint, codespell) -- IDE setup baselines and profile overlays for VS Code +Configuration model details are in +[docs/ide-customization.md](docs/ide-customization.md). ## Cross-Repo Reuse diff --git a/bin/bootstrap-python.ps1 b/bin/bootstrap-python.ps1 new file mode 100644 index 0000000..0232ffb --- /dev/null +++ b/bin/bootstrap-python.ps1 @@ -0,0 +1,93 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Test-PythonCommand { + param( + [Parameter(Mandatory = $true)][string]$Command, + [switch]$UsePyLauncher + ) + + $cmd = Get-Command $Command -ErrorAction SilentlyContinue + if (-not $cmd) { + return $null + } + + try { + if ($UsePyLauncher) { + $versionOut = & $Command '-3' '--version' 2>&1 + } + else { + $versionOut = & $Command '--version' 2>&1 + } + if ($LASTEXITCODE -ne 0) { + return $null + } + return @{ + Command = $Command + Path = $cmd.Source + Version = ($versionOut | Out-String).Trim() + } + } + catch { + return $null + } +} + +Write-Host '[bootstrap-python] checking for Python 3 runtime' + +$detected = Test-PythonCommand -Command 'py' -UsePyLauncher +if (-not $detected) { + $detected = Test-PythonCommand -Command 'python' +} + +if ($detected) { + Write-Host "[bootstrap-python] found: $($detected.Command)" + Write-Host "[bootstrap-python] path: $($detected.Path)" + Write-Host "[bootstrap-python] version: $($detected.Version)" + Write-Host '[bootstrap-python] Python bootstrap check passed' + exit 0 +} + +Write-Host '[bootstrap-python] Python 3 not found; attempting install via winget' + +$winget = Get-Command winget -ErrorAction SilentlyContinue +if (-not $winget) { + Write-Error @' +[bootstrap-python] Python 3 not found and winget is not available. + +Install Python 3 manually, then rerun this script. +Recommended command: +winget install -e --id Python.Python.3.12 +'@ + exit 1 +} + +& winget install -e --id Python.Python.3.12 --accept-package-agreements --accept-source-agreements +if ($LASTEXITCODE -ne 0) { + Write-Error "[bootstrap-python] winget install failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE +} + +Write-Host '[bootstrap-python] rechecking Python runtime after install' +$detected = Test-PythonCommand -Command 'py' -UsePyLauncher +if (-not $detected) { + $detected = Test-PythonCommand -Command 'python' +} + +if (-not $detected) { + Write-Error @' +[bootstrap-python] Python install appears complete, but python is not yet on PATH. + +Open a new terminal and rerun bootstrap-python.ps1. + +Future enhancement: +- add a separate script for controlled Python runtime updates. +'@ + exit 1 +} + +Write-Host "[bootstrap-python] found: $($detected.Command)" +Write-Host "[bootstrap-python] path: $($detected.Path)" +Write-Host "[bootstrap-python] version: $($detected.Version)" +Write-Host '[bootstrap-python] Python bootstrap install/check passed' +exit 0 diff --git a/bin/bootstrap-python.sh b/bin/bootstrap-python.sh new file mode 100644 index 0000000..f4a275a --- /dev/null +++ b/bin/bootstrap-python.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "[bootstrap-python] checking for Python 3 runtime" + +if ! command -v python3 >/dev/null 2>&1; then + cat >&2 <<'EOF' +[bootstrap-python] Python 3 was not found. + +Install Python 3, then open a new terminal and rerun this script. + +Examples: +- Ubuntu/Debian: sudo apt install python3 +- macOS (Homebrew): brew install python +EOF + exit 1 +fi + +echo "[bootstrap-python] found: python3" +echo "[bootstrap-python] version: $(python3 --version)" +echo "[bootstrap-python] Python bootstrap check passed" diff --git a/bin/bootstrap-windows-dev.ps1 b/bin/bootstrap-windows-dev.ps1 new file mode 100644 index 0000000..d33d2ba --- /dev/null +++ b/bin/bootstrap-windows-dev.ps1 @@ -0,0 +1,197 @@ +param( + [switch]$ValidateOnly +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Get-CommandInfo { + param( + [Parameter(Mandatory = $true)][string]$Name, + [switch]$UsePyLauncher + ) + + $cmd = Get-Command $Name -ErrorAction SilentlyContinue + if (-not $cmd) { + return $null + } + + try { + if ($UsePyLauncher) { + $out = & $Name '-3' '--version' 2>&1 + } + else { + $out = & $Name '--version' 2>&1 + } + if ($LASTEXITCODE -ne 0) { + return $null + } + return @{ + Name = $Name + Path = $cmd.Source + Version = ($out | Out-String).Trim() + } + } + catch { + return $null + } +} + +function Test-WingetAvailable { + $winget = Get-Command winget -ErrorAction SilentlyContinue + if (-not $winget) { + throw @' +[bootstrap-windows-dev] winget is required but was not found. + +Install App Installer from Microsoft Store (winget provider), then rerun. +'@ + } +} + +function Install-WingetPackage { + param( + [Parameter(Mandatory = $true)][string]$PackageId, + [Parameter(Mandatory = $true)][string]$DisplayName, + [switch]$ValidateOnly + ) + + if ($ValidateOnly) { + Write-Host "[bootstrap-windows-dev] validate-only: skipping install check for $DisplayName" + return + } + + Write-Host "[bootstrap-windows-dev] ensuring $DisplayName via winget ($PackageId)" + & winget install -e --id $PackageId --accept-package-agreements --accept-source-agreements + if ($LASTEXITCODE -ne 0) { + throw "[bootstrap-windows-dev] winget install failed for $DisplayName (exit $LASTEXITCODE)" + } +} + +function Install-PyYaml { + param( + [switch]$ValidateOnly + ) + + if ($ValidateOnly) { + Write-Host '[bootstrap-windows-dev] validate-only: skipping PyYAML install' + return + } + + $pythonCmd = $null + if (Get-Command py -ErrorAction SilentlyContinue) { + $pythonCmd = @('py', '-3') + } + elseif (Get-Command python -ErrorAction SilentlyContinue) { + $pythonCmd = @('python') + } + + if (-not $pythonCmd) { + throw '[bootstrap-windows-dev] Python not available to install PyYAML' + } + + Write-Host '[bootstrap-windows-dev] checking Python package: pyyaml' + if ($pythonCmd[0] -eq 'py') { + & $pythonCmd[0] $pythonCmd[1] -c "import yaml" 2>$null + } + else { + & $pythonCmd[0] -c "import yaml" 2>$null + } + if ($LASTEXITCODE -eq 0) { + Write-Host '[bootstrap-windows-dev] pyyaml already available' + return + } + + Write-Host '[bootstrap-windows-dev] installing Python package: pyyaml' + if ($pythonCmd[0] -eq 'py') { + & $pythonCmd[0] $pythonCmd[1] -m pip install pyyaml + } + else { + & $pythonCmd[0] -m pip install pyyaml + } + if ($LASTEXITCODE -ne 0) { + throw "[bootstrap-windows-dev] failed to install pyyaml (exit $LASTEXITCODE)" + } +} + +function Set-GitGlobalConfig { + param( + [switch]$ValidateOnly + ) + + $desiredConfig = @( + @{ Key = 'core.autocrlf'; Value = 'input' }, + @{ Key = 'core.eol'; Value = 'lf' }, + @{ Key = 'core.safecrlf'; Value = 'true' }, + @{ Key = 'core.filemode'; Value = 'false' }, + @{ Key = 'core.symlinks'; Value = 'false' }, + @{ Key = 'core.longpaths'; Value = 'true' } + ) + + foreach ($entry in $desiredConfig) { + $current = (& git config --global --get $entry.Key 2>$null | Out-String).Trim() + $isMatch = (-not [string]::IsNullOrWhiteSpace($current)) -and ($current -eq $entry.Value) + + if ($isMatch) { + Write-Host "[bootstrap-windows-dev] git config $($entry.Key)=$($entry.Value)" + continue + } + + if ($ValidateOnly) { + Write-Host "[bootstrap-windows-dev] validate-only: git config $($entry.Key) expected '$($entry.Value)' (current '$current')" + continue + } + + Write-Host "[bootstrap-windows-dev] setting git config --global $($entry.Key) $($entry.Value)" + & git config --global $entry.Key $entry.Value + if ($LASTEXITCODE -ne 0) { + throw "[bootstrap-windows-dev] failed to set git config $($entry.Key) (exit $LASTEXITCODE)" + } + } +} + +Write-Host '[bootstrap-windows-dev] validating Windows developer prerequisites' + +$gitInfo = Get-CommandInfo -Name 'git' +if (-not $gitInfo) { + Test-WingetAvailable + Install-WingetPackage -PackageId 'Git.Git' -DisplayName 'Git for Windows' -ValidateOnly:$ValidateOnly + $gitInfo = Get-CommandInfo -Name 'git' +} +if (-not $gitInfo) { + throw '[bootstrap-windows-dev] git was not detected after bootstrap' +} +Write-Host "[bootstrap-windows-dev] git: $($gitInfo.Version)" +Set-GitGlobalConfig -ValidateOnly:$ValidateOnly + +$bashInfo = Get-CommandInfo -Name 'bash' +if (-not $bashInfo) { + throw '[bootstrap-windows-dev] bash was not detected. Git for Windows should provide bash. Reopen terminal and rerun.' +} +Write-Host "[bootstrap-windows-dev] bash: $($bashInfo.Version)" + +$pythonInfo = Get-CommandInfo -Name 'py' -UsePyLauncher +if (-not $pythonInfo) { + $pythonInfo = Get-CommandInfo -Name 'python' +} +if (-not $pythonInfo) { + Test-WingetAvailable + Install-WingetPackage -PackageId 'Python.Python.3.12' -DisplayName 'Python 3' -ValidateOnly:$ValidateOnly + $pythonInfo = Get-CommandInfo -Name 'py' -UsePyLauncher + if (-not $pythonInfo) { + $pythonInfo = Get-CommandInfo -Name 'python' + } +} +if (-not $pythonInfo) { + throw '[bootstrap-windows-dev] python was not detected after bootstrap' +} +Write-Host "[bootstrap-windows-dev] python: $($pythonInfo.Version)" + +Install-PyYaml -ValidateOnly:$ValidateOnly + +if (-not $ValidateOnly) { + Write-Host '[bootstrap-windows-dev] install/check complete' + Write-Host '[bootstrap-windows-dev] if tools were newly installed, reopen terminal and rerun this script once.' +} +else { + Write-Host '[bootstrap-windows-dev] validation complete' +} diff --git a/bin/ide-workspace-setup.ps1 b/bin/ide-workspace-setup.ps1 new file mode 100644 index 0000000..836400f --- /dev/null +++ b/bin/ide-workspace-setup.ps1 @@ -0,0 +1,42 @@ +param( + [switch]$Apply, + [string]$ConfigPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path -Parent $PSScriptRoot +$pythonCmd = $null + +if (Get-Command py -ErrorAction SilentlyContinue) { + $pythonCmd = @('py', '-3') +} +elseif (Get-Command python -ErrorAction SilentlyContinue) { + $pythonCmd = @('python') +} +else { + throw 'Python 3 is required for bin/ide-workspace-setup.ps1. Run .\bin\bootstrap-windows-dev.ps1 first.' +} + +$argsList = @() +if ($Apply) { + $argsList += '--apply' +} +if ($ConfigPath) { + $argsList += '--config' + $argsList += $ConfigPath +} + +$scriptPath = Join-Path $repoRoot 'bin/ide-workspace-setup.py' + +if ($pythonCmd[0] -eq 'py') { + & $pythonCmd[0] $pythonCmd[1] $scriptPath @argsList +} +else { + & $pythonCmd[0] $scriptPath @argsList +} + +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} diff --git a/bin/ide-workspace-setup.py b/bin/ide-workspace-setup.py new file mode 100644 index 0000000..da91eff --- /dev/null +++ b/bin/ide-workspace-setup.py @@ -0,0 +1,494 @@ +#!/usr/bin/env python3 +import argparse +import importlib +import json +import os +import shutil +import subprocess +import sys + + +def ensure_yaml_module(): + """Return imported PyYAML module, installing it on demand if missing.""" + try: + return importlib.import_module("yaml") + except ImportError: + if os.name != "nt": + raise SystemExit( + "Missing dependency: PyYAML. On non-Windows platforms, " + "install the distro package first (for example: " + "python3-yaml / PyYAML), then rerun setup." + ) + print( + "[ide-workspace-setup] PyYAML not found; attempting bootstrap install via pip" + ) + install = subprocess.run( + [ + sys.executable, + "-m", + "pip", + "install", + "--disable-pip-version-check", + "pyyaml", + ], + capture_output=True, + text=True, + ) + if install.returncode != 0: + details = "\n".join( + part for part in (install.stdout.strip(), install.stderr.strip()) if part + ) + raise SystemExit( + "Missing dependency: PyYAML and bootstrap install failed. " + "Install with 'python -m pip install pyyaml'.\n" + + details + ) + return importlib.import_module("yaml") + + +yaml = ensure_yaml_module() + + +def parse_yaml(path): + result = { + "vscode_settings": {}, + "vscode_extensions": [], + "python_packages": [], + "lint_profiles": [], + "pre_commit_mode": "selected", + "allow_uncertified_sources": False, + "allowed_sources": [], + } + + with open(path, "r", encoding="utf-8") as f: + doc = yaml.safe_load(f) or {} + + if not isinstance(doc, dict): + raise SystemExit("Invalid YAML: root document must be a mapping") + + ide = doc.get("ide", {}) + if ide and not isinstance(ide, dict): + raise SystemExit("Invalid YAML: 'ide' must be a mapping") + + vscode = ide.get("vscode", {}) if isinstance(ide, dict) else {} + if vscode and not isinstance(vscode, dict): + raise SystemExit("Invalid YAML: 'ide.vscode' must be a mapping") + + settings = vscode.get("settings", {}) if isinstance(vscode, dict) else {} + if settings and not isinstance(settings, dict): + raise SystemExit("Invalid YAML: 'ide.vscode.settings' must be a mapping") + result["vscode_settings"] = settings + + extensions = vscode.get("extensions", {}) if isinstance(vscode, dict) else {} + if extensions and not isinstance(extensions, dict): + raise SystemExit("Invalid YAML: 'ide.vscode.extensions' must be a mapping") + recommendations = extensions.get("recommendations", []) if isinstance(extensions, dict) else [] + if recommendations and not isinstance(recommendations, list): + raise SystemExit( + "Invalid YAML: 'ide.vscode.extensions.recommendations' must be a list" + ) + result["vscode_extensions"] = recommendations + + linting = doc.get("linting", {}) + if linting: + if not isinstance(linting, dict): + raise SystemExit("Invalid YAML: 'linting' must be a mapping") + lint_profiles = linting.get("profiles", []) + if lint_profiles and not isinstance(lint_profiles, list): + raise SystemExit("Invalid YAML: 'linting.profiles' must be a list") + result["lint_profiles"] = lint_profiles + + pre_commit = doc.get("preCommit", {}) + if pre_commit: + if not isinstance(pre_commit, dict): + raise SystemExit("Invalid YAML: 'preCommit' must be a mapping") + result["pre_commit_mode"] = pre_commit.get("mode", "selected") + + pkg_sources = doc.get("packageSources", {}) + if pkg_sources: + if not isinstance(pkg_sources, dict): + raise SystemExit("Invalid YAML: 'packageSources' must be a mapping") + allow_untrusted = pkg_sources.get("allowUncertifiedSources", False) + if not isinstance(allow_untrusted, bool): + raise SystemExit( + "Invalid YAML: 'packageSources.allowUncertifiedSources' must be boolean" + ) + allowed_sources = pkg_sources.get("allowedSources", []) + if allowed_sources and not isinstance(allowed_sources, list): + raise SystemExit( + "Invalid YAML: 'packageSources.allowedSources' must be a list" + ) + result["allow_uncertified_sources"] = allow_untrusted + result["allowed_sources"] = allowed_sources + + setup = doc.get("setup", {}) + if setup: + if not isinstance(setup, dict): + raise SystemExit("Invalid YAML: 'setup' must be a mapping") + setup_python = setup.get("python", {}) + if setup_python: + if not isinstance(setup_python, dict): + raise SystemExit("Invalid YAML: 'setup.python' must be a mapping") + packages = setup_python.get("packages", []) + if packages and not isinstance(packages, list): + raise SystemExit("Invalid YAML: 'setup.python.packages' must be a list") + for pkg in packages: + if not isinstance(pkg, str): + raise SystemExit( + "Invalid YAML: each 'setup.python.packages' entry must be a string" + ) + result["python_packages"] = packages + + return result + + +def read_json(path): + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def deep_merge(base, overlay): + """Merge overlay into base in-place. + + - Dicts are merged recursively. + - Lists are union-merged so existing entries are never removed. + - All other values in overlay replace the corresponding base value. + """ + for key, value in overlay.items(): + if key in base and isinstance(base[key], dict) and isinstance(value, dict): + deep_merge(base[key], value) + elif key in base and isinstance(base[key], list) and isinstance(value, list): + existing = base[key] + for item in value: + if item not in existing: + existing.append(item) + else: + base[key] = value + + +def normalize_rulers(rulers): + """Collapse ruler entries by column number. + + VS Code accepts each ruler as either a plain integer (column only) + or a dict with at minimum {"column": N} and an optional "color" key. + When the same column appears as both a plain integer and a dict (or + appears more than once), the dict form wins so that an explicitly + assigned color is preserved. Entries are returned sorted by column. + """ + by_column = {} + for entry in rulers: + if isinstance(entry, int): + col = entry + if col not in by_column: + by_column[col] = entry # plain int as default + elif isinstance(entry, dict) and "column" in entry: + col = entry["column"] + by_column[col] = entry # dict always beats plain int + return [by_column[col] for col in sorted(by_column)] + + +def find_code_cli(): + if os.name == "nt": + code_cmd = shutil.which("code.cmd") + if code_cmd: + return code_cmd + return shutil.which("code") + + +def ensure_python_packages(package_list, apply): + """Ensure Python packages are installed in the current interpreter env.""" + seen = set() + ordered = [] + for pkg in package_list: + name = pkg.strip() + if not name: + continue + lower = name.lower() + if lower not in seen: + seen.add(lower) + ordered.append(name) + + for pkg in ordered: + if os.name != "nt" and pkg.lower() == "pyyaml": + print( + "[ide-workspace-setup] non-Windows platform: skipping pip install " + "for pyyaml; prefer distro package management" + ) + continue + if apply: + result = subprocess.run( + [ + sys.executable, + "-m", + "pip", + "install", + "--disable-pip-version-check", + pkg, + ], + capture_output=True, + text=True, + ) + if result.returncode != 0: + details = "\n".join( + part for part in (result.stdout.strip(), result.stderr.strip()) if part + ) + print( + f"[ide-workspace-setup] WARNING: failed to install Python package {pkg}: " + + details + ) + else: + print(f"[ide-workspace-setup] ensured Python package: {pkg}") + else: + print(f"[ide-workspace-setup] would ensure Python package: {pkg}") + + +def install_extensions(ext_list, dry_run): + code_cmd = find_code_cli() + if not code_cmd: + print( + "[ide-workspace-setup] WARNING: 'code' not on PATH; " + "extension install skipped" + ) + print( + "[ide-workspace-setup] rerun from a VS Code integrated terminal " + "or add 'code' to PATH manually" + ) + return + + # Get list of already-installed extensions + installed_extensions = set() + if not dry_run: + result = subprocess.run( + [code_cmd, "--list-extensions"], + capture_output=True, + text=True, + ) + if result.returncode == 0: + # Extract extension ID only (strip version info like @1.0.0) + for line in result.stdout.splitlines(): + if line.strip(): + # Split on @ to remove version info + ext_id = line.strip().split("@")[0].lower() + installed_extensions.add(ext_id) + + for ext in ext_list: + if dry_run: + print(f"[ide-workspace-setup] would ensure extension: {ext}") + else: + # Check if already installed (case-insensitive, ignoring version) + ext_id = ext.lower().split("@")[0] + if ext_id in installed_extensions: + print(f"[ide-workspace-setup] already installed: {ext}") + else: + result = subprocess.run( + [code_cmd, "--install-extension", ext], + capture_output=True, + text=True, + ) + combined_output = "\n".join( + part for part in (result.stdout.strip(), result.stderr.strip()) if part + ) + if "already installed" in combined_output.lower(): + print(f"[ide-workspace-setup] already installed: {ext}") + elif "failed installing extensions" in combined_output.lower(): + print( + f"[ide-workspace-setup] WARNING: install failed for {ext}: " + + combined_output + ) + elif result.returncode != 0: + print( + f"[ide-workspace-setup] WARNING: install failed for {ext}: " + + combined_output + ) + else: + print(f"[ide-workspace-setup] installed: {ext}") + + +def ensure_cspell_config(target_root, repo_root, apply): + """Create cspell.config.yaml and project word list if they do not exist.""" + project_words = os.path.join(target_root, "vscode-project-words.txt") + cspell_config = os.path.join(target_root, "cspell.config.yaml") + + # Check if cspell config already exists + if os.path.exists(cspell_config): + return + + if apply: + # Create empty project-words.txt if it doesn't exist + if not os.path.exists(project_words): + with open(project_words, "w", encoding="utf-8") as f: + f.write("# Project-specific words accepted by cspell\n") + f.write("# Add one word per line\n") + print(f"[ide-workspace-setup] created: {project_words}") + + # Create cspell.config.yaml + cspell_content = """version: '0.2' +language: en +useGitignore: true +words: [] +flagWords: [] +dictionaryDefinitions: + - name: project-words + path: ./vscode-project-words.txt + addWords: true + scope: workspace + +dictionaries: + - project-words + +ignorePaths: + - .git/** + - .venv/** + - node_modules/** +""" + with open(cspell_config, "w", encoding="utf-8") as f: + f.write(cspell_content) + print(f"[ide-workspace-setup] created: {cspell_config}") + else: + print(f"[ide-workspace-setup] would create: {cspell_config}") + print(f"[ide-workspace-setup] would create: {project_words}") + + +def copy_linter_configs(target_root, repo_root, apply): + """Copy linter config files from repo_root to target_root if missing.""" + # List of common linter/formatter config files to copy + linter_files = [ + ".shellcheckrc", + ".pylintrc", + ".flake8", + ".yamllint", + "ansible.cfg", + ".editorconfig", + ".autopep8", + "pyproject.toml", + "setup.cfg", + ] + + for filename in linter_files: + src = os.path.join(repo_root, filename) + dst = os.path.join(target_root, filename) + + # Only copy if source exists and destination does not + if os.path.exists(src) and not os.path.exists(dst): + if apply: + shutil.copy2(src, dst) + print(f"[ide-workspace-setup] copied: {dst}") + else: + print(f"[ide-workspace-setup] would copy: {filename}") + + +def main(argv): + parser = argparse.ArgumentParser() + parser.add_argument("--apply", action="store_true") + parser.add_argument("--config", default="") + args = parser.parse_args(argv) + + repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) + target_root = os.getcwd() + default_yaml = os.path.join(target_root, "local_ide_settings.yml") + reference_yaml = os.path.join( + repo_root, "ide", "reference", "recommended_settings.yml" + ) + + config_path = args.config or ( + default_yaml if os.path.exists(default_yaml) else reference_yaml + ) + if not os.path.exists(config_path): + raise SystemExit(f"Config file not found: {config_path}") + + cfg = parse_yaml(config_path) + if cfg["pre_commit_mode"] not in ("selected", "none", "all"): + raise SystemExit("Invalid pre_commit_mode: expected selected|none|all") + + baseline_cfg = parse_yaml(reference_yaml) + baseline_settings = baseline_cfg["vscode_settings"] + baseline_extensions = baseline_cfg["vscode_extensions"] + + out_settings = os.path.join(target_root, ".vscode", "settings.json") + out_ext = os.path.join(target_root, ".vscode", "extensions.json") + + # Start from any existing workspace settings so nothing is lost. + # Merge order (each step can add/override the previous): + # existing .vscode/settings.json → repo baseline → local YAML + existing_settings = {} + if os.path.exists(out_settings): + try: + existing_settings = read_json(out_settings) + except Exception: + print( + f"[ide-workspace-setup] WARNING: could not read existing " + f"{out_settings}; starting from baseline only" + ) + + merged_settings = {} + deep_merge(merged_settings, existing_settings) + deep_merge(merged_settings, baseline_settings) + deep_merge(merged_settings, cfg["vscode_settings"]) + + # Rulers need column-aware deduplication; plain ints and dicts with the + # same column number must be collapsed to a single entry (dict wins). + rulers = merged_settings.get("editor", {}).get("rulers") + if isinstance(rulers, list): + merged_settings["editor"]["rulers"] = normalize_rulers(rulers) + + # Build extension list: existing + baseline + selected YAML config, + # deduped and order-preserving. + existing_recommendations = [] + if os.path.exists(out_ext): + try: + existing_recommendations = read_json( + out_ext).get("recommendations", []) + except Exception: + pass + + ext_ordered = list(existing_recommendations) + for ext in baseline_extensions: + if ext not in ext_ordered: + ext_ordered.append(ext) + for ext in cfg["vscode_extensions"]: + if ext not in ext_ordered: + ext_ordered.append(ext) + + print(f"[ide-workspace-setup] config: {config_path}") + print(f"[ide-workspace-setup] target workspace: {target_root}") + print( + "[ide-workspace-setup] extensions (existing + baseline + config): " + + str(len(ext_ordered)) + ) + print( + "[ide-workspace-setup] lint profiles: " + + (", ".join(cfg["lint_profiles"]) if cfg["lint_profiles"] else "(none)") + ) + print(f"[ide-workspace-setup] pre-commit mode: {cfg['pre_commit_mode']}") + + ensure_python_packages(cfg["python_packages"], apply=args.apply) + + if args.apply: + os.makedirs(os.path.join(target_root, ".vscode"), exist_ok=True) + with open(out_settings, "w", encoding="utf-8") as f: + json.dump(merged_settings, f, indent=2) + f.write("\n") + with open(out_ext, "w", encoding="utf-8") as f: + json.dump({"recommendations": ext_ordered}, f, indent=2) + f.write("\n") + print(f"[ide-workspace-setup] applied settings: {out_settings}") + print(f"[ide-workspace-setup] applied extensions: {out_ext}") + install_extensions(ext_ordered, dry_run=False) + ensure_cspell_config(target_root, repo_root, apply=True) + copy_linter_configs(target_root, repo_root, apply=True) + else: + print("[ide-workspace-setup] DRY RUN (no files written)") + print(f"[ide-workspace-setup] would write: {out_settings}") + print(f"[ide-workspace-setup] would write: {out_ext}") + install_extensions(ext_ordered, dry_run=True) + ensure_cspell_config(target_root, repo_root, apply=False) + copy_linter_configs(target_root, repo_root, apply=False) + + print( + "[ide-workspace-setup] rerun after editing " + "./local_ide_settings.yml" + ) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/bin/ide-workspace-setup.sh b/bin/ide-workspace-setup.sh new file mode 100644 index 0000000..fe9d554 --- /dev/null +++ b/bin/ide-workspace-setup.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +PYTHON_CMD="" +PYTHON_ARGS=() + +# On Windows shells such as Git Bash/MSYS, prefer native Windows Python over +# the POSIX-layer /usr/bin/python3 so subprocess calls to VS Code CLI wrappers +# behave consistently. +if [[ "${OS:-}" == "Windows_NT" || "${MSYSTEM:-}" != "" || "${OSTYPE:-}" == msys* || "${OSTYPE:-}" == cygwin* ]]; then + if command -v python.exe >/dev/null 2>&1; then + PYTHON_CMD="$(command -v python.exe)" + elif command -v py.exe >/dev/null 2>&1; then + PYTHON_CMD="$(command -v py.exe)" + PYTHON_ARGS=(-3) + fi +fi + +if [[ -z "${PYTHON_CMD}" ]]; then + if command -v python3 >/dev/null 2>&1; then + PYTHON_CMD="$(command -v python3)" + elif command -v python >/dev/null 2>&1; then + PYTHON_CMD="$(command -v python)" + fi +fi + +if [[ -z "${PYTHON_CMD}" ]]; then + echo "python3 is required for bin/ide-workspace-setup.sh" >&2 + echo "run ./bin/bootstrap-python.sh first" >&2 + exit 1 +fi + +exec "${PYTHON_CMD}" "${PYTHON_ARGS[@]}" \ + "${REPO_ROOT}/bin/ide-workspace-setup.py" "$@" diff --git a/bin/run-checks.ps1 b/bin/run-checks.ps1 index f75582a..c592534 100644 --- a/bin/run-checks.ps1 +++ b/bin/run-checks.ps1 @@ -10,8 +10,7 @@ $requiredPaths = @( 'LICENSE', 'bin/run-checks.sh', 'checks/pre-commit_d', - 'ide/vscode/settings-baseline.json', - 'ide/vscode/extensions-baseline.json' + 'ide/reference/recommended_settings.yml' ) foreach ($path in $requiredPaths) { @@ -20,9 +19,4 @@ foreach ($path in $requiredPaths) { } } -Write-Host "[check] validating JSON syntax" -Get-ChildItem -Path 'ide/vscode' -Filter '*.json' -Recurse | ForEach-Object { - $null = Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json -} - Write-Host "All checks passed." diff --git a/bin/run-checks.sh b/bin/run-checks.sh index a386023..ca0f532 100644 --- a/bin/run-checks.sh +++ b/bin/run-checks.sh @@ -10,8 +10,7 @@ required_paths=( "LICENSE" "bin/run-checks.sh" "checks/pre-commit_d" - "ide/vscode/settings-baseline.json" - "ide/vscode/extensions-baseline.json" + "ide/reference/recommended_settings.yml" ) for p in "${required_paths[@]}"; do @@ -26,9 +25,4 @@ while IFS= read -r -d '' file; do bash -n "$file" done < <(find bin checks -type f -name '*.sh' -print0) -echo "[check] validating JSON syntax" -while IFS= read -r -d '' file; do - python3 -m json.tool "$file" >/dev/null -done < <(find ide/vscode -type f -name '*.json' -print0) - echo "All checks passed." diff --git a/cspell.config.yaml b/cspell.config.yaml new file mode 100644 index 0000000..1a6ba3d --- /dev/null +++ b/cspell.config.yaml @@ -0,0 +1,18 @@ +version: '0.2' +language: en +useGitignore: true +words: [] +flagWords: [] +dictionaryDefinitions: + - name: project-words + path: ./vscode-project-words.txt + addWords: true + scope: workspace + +dictionaries: + - project-words + +ignorePaths: + - .git/** + - .venv/** + - node_modules/** diff --git a/docs/ide-customization.md b/docs/ide-customization.md new file mode 100644 index 0000000..4bad485 --- /dev/null +++ b/docs/ide-customization.md @@ -0,0 +1,191 @@ +# Developer Customization Design + +This document describes the current local customization model with one +human-maintained YAML file as input. + +## Goals + +- Keep one master input file for team readability. +- Remove profile-file indirection from user customization. +- Keep local user customization out of version control. +- Keep setup cross-platform with one Python engine and thin shell wrappers. + +## Core Decision + +Use a single YAML customization file. + +- Reference file (committed): `ide/reference/recommended_settings.yml` +- Optional local user file (untracked): `./local_ide_settings.yml` + +No profile selection is required in user config. Users edit final desired +settings directly in YAML, under IDE-specific sections. + +## File Layout + +Committed files: + +- `ide/reference/recommended_settings.yml` +- `bin/ide-workspace-setup.py` +- `bin/ide-workspace-setup.ps1` +- `bin/ide-workspace-setup.sh` + +Optional untracked local file: + +- `local_ide_settings.yml` + +Tracked ignore rule: + +```gitignore +/local_ide_settings.yml +``` + +## YAML Contract (v1) + +Top-level keys: + +- `ide` +- `linting` +- `preCommit` +- `packageSources` + +Current IDE section: + +- `ide.vscode` + +Future IDE sections may be added by maintainers (for example, `ide.intellij`). + +Minimal example: + +```yaml +--- +ide: + vscode: + settings: {} + extensions: + recommendations: [] + +linting: + profiles: [] + +preCommit: + mode: selected + +packageSources: + allowUncertifiedSources: false + allowedSources: [] +``` + +Practical example: + +```yaml +--- +ide: + vscode: + settings: + editor: + formatOnSave: false + files: + associations: + LICENSE: plaintext + NOTICE: plaintext + extensions: + recommendations: + - redhat.ansible + - redhat.vscode-yaml + +linting: + profiles: [] + +preCommit: + mode: selected + +packageSources: + allowUncertifiedSources: false + allowedSources: [] +``` + +## Setup Behavior + +`bin/ide-workspace-setup.py` reads config in this order: + +1. `./local_ide_settings.yml` in current working directory (if present) +2. otherwise `ide/reference/recommended_settings.yml` + +Execution model: + +- Run from consumer repository root when this repository is used as a submodule. +- Run from this repository root when developing this repository directly. +- Use the chosen submodule directory name as path prefix for scripts and + reference files in commands. + +Processing behavior: + +- Baseline VS Code settings and extension recommendations are loaded from + `ide/reference/recommended_settings.yml`. +- The selected YAML config (`./local_ide_settings.yml` when present, otherwise + the reference file) is merged on top of baseline values. +- Existing `.vscode/extensions.json` recommendations are preserved, and YAML + recommendations are appended with deduplication. + +Outputs: + +- `.vscode/settings.json` +- `.vscode/extensions.json` +- `cspell.config.yaml` if missing +- `vscode-project-words.txt` if missing +- Missing root-level linter/config files copied from the shared repository + +## Validation + +Current strict validation: + +- YAML root must be a mapping. +- `ide` must be a mapping. +- `ide.vscode.settings` must be a mapping. +- `ide.vscode.extensions.recommendations` must be a list. +- `linting.profiles` must be a list. +- `preCommit.mode` must be one of: `selected`, `none`, `all`. +- `packageSources.allowUncertifiedSources` must be boolean. +- `packageSources.allowedSources` must be a list. + +## Dependency Note + +The setup engine uses `PyYAML`. + +On Windows, invoke `bin/ide-workspace-setup.py` explicitly with `python`. +Do not rely on `.py` file association, because it may be unset or may select +an interpreter outside the intended environment. +Use `bin/ide-workspace-setup.sh` for non-Windows shells. + +If missing, install using your platform's package manager: + +**macOS:** + +```bash +brew install pyyaml +``` + +**Debian/Ubuntu:** + +```bash +sudo apt install python3-yaml +``` + +**RPM-based (RHEL, CentOS, Fedora):** + +```bash +sudo dnf install python3-pyyaml +``` + +**Windows:** + +```cmd +python -m pip install pyyaml +``` + +## Why This Model + +- Easier for humans to review and maintain. +- One file contains both guidance and active configuration structure. +- Supports future IDE sections without changing the input-file model. +- YAML comments enable inline team guidance without external indirection. diff --git a/docs/maintenance.md b/docs/maintenance.md new file mode 100644 index 0000000..d0bf1ef --- /dev/null +++ b/docs/maintenance.md @@ -0,0 +1,17 @@ +# Maintenance Notes + +This document contains maintainer-oriented planning notes that are intentionally +kept out of the primary usage README. + +## Initial Consumer Targets + +- ansible-lab +- system-pipeline-lib + +## Planned Content Areas + +- Shell and PowerShell check runners +- Check script library (ansible-lint, yamllint, shellcheck, markdownlint, + groovylint, codespell) +- IDE setup baselines and a master IDE-agnostic YAML input (with VS Code + section) diff --git a/docs/usage.md b/docs/usage.md index bff2341..01a8b7f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -23,3 +23,142 @@ bash bin/setup-dev.sh ```powershell pwsh -File ./bin/setup-dev.ps1 ``` + +## Local scratch files + +Use a `zzz-` prefix for temporary local scratch files such as planning notes +and draft commit messages. + +Examples: + +- `zzz-next-review-plan.txt` +- `zzz-commit-messages.txt` + +Root-level `zzz-*` files are ignored by the repository `.gitignore` and are not +intended to be committed. + +## IDE customization design + +See [docs/ide-customization.md](docs/ide-customization.md) for the +configuration model, YAML contract, and validation rules. + +## VS Code extensions baseline + +See [docs/vscode-extensions.md](docs/vscode-extensions.md) for recommendations +on which VS Code extensions to install, platform-specific requirements, and +optional external tool dependencies. + +## VS Code spell checker + +See [docs/vscode-cspell.md](docs/vscode-cspell.md) for guidance on using the +VS Code Code Spell Checker extension, including how to keep an IDE-only +project dictionary separate from `codespell` hook and CI configuration. + +## IDE customization + +Submodule usage model: + +- Run from the base directory of the consumer repository clone. +- Prefix script and reference paths with your chosen submodule directory name. +- If you want to customize the recommended settings, keep an optional + user-maintained file at `./local_ide_settings.yml` in the directory where + setup is run. +- `./local_ide_settings.yml` is intentionally untracked and should not be + committed. +- When working directly in this repository, drop the submodule path prefix + on commands and paths. + +Run the Windows bootstrap script (normally one-time per system; rerun when +validating or repairing tooling): + +```powershell +.\code_checking\bin\bootstrap-windows-dev.ps1 +``` + +The bootstrap script installs and configures: + +- Git for Windows (via winget) +- Python 3 (via winget) +- PyYAML Python package (via pip) +- Global Git settings: `core.autocrlf=input`, `core.eol=lf`, + `core.safecrlf=true`, `core.filemode=false`, `core.symlinks=false`, + `core.longpaths=true` + +The only prerequisite for the bootstrap script itself is `winget` (App +Installer), which is included with Windows 11 or available from the Microsoft +Store. + +Note: Use one Git implementation per working tree on Windows. Do not alternate +writes between Git for Windows, WSL git, and other Git clients in the same +checkout because file mode, symlink, and path handling differences can +corrupt files or repository metadata. + +Non-Windows shells can use `bootstrap-python.sh` to verify that Python 3 is +available before running setup. + +```bash +bash ./code_checking/bin/bootstrap-python.sh +``` + +CI validation on Windows (no installs): + +```powershell +.\code_checking\bin\bootstrap-windows-dev.ps1 -ValidateOnly +``` + +Copy template to local file if you want custom ide settings different than +the recommended settings. Note that you can remove settings or change values, +but the ide-workspace-setup.py script only knows about what settings are +in the recommended settings YAML file. + +```bash +cp ./code_checking/ide/reference/recommended_settings.yml ./local_ide_settings.yml +``` + +Then edit `./local_ide_settings.yml` and adjust: + +- `ide.vscode.settings` for workspace settings overrides +- `ide.vscode.extensions.recommendations` for extra extension recommendations +- `linting`, `preCommit`, and `packageSources` for local policy values + +Setup command names: + +- Windows: `python .\code_checking\bin\ide-workspace-setup.py` (dry run) +- Non-Windows shells: `bash ./code_checking/bin/ide-workspace-setup.sh` (dry run) +- Add `--apply` to perform a live write + +Examples: + +```powershell +python .\code_checking\bin\ide-workspace-setup.py +python .\code_checking\bin\ide-workspace-setup.py --apply +``` + +```bash +bash ./code_checking/bin/ide-workspace-setup.sh +bash ./code_checking/bin/ide-workspace-setup.sh --apply +``` + +Note: `ide-workspace-setup` merges into any existing `.vscode/settings.json` +rather than replacing it, so existing settings and word lists are preserved. +It also runs `code --install-extension` for each recommended extension. +Run from a VS Code integrated terminal or ensure `code` is on your PATH. +On Windows, use explicit `python ...\ide-workspace-setup.py` invocation. +Do not rely on `.py` file association, because it may be unset or may launch +an interpreter outside the intended VS Code or workspace Python environment. +Also avoid plain `bash` because it may accidentally run the setup through WSL. + +### VS Code Python Interpreter Selection + +After running `ide-workspace-setup` for the first time, VS Code may prompt you +to select a Python interpreter when you open a python or ansible file. This is +a one-time manual step: + +1. Click "Select Interpreter" in the status bar or run + `Python: Select Interpreter` from the Command Palette. +2. Choose your desired Python 3 installation from the list. +3. VS Code caches this selection in the workspace and will not prompt again. + +Setup cannot pre-select the interpreter automatically because VS Code stores +interpreter selections in internal workspace state, and the correct path varies +by system and installation method. diff --git a/docs/vscode-cspell.md b/docs/vscode-cspell.md new file mode 100644 index 0000000..0bf4d8b --- /dev/null +++ b/docs/vscode-cspell.md @@ -0,0 +1,212 @@ +# VS Code Code Spell Checker Usage + +This document covers use of the VS Code extension +`streetsidesoftware.code-spell-checker`. + +It is intentionally separate from `codespell`, which is the spell checker used +by commit hooks and GitHub Actions. + +The VS Code spell checker can use a repository-stored configuration file and a +repository-stored custom dictionary file that are separate from the dictionary +or ignore-word files used by `codespell` in commit hooks and CI. + +Recommended pattern: + +- Use a repo `cspell` config file for IDE spell checking. +- Use a separate repo text file for IDE-specific accepted words. +- Keep `codespell` ignore words and config separate for hooks and CI. +- Do not rely on user-global settings or `.vscode/settings.json` for project + training. + +## Important Distinction + +These are different tools: + +- VS Code extension: `streetsidesoftware.code-spell-checker` +- Underlying config format for that extension: `cspell` +- Commit hook / CI tool: `codespell` + +They do not use the same dictionary format and do not read the same config files +by default. + +That means a repo `cspell.json` or `cspell.config.yaml` file can be committed +for IDE use without changing `codespell` behavior in hooks or GitHub Actions. + +## Why a Separate IDE Dictionary Makes Sense + +The Code Spell Checker extension is designed to catch unknown words in general +text and code comments. It often needs project-specific training for: + +- product names +- repository names +- acronyms +- internal abbreviations +- domain-specific terms + +`codespell` behaves differently. It uses curated typo dictionaries and is better +at catching common misspellings with fewer project-training changes. + +Because of that difference, it is reasonable to keep: + +- a `codespell` allowlist for hook and CI false positives +- a separate `cspell` project dictionary for IDE comfort + +## Recommended Repository Layout + +Suggested files: + +- `cspell.config.yaml` +- `vscode-project-words.txt` + +Example purpose of each file: + +- `cspell.config.yaml`: project-level config for the VS Code extension +- `vscode-project-words.txt`: accepted words for IDE spell checking + +## Recommended Configuration + +A root `cspell.config.yaml` file is the cleanest option because the VS Code +extension can discover it automatically in the repository. + +Example: + +```yaml +version: '0.2' +language: en +useGitignore: true +words: [] +dictionaryDefinitions: + - name: project-words + path: ./vscode-project-words.txt + addWords: true + scope: workspace + +dictionaries: + - project-words + +ignorePaths: + - .git/** + - .venv/** + - node_modules/** +``` + +Example dictionary file: + +```text +ansible-lab +codespell +groovylint +shellcheck +``` + +## How This Helps + +With that pattern: + +- the VS Code extension gets a project dictionary from the repository +- the dictionary file is separate from `codespell` hook configuration +- user-global spell checker state is not required +- project-specific training can be reviewed like normal source changes + +## How to Add Words + +Preferred order: + +1. If the word is a real project term used broadly, add it to the repo + `vscode-project-words.txt` file. +2. If the word is specific to one file, use an inline `cSpell:ignore` or + `cSpell:words` comment. +3. If the word is personal or not appropriate for the repository, keep it in the + user's own global dictionary instead of committing it. + +In VS Code, right-click on a misspelled word and choose one of these: + +- Add to Workspace Dictionary: writes the word to `vscode-project-words.txt` + based on `cspell.config.yaml` dictionaryDefinitions. +- Add to Workspace Settings: writes the word into `.vscode/settings.json` + (`cSpell.words`) for this workspace. +- Add to User Settings: writes the word into your personal VS Code user settings. + +Recommended team workflow: + +1. Use Add to Workspace Dictionary for stable, project-wide terms. +2. Review and commit updates in `vscode-project-words.txt`. +3. Use Add to Workspace Settings only for temporary local exceptions. +4. Use Add to User Settings only for personal preferences. + +Manual edit workflow: + +1. Open `vscode-project-words.txt` in the repository root. +2. Add one accepted word per line. +3. Save the file; VS Code cspell diagnostics update automatically. +4. Commit the change when the term is broadly applicable to the project. + +Examples: + +JavaScript / TypeScript / many code files: + +```text +// cSpell:ignore myspecialtoken +// cSpell:words repoSpecificTerm +``` + +Markdown: + +```html + +``` + +## What Not to Do + +Avoid these patterns for project training: + +- storing project spell-check training only in VS Code user settings +- storing project spell-check training only in `.vscode/settings.json` +- mixing `cspell` project words into `codespell` ignore-word files + +Those approaches either make the setup non-portable or blur the distinction +between the IDE checker and the hook/CI checker. + +## Interaction With `codespell` + +`codespell` uses its own configuration and allowlist mechanisms, such as: + +- `.codespellrc` +- `setup.cfg` +- `pyproject.toml` +- `codespell -I ` ignore-word files + +A `cspell.config.yaml` or `cspell.json` file does not replace those and does not +change `codespell` behavior unless you explicitly wire the two together. + +That separation is useful here. + +## Recommended Policy For This Repository + +Recommended baseline: + +- Commit a repo-level `cspell` config for IDE use. +- Commit a repo-level `vscode-project-words.txt` file for IDE-only accepted terms. +- Keep `codespell` hook and CI configuration separate. +- Only add stable, broadly used terms to the repo IDE dictionary. +- Keep user-specific or temporary words out of the repository. + +## Scope Note + +This document only covers repository-managed spell-check behavior for the VS Code +extension. + +It does not attempt to standardize: + +- a user's personal global dictionary +- local site policy for extension installation +- local AI policy or compliance requirements +- editor settings unrelated to spell checking + +## Current Repository State + +This repository currently uses: + +1. `cspell.config.yaml` +2. `vscode-project-words.txt` +3. separate `codespell` configuration for hooks and CI diff --git a/docs/vscode-extensions.md b/docs/vscode-extensions.md new file mode 100644 index 0000000..43246ee --- /dev/null +++ b/docs/vscode-extensions.md @@ -0,0 +1,340 @@ +# VS Code Extensions Baseline + +This document catalogs the VS Code extensions initially used in this +project, their purposes, and platform-specific requirements. + +The `ide/reference/recommended_settings.yml` has a list of the current +recommended settings. This document explains the baseline and platform notes. + +**Scope:** IDE productivity and code quality tools. Docker/Podman +containerization is explicitly out of scope and excluded from this baseline. + +## Extensions to Install Directly + +These are the primary extensions recommended for all developers. +Install only these; their dependent extensions will be pulled in automatically. +Some extensions require specific software packages to be installed on the host. + +### AI & Code Assistance + +> **⚠ Important:** AI tools like GitHub Copilot may be subject to local +> site security policies, data residency requirements, or acceptable use +> policies. Verify compliance with your organization's IT and legal guidelines +> _before_ installing or using these extensions. +> **Configuring AI tools to meet local site requirements is outside the scope +> of this document.** Contact your site administrator or security team +> for guidance. + +#### `github.copilot-chat` + +- **Publisher:** GitHub +- **Purpose:** AI-powered code suggestions and conversations within the editor. +- **Platform Notes:** + - Windows: Requires Windows Git to be installed for SSH key authentication. + - macOS/Linux: SSH support included with native Git. +- **Related Dependencies:** None (stands alone; Copilot Chat does not + require base Copilot). + +### Language Support & Linting + +#### `ms-python.python` + +- **Publisher:** Microsoft +- **Purpose:** Full Python language support including IntelliSense, + debugging, testing. +- **Platform Notes:** + - Windows: Python path will be auto-detected if in `PATH`; + compatible environments + include system Python, venv, virtualenv, conda, conda-forge, Poetry, + Pipenv, pyenv. + - macOS/Linux: Same as Windows. + - **Important:** For Ansible IDE support to work, a compatible Python environment + must be installed on the system (not portable within VS Code). +- **Included Extensions:** + - `ms-python.debugpy` (Python debugger) + - `ms-python.vscode-pylance` (Language server) + - `ms-python.vscode-python-envs` (Environment detection) + - `ms-python.isort` (Import sorting, optional) + +#### `redhat.ansible` + +- **Publisher:** Red Hat +- **Purpose:** Ansible playbook and role editing with syntax highlighting, completion, + and validation. +- **Platform Notes:** + - Requires Ansible CLI to be installed locally for full validation. + - Requires a compatible Python environment (see `ms-python.python`). + - WSL and remote SSH workflows are supported. +- **Related Dependencies:** `redhat.vscode-yaml` (YAML support). + +#### `redhat.vscode-yaml` + +- **Publisher:** Red Hat +- **Purpose:** YAML syntax highlighting and validation. +- **Use Cases:** Ansible, cloud-init, Kubernetes, CI/CD configs, general YAML. +- **Related Dependencies:** None (standalone). + +#### `timonwong.shellcheck` + +- **Publisher:** Timon Wong +- **Purpose:** Shell script linting (bash, sh). +- **Platform Notes:** + - Bundled `shellcheck` binaries are included for supported Windows, macOS, + and Linux platforms. + - A separate `shellcheck` CLI install is optional when you want to override + the bundled binary or need support for an unsupported platform or + architecture. +- **Related Dependencies:** None (standalone). + +#### `nicolasvuillamy.vscode-groovy-lint` + +- **Publisher:** Nicolas Vuillamy +- **Purpose:** Groovy and Jenkins DSL linting and formatting. +- **Platform Notes:** + - The extension bundles the Groovy linting stack. + - For the default setup path, no separate system Java install is required. +- **Related Dependencies:** None (standalone). + +### Documentation & Quality Tools + +#### `davidanson.vscode-markdownlint` + +- **Publisher:** David Anson +- **Purpose:** Markdown linting for consistent document formatting. +- **Use Cases:** README files, documentation, commit message formatting. +- **Related Dependencies:** None (standalone). + +#### `bierner.markdown-preview-github-styles` + +- **Publisher:** Matt Bierner +- **Purpose:** Markdown preview with GitHub-flavored styling. +- **Use Cases:** Previewing README and documentation during editing. +- **Related Dependencies:** None (standalone). + +#### `streetsidesoftware.code-spell-checker` + +- **Publisher:** Street Side Software +- **Purpose:** Spell checking for code comments, strings, and documentation. +- **Usage Notes:** Project usage guidance and dictionary strategy are documented + in `docs/vscode-cspell.md`. +- **Related Dependencies:** None (standalone). + +### Remote & SSH Access + +#### `ms-vscode-remote.remote-ssh` + +- **Publisher:** Microsoft +- **Purpose:** SSH client integration for working on remote Linux hosts, lab systems, etc. +- **Platform Notes:** + - Windows: + - Can use Windows built-in SSH (Windows 10+) if configured. + - Can use Git for Windows SSH client as an alternative. + - Does NOT support Pageant or other third-party SSH agents in older versions. + - macOS/Linux: Uses native SSH client. + - **Important:** Direct access to lab hosts may require: + - Fully qualified domain names (FQDN) for host discovery. + - Local proxy settings for jump host access. + - These configuration details are beyond the scope of this repository. +- **Related Dependencies:** + - `ms-vscode-remote.remote-ssh-edit` (Remote file editing support) + - `ms-vscode.remote-explorer` (Explorer UI for remote systems). + +### Build Tools + +#### `ms-vscode.makefile-tools` + +- **Publisher:** Microsoft +- **Purpose:** Makefile support including syntax highlighting, task integration, and debugging. +- **Related Dependencies:** None (standalone). + +#### `ms-vscode.powershell` + +- **Publisher:** Microsoft +- **Purpose:** PowerShell script editing, IntelliSense, debugging, and execution. +- **Use Cases:** Windows build scripts, CI/CD pipeline scripts, setup automation. +- **Related Dependencies:** None (standalone). + +--- + +## Dependencies (Automatically Installed) + +These extensions are automatically installed as dependencies of the direct installs +above. **Do not install these separately.** + +| Extension | Installed As Dependency Of | Purpose | +| --------- | ------------------------- | ------- | +| `ms-python.debugpy` | `ms-python.python` | Python runtime debugging support. | +| `ms-python.vscode-pylance` | `ms-python.python` | Pylance language server for advanced type checking. | +| `ms-python.vscode-python-envs` | `ms-python.python` | Automatic Python environment detection and management. | +| `ms-python.isort` | `ms-python.python` | Optional: Python import sorting. | +| `ms-vscode-remote.remote-ssh-edit` | `ms-vscode-remote.remote-ssh` | SSH file editing and remote path handling. | +| `ms-vscode.remote-explorer` | Remote SSH/WSL | Unified explorer UI for remote connections. | + +--- + +## Platform-Specific Installation Requirements + +This is mainly informational, some of these dependencies may be installed +by scripts run during setup of the IDE. + +### Windows + +1. **Git Installation** + - Required by: `github.copilot-chat` + - Download: + [https://git-scm.com/download/win](https://git-scm.com/download/win) + - Alternatively provides: SSH client (can be used instead of Windows SSH) + +2. **Python** + - Required by: `ms-python.python`, `redhat.ansible` + - Options: + - Windows Store: `python` app + - System installer: python.org + - Conda: conda-forge (recommended for Ansible users) + - Package managers: `choco install python`, `scoop install python` + - Verify: `python --version` in PowerShell + +3. **shell** linting + - Required by: `timonwong.shellcheck` + - Separate install: not required on supported platforms because the VS Code + extension bundles `shellcheck` + - Optional standalone install: use winget, scoop, chocolatey, or a manual + native install if you want a system `shellcheck` binary + +4. **SSH Client** + - Default: Windows 10+ native SSH (built-in) + - Alternative: Git for Windows SSH client + - For lab access: configure FQDN/proxy separately (out of scope) + +### macOS + +1. **Git** + - Usually pre-installed; check: `git --version` + - Or install via Homebrew: `brew install git` + +2. **Python** + - System Python: `python3` (usually available) + - Managed: `brew install python@3.11` or use conda + - Verify: `python3 --version` + +3. **Ansible CLI** (for full validation) + - Install: `brew install ansible` + - Verify: `ansible --version` + +4. **Shell linting** + - Separate install: not required on supported platforms because the VS Code + extension bundles `shellcheck` + - Optional standalone install: `brew install shellcheck` + +5. **SSH Client** + - Built-in: OpenSSH (native) + - Configure lab access separately if needed. + +### Linux + +1. **Git** + - Do not assume it is pre-installed; check: `git --version` + - Or: `sudo apt install git` (Debian/Ubuntu) or equivalent + +2. **Python** + - Often already present, especially on RPM-based systems; check: + `python3 --version` + - Or: Package manager install (e.g., `apt install python3`) + +3. **Ansible CLI** (for full validation) + - Install: `sudo apt install ansible` or equivalent + - Verify: `ansible --version` + +4. **Shell linting** + - Separate install: not required on supported platforms because the VS Code + extension bundles `shellcheck` + - Optional standalone install: `apt install shellcheck` (Debian/Ubuntu) + - Or equivalent on your distro + +5. **SSH Client** + - Built-in: OpenSSH client (usually pre-installed) + - Configure lab access separately if needed. + +--- + +## Not Included (Out of Scope) + +The following kinds of extensions/tools are explicitly **not** part of +this baseline: + +- **Docker/Podman:** Container tooling is deferred to a future phase. + Direct local Docker development requires a paid Docker Desktop license + on Windows; WSL-based + workflows are better deferred until container strategy is decided. +- **Lab System Configuration:** FQDN discovery, jump host proxies, and other + infrastructure-specific setup are handled outside this repository. +- **Third-Party Cloud SDKs:** AWS, Azure, Google Cloud extensions are + not included by default but can be added on a per-project basis. + +--- + +## Installation Quick Start + +This is a manual fallback. In the normal path, `ide-workspace-setup` handles +the workspace files and recommended extension installs. + +### Step 1: Install Base Extensions + +```bash +code --install-extension github.copilot-chat +code --install-extension ms-python.python +code --install-extension redhat.ansible +code --install-extension redhat.vscode-yaml +code --install-extension timonwong.shellcheck +code --install-extension nicolasvuillamy.vscode-groovy-lint +code --install-extension davidanson.vscode-markdownlint +code --install-extension bierner.markdown-preview-github-styles +code --install-extension streetsidesoftware.code-spell-checker +code --install-extension ms-vscode-remote.remote-ssh +code --install-extension ms-vscode.makefile-tools +code --install-extension ms-vscode.powershell +``` + +### Step 2: Install Platform-Specific Tools + +**Windows:** + +Windows Python is obtained from the Microsoft Store. +Windows Git is obtained from its official location. + +**macOS:** + +```bash +brew install git python@3.11 ansible +``` + +**Linux:** + +```bash +# Debian/Ubuntu example: +sudo apt install git python3 ansible +``` + +### Step 3: Verify Installations + +```bash +git --version +python --version +ansible --version # if Ansible CLI needed +``` + +`shellcheck --version` is only applicable if you installed a standalone system +`shellcheck` binary. The VS Code extension can work without it by using its +bundled binary. + +--- + +## Future Enhancements + +- [ ] Document Docker/Podman workflow once containerization + strategy is finalized. +- [ ] Add cloud SDK recommendations if multi-cloud development is needed. +- [ ] Consider language-specific extensions (Go, Rust, etc.) based on + project scope. +- [ ] Define Ansible-specific profiling recommendations once workflows + are documented. diff --git a/ide/reference/recommended_settings.yml b/ide/reference/recommended_settings.yml new file mode 100644 index 0000000..8bdef8a --- /dev/null +++ b/ide/reference/recommended_settings.yml @@ -0,0 +1,109 @@ +--- +# Master IDE customization reference. +# +# Copy to: ./local_ide_settings.yml +# Edit there for local preferences. +# +# IDE-specific customization. +# Add future IDE sections here (for example, ide.intellij) if teams choose to +# maintain translations from VS Code concepts. +ide: + # VS Code workspace customization. + vscode: + # Workspace settings merged on top of repository baseline defaults. + settings: + editor: + # Default tab size (can be overridden per language or use EditorConfig). + tabSize: 4 + # Insert spaces when Tab is pressed; overridden per language where + # literal tabs are required (e.g. makefiles). + insertSpaces: true + # Don't auto-detect indentation from file content; honour these settings. + detectIndentation: false + # Visual column guides at the two standard terminal widths. + # Each entry is a mapping with a required "column" key and an + # optional "color" key (CSS hex color string, e.g. "#FF000066" for + # semi-transparent red). Colors can also be set interactively via + # the VS Code settings UI color picker. + rulers: + # default standard terminal width, also best for quickly reading + # without wrapping or horizontal scrolling. + - column: 80 + # color: "#00FF00" + # wider standard terminal width / default width for line printers + # Many listing programs use this format to annotate 80-column code. + - column: 132 + # color: "#FFFF00" + # Workaround for shellcheck extension not clearing fixed problems + # after they are repaired. Need to save a file to trigger a + # shellcheck re-run and clear the fixed diagnostics + formatOnSave: false + files: + # Default to LF line endings to avoid CRLF-related portability issues. + eol: "\n" + # Force known files specifications that vscode may not automatically + # recognize as having the correct language mode. + associations: + # Plaintext files without extensions + LICENSE: plaintext + NOTICE: plaintext + "**/CODEOWNERS": plaintext + # Ansible files + "**/group_vars/**/vars": ansible + "**/group_vars/**/gvars": ansible + "**/host_vars/**/vars": ansible + + # Language-specific settings (groovy/gradle indent matches groovylint + # and shellcheck defaults of 4 spaces) + "[groovy]": + editor: + tabSize: 4 + + # Makefiles require literal tab characters for recipe lines. + "[makefile]": + editor: + insertSpaces: false + + # Extension recommendations merged from baseline and local preferences. + extensions: + recommendations: + # Core IDE extensions + - github.copilot-chat + - ms-python.python + - ms-vscode.powershell + - ms-vscode.makefile-tools + # EditorConfig behavior is defined in .editorconfig; install a + # trusted extension manually if policy allows. + # Linting and formatting + - redhat.ansible + - redhat.vscode-yaml + - timonwong.shellcheck + - nicolasvuillamy.vscode-groovy-lint + - streetsidesoftware.code-spell-checker + # Documentation + - davidanson.vscode-markdownlint + - bierner.markdown-preview-github-styles + # Remote access + - ms-vscode-remote.remote-ssh + +# Reserved for future local lint profile wiring. +linting: + profiles: [] + +# Local pre-commit behavior. +preCommit: + # selected | none | all + mode: selected + +# Local setup self-healing for Python package prerequisites. +# Note: pyyaml auto-install via pip is Windows-only; on non-Windows hosts, +# prefer installing distro packages first. +setup: + python: + packages: + - pyyaml + +# Local policy for non-certified package sources. +packageSources: + allowUncertifiedSources: false + allowedSources: [] diff --git a/ide/vscode/extensions-baseline.json b/ide/vscode/extensions-baseline.json deleted file mode 100644 index d701754..0000000 --- a/ide/vscode/extensions-baseline.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "recommendations": [ - "github.copilot", - "github.copilot-chat", - "redhat.ansible", - "timonwong.shellcheck" - ] -} diff --git a/ide/vscode/profiles/ansible.json b/ide/vscode/profiles/ansible.json deleted file mode 100644 index d298ce8..0000000 --- a/ide/vscode/profiles/ansible.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extensions": [ - "redhat.ansible" - ], - "settings": { - "files.associations": { - "**/group_vars/**/vars": "ansible", - "**/group_vars/**/gvars": "ansible", - "**/host_vars/**/vars": "ansible" - } - } -} diff --git a/ide/vscode/profiles/ci-groovy.json b/ide/vscode/profiles/ci-groovy.json deleted file mode 100644 index fe0e9e4..0000000 --- a/ide/vscode/profiles/ci-groovy.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extensions": [ - "naco-siren.gradle-language" - ], - "settings": { - "editor.tabSize": 2 - } -} diff --git a/ide/vscode/profiles/coding.json b/ide/vscode/profiles/coding.json deleted file mode 100644 index 8e083fc..0000000 --- a/ide/vscode/profiles/coding.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extensions": [], - "settings": { - "editor.formatOnSave": false - } -} diff --git a/ide/vscode/settings-baseline.json b/ide/vscode/settings-baseline.json deleted file mode 100644 index c388cbf..0000000 --- a/ide/vscode/settings-baseline.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "git.inputValidation": true, - "git.inputValidationSubjectLength": 50, - "git.inputValidationLength": 72, - "[git-commit]": { - "editor.rulers": [50, 72], - "editor.wordWrap": "wordWrapColumn", - "editor.wordWrapColumn": 72 - } -} diff --git a/vscode-project-words.txt b/vscode-project-words.txt new file mode 100644 index 0000000..2fd82d7 --- /dev/null +++ b/vscode-project-words.txt @@ -0,0 +1,27 @@ +# Project-specific words accepted by cspell +# Add one word per line +Anson +choco +codespellrc +davidanson +debugpy +filemode +groovylint +gvars +isort +LASTEXITCODE +longpaths +makefiles +MSYSTEM +nicolasvuillamy +Pipenv +pyenv +pylintrc +pyproject +pyyaml +safecrlf +timonwong +venv +virtualenv +Vuillamy +winget