diff --git a/.gitignore b/.gitignore index 6531108..a6c7c7d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,10 @@ docs/contributing.md docs/home.md docs/vendors.md docs/examples/**/*.md + +# Development setup exclusions β€” keeping the repository clean and minimal. +# Except for `.venv/`, these files are typically not included in +# `.gitignore`, but are listed here to maintain simplicity and clarity. +.venv/ +.pre-commit-config.yaml +.markdownlint.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8545d98..cb712a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,3 +7,44 @@ image: "https://raw.githubusercontent.com/bitol-io/artwork/main/horizontal/color # Contributing to Open Data Contract Standard Thank you for your interest in contributing to Open Data Contract Standard (ODCS). Please refer to the [TSC contributing guidelines](https://github.com/bitol-io/tsc/blob/main/CONTRIBUTING.md). + +## Create a Local Development Environment + +To set up a local development environment, use the predefined setup scripts: + +
For Windows Development Environment run: + +```shell +. \scr\script\dev_setup.ps1 +``` + +
+ +
For Unix Development Environment run: + +```bash +source src/script/dev_setup.sh +``` + +
+ +Each of these scripts will: + +* Check the virtual environment: + * Create and activate it if missing + * Activate it if not already active +* Check `pip` status: + * Verify the current version + * Compare against the latest version + * Upgrade if necessary +* Check `pre_commit` status: + * Install if missing +* Check `pre-commit` version: + * Verify the current version + * Compare against the latest version + * Upgrade if necessary +* Check `pre-commit` hooks: + * Create `.pre-commit-config.yaml` if missing + * Update and install all hooks +* Check `.markdownlint.json`: + * Create the file if it doesn’t exist diff --git a/src/script/dev_setup.ps1 b/src/script/dev_setup.ps1 new file mode 100644 index 0000000..73857c4 --- /dev/null +++ b/src/script/dev_setup.ps1 @@ -0,0 +1,267 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Developer environment setup (PowerShell 7+) +.DESCRIPTION + Creates/activates venv (where possible), checks/updates pip and pre-commit, + and Asserts config files (.pre-commit-config.yaml, .markdownlint.json, .commitlintrc.json). +#> + +# ----------------------------- +# 🎨 Color map & helpers +# ----------------------------- +$Colors = @{ + 'Info' = 'Cyan' + 'Task' = 'Yellow' + 'Pass' = 'Green' + 'Warn' = 'Magenta' + 'Fail' = 'Red' +} + +function Write-Info([string]$m){Write-Host "πŸ’‘ [INFO] $m" -ForegroundColor $Colors.Info } +function Write-Task([string]$m){ Write-Host "⚑ [TASK] $m" -ForegroundColor $Colors.Task } +function Write-Pass([string]$m){ Write-Host "βœ… [PASS] $m" -ForegroundColor $Colors.Pass } +function Write-Warn([string]$m){ Write-Host "⚠️ [WARN] $m" -ForegroundColor $Colors.Warn } +function Write-Fail([string]$m){ Write-Host "❌ [FAIL] $m" -ForegroundColor $Colors.Fail } + +# ----------------------------- +# 🌱 Virtual environment +# ----------------------------- +$VenvDir = ".venv" + +function Assert-VirtualEnv { + Write-Info "Checking virtual environment status ..." + + # PowerShell activation path (Windows & pwsh) + $ActivatePs1 = Join-Path $VenvDir "Scripts/Activate.ps1" + + if (Test-Path $ActivatePs1) { + if ($env:VIRTUAL_ENV) { + Write-Pass "Virtual environment found and active." + } else { + Write-Info "Virtual environment found but not active." + Write-Task "activating ..." + try { + & $ActivatePs1 + } catch { + Write-Warn "Activation script ran but returned an error: $($_.Exception.Message)" + } + } + } else { + Write-Warn "No virtual environment found." + Write-Task "creating and activating ..." + try { + & python -m venv $VenvDir 2>&1 | Out-Null + & $ActivatePs1 + } catch { + Write-Fail "Failed to create virtual environment: $($_.Exception.Message)" + } + } +} + +# ----------------------------- +# πŸ”§ Pip version helpers (robust regex) +# ----------------------------- +function Get-PipVersion { + try { + $out = & pip --version 2>&1 + } catch { + return $null + } + $match = [regex]::Match($out, 'pip\s+(\d+(?:\.\d+)+)') + if ($match.Success) { return $match.Groups[1].Value } + return $null +} + +function Get-LatestPipVersion { + try { + $dry = & python -m pip install --upgrade pip --dry-run 2>&1 + } catch { + $dry = $null + } + if ($null -ne $dry) { + # look for "pip-1.2.3" or parenthesized versions like "(1.2.3)" + $m = [regex]::Match($dry, 'pip-?(\d+(?:\.\d+)*)') + if ($m.Success) { return $m.Groups[1].Value } + $m2 = [regex]::Match($dry, '\((\d+(?:\.\d+)*)\)') + if ($m2.Success) { return $m2.Groups[1].Value } + } + return $null +} + +function Assert-PipUpToDate { + Write-Info "Checking pip version ..." + $current = Get-PipVersion + $latest = Get-LatestPipVersion + + if (-not $current -or -not $latest) { + Write-Warn "Could not determine pip versions (current: $current, latest: $latest). Skipping upgrade check." + return + } + Write-Info "Current: $current | Latest: $latest" + if ($current -eq $latest) { + Write-Pass "pip is up to date." + } else { + Write-Warn "pip is outdated." + Write-Task "upgrading ..." + try { & python -m pip install --upgrade pip } catch { Write-Fail "pip upgrade failed: $($_.Exception.Message)" } + } +} + +# ----------------------------- +# πŸ”„ pre-commit helpers (robust regex) +# ----------------------------- +function Assert-PreCommitInstalled { + Write-Info "Checking pre-commit installation ..." + if (Get-Command pre-commit -ErrorAction SilentlyContinue) { + Write-Pass "pre-commit is installed." + } else { + Write-Warn "pre-commit is missing." + Write-Task "installing ..." + try { & pip install pre-commit } catch { Write-Fail "Failed to install pre-commit: $($_.Exception.Message)"; return } + } +} + +function Get-PreCommitVersion { + try { $out = & pre-commit --version 2>&1 } catch { return $null } + $match = [regex]::Match($out, 'pre-commit\s+(\d+(?:\.\d+)*)') + if ($match.Success) { return $match.Groups[1].Value } + return $null +} + +function Get-LatestPreCommitVersion { + try { $dry = & pip install pre-commit --upgrade --dry-run 2>&1 } catch { $dry = $null } + if ($null -ne $dry) { + $m = [regex]::Match($dry, 'commit-?(\d+(?:\.\d+)*)') + if ($m.Success) { return $m.Groups[1].Value } + $m2 = [regex]::Match($dry, '\((\d+(?:\.\d+)*)\)') + if ($m2.Success) { return $m2.Groups[1].Value } + } + return $null +} + +function Assert-PreCommitUpToDate { + Write-Info "Checking pre-commit version ..." + $current = Get-PreCommitVersion + $latest = Get-LatestPreCommitVersion + if (-not $current -or -not $latest) { + Write-Warn "Could not determine pre-commit versions (current: $current, latest: $latest). Skipping upgrade check." + return + } + Write-Info "Current: $current | Latest: $latest" + if ($current -eq $latest) { + Write-Pass "pre-commit is up to date." + } else { + Write-Warn "pre-commit is outdated." + Write-Task "upgrading ..." + try { & pip install --upgrade pre-commit } catch { Write-Fail "Failed to upgrade pre-commit: $($_.Exception.Message)" } + } +} + +# ----------------------------- +# πŸ“„ Config file creators (PowerShell-native) +# ----------------------------- +function New-PreCommitConfig { @" +default_stages: [pre-commit, manual] + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + args: ["--unsafe"] + - id: check-added-large-files + + - repo: https://github.com/tcort/markdown-link-check + rev: v3.13.7 + hooks: + - id: markdown-link-check + args: [-q] + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.45.0 + hooks: + - id: markdownlint + args: ["--ignore", "CHANGELOG.md", "--fix"] +"@ | Out-File -FilePath ".pre-commit-config.yaml" -Encoding utf8 } + +function New-MarkdownLintConfig { @" +{ + "comment": "Markdown Lint Rules", + "default": true, + "MD007": {"indent": 4}, + "MD013": false, + "MD024": false, + "MD025": {"front_matter_title": ""}, + "MD029": {"style": "one_or_ordered"}, + "MD033": false +} +"@ | Out-File -FilePath ".markdownlint.json" -Encoding utf8 } + +function New-CommitLintConfig { @" +{ + "rules": { + "body-leading-blank": [1, "always"], + "footer-leading-blank": [1, "always"], + "header-max-length": [2, "always", 72], + "scope-case": [2, "always", "upper-case"], + "scope-empty": [2, "never"], + "subject-case": [2, "never", ["start-case", "pascal-case", "upper-case"]], + "subject-empty": [2, "never"], + "subject-full-stop": [2, "never", "."], + "type-case": [2, "always", "lower-case"], + "type-empty": [2, "never"], + "type-enum": [2, "always", ["build","chore","ci","docs","feat","fix","perf","refactor","revert","style","test"]] + } +} +"@ | Out-File -FilePath ".commitlintrc.json" -Encoding utf8 } + +# ----------------------------- +# πŸ”§ Assert files and hooks +# ----------------------------- +function Assert-File([string]$path, [scriptblock]$createBlock) { + if (Test-Path $path) { + Write-Pass "$path already exists, please ensure it has the correct format." + } else { + Write-Warn "$path file is missing." + Write-Task "creating ..." + & $createBlock + } +} + +function Assert-PreCommitHooks { + Write-Info "Checking pre-commit config and hooks ..." + Assert-File ".pre-commit-config.yaml" { New-PreCommitConfig } + try { + & pre-commit autoupdate + & pre-commit install + } catch { + Write-Warn "pre-commit command failed to run: $($_.Exception.Message)" + } + + # If commit-msg/commitlint present, install commit-msg hook and Assert commitlintrc + $hasCommitMsg = Select-String -Path ".pre-commit-config.yaml" -Pattern "commit-msg|commitlint" -Quiet + if ($hasCommitMsg) { + Write-Task "Installing commit-msg hook ..." + try { + & pre-commit install --hook-type commit-msg + } catch { + Write-Warn "Could not install commit-msg hook: $($_.Exception.Message)" + } + Assert-File ".commitlintrc.json" { New-CommitLintConfig } + } + Assert-File ".markdownlint.json" { New-MarkdownLintConfig } +} + +# ----------------------------- +# πŸš€ Run tasks +# ----------------------------- +Assert-VirtualEnv +Assert-PipUpToDate +Assert-PreCommitInstalled +Assert-PreCommitUpToDate +Assert-PreCommitHooks + +Write-Pass "πŸŽ‰ Setup Completed Successfully!" diff --git a/src/script/dev_setup.sh b/src/script/dev_setup.sh new file mode 100644 index 0000000..0581747 --- /dev/null +++ b/src/script/dev_setup.sh @@ -0,0 +1,263 @@ +#!/usr/bin/env bash +set -o pipefail + +# 🎨 Colors +NC='\033[0m' # No Color +CYAN='\033[0;36m' +YELLOW='\033[0;33m' +GREEN='\033[0;32m' +RED='\033[0;31m' +MAGENTA='\033[0;35m' + +# 🌱 Default virtual environment directory +VENV_DIR=".venv" + +# ----------------------------- +# 🎯 Logging Functions +# ----------------------------- +print_info() { + echo -e "πŸ’‘ [${CYAN}INFO${NC}] ${CYAN}$1${NC}" +} +print_task() { + echo -e "⚑ [${YELLOW}TASK${NC}] ${YELLOW}$1${NC}" +} +print_pass() { + echo -e "βœ… [${GREEN}PASS${NC}] ${GREEN}$1${NC}" +} +print_warning() { + echo -e "⚠️ [${MAGENTA}WARN${NC}] ${MAGENTA}$1${NC}" +} +print_error() { + echo -e "❌ [${RED}FAIL${NC}] ${RED}$1${NC}" +} + +# ----------------------------- +# 🐍 Virtual Environment Check +# ----------------------------- +virtual_environment_check() { + print_info "Checking virtual environment ..." + + if [[ -d "$VENV_DIR" && -f "$VENV_DIR/bin/activate" ]]; then + if [[ -n "${VIRTUAL_ENV:-}" ]]; then + print_pass "Virtual environment found and active." + else + print_info "Virtual environment found but not active." + print_task "activating ..." + source "$VENV_DIR/bin/activate" + fi + else + print_warning "No virtual environment found." + print_task "creating and activating ..." + python3 -m venv "$VENV_DIR" + source "$VENV_DIR/bin/activate" + fi +} + +# ----------------------------- +# πŸ”§ Pip Version Check +# ----------------------------- +pip_current_version_check() { + CURRENT_VERSION=$(pip --version | grep -oP '(?<=pip )\d+(\.\d+)+') + [[ -z "$CURRENT_VERSION" ]] && print_error "Could not extract pip version." +} + +pip_latest_version_check() { + DRY_RUN_OUTPUT=$(python3 -m pip install --upgrade pip --dry-run 2> /dev/null) + LATEST_VERSION=$(echo "$DRY_RUN_OUTPUT" | grep -oP 'pip-[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n1 | tr -d 'pip-') + [[ -z "$LATEST_VERSION" ]] && LATEST_VERSION=$(echo "$DRY_RUN_OUTPUT" | grep -oP '\([0-9]+\.[0-9]+(\.[0-9]+)?\)' | head -n1 | tr -d '()') + [[ -z "$LATEST_VERSION" ]] && print_error "Could not determine the latest pip version." +} + +pip_status_check() { + print_info "Checking pip version ..." + print_info "Current: ${CURRENT_VERSION} | Latest: ${LATEST_VERSION}" + + if [[ "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then + print_pass "pip is up to date." + else + print_warning "pip is outdated." + print_task "updating ..." + python3 -m pip install --upgrade pip + fi +} + +# ----------------------------- +# πŸ”„ Pre-commit Check +# ----------------------------- +pre_commit_status_check() { + print_info "Checking pre-commit installation ..." + if command -v pre-commit > /dev/null 2>&1; then + print_pass "pre-commit is installed." + else + print_warning "pre-commit is missing." + print_task "Installing pre-commit ..." + pip install pre-commit + fi +} + +pre_commit_current_version_check() { + CURRENT_VERSION=$(pre-commit --version | grep -oP '(?<=pre-commit )\d+(\.\d+)+') +} + +pre_commit_latest_version_check() { + DRY_RUN_OUTPUT=$(pip install pre-commit --upgrade --dry-run 2> /dev/null) + LATEST_VERSION=$(echo "$DRY_RUN_OUTPUT" | grep -oP 'commit-[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n1 | tr -d 'commit-') + [[ -z "$LATEST_VERSION" ]] && LATEST_VERSION=$(echo "$DRY_RUN_OUTPUT" | grep -oP '\([0-9]+\.[0-9]+(\.[0-9]+)?\)' | head -n1 | tr -d '()') + [[ -z "$LATEST_VERSION" ]] && print_error "Could not determine the latest pre-commit version." +} + +pre_commit_version_check() { + print_info "Checking pre-commit version ..." + print_info "Current: ${CURRENT_VERSION} | Latest: ${LATEST_VERSION}" + + if [[ "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then + print_pass "pre-commit is up to date." + else + print_warning "pre-commit is outdated." + print_task "updating ..." + pip install --upgrade pre-commit + fi +} + +# ----------------------------- +# πŸ“„ Pre Commit Config File Creation +# ----------------------------- +pre_commit_config_create() { + cat << EOF > .pre-commit-config.yaml +default_stages: [pre-commit, manual] + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + args: ["--unsafe"] + - id: check-added-large-files + + - repo: https://github.com/tcort/markdown-link-check + rev: v3.13.7 + hooks: + - id: markdown-link-check + args: [-q] + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.45.0 + hooks: + - id: markdownlint + args: ["--ignore", "CHANGELOG.md", "--fix"] +EOF +} + +# ----------------------------- +# πŸ“„ Markdown Lint Config File Creation +# ----------------------------- +markdownlint_create() { + cat << EOF > .markdownlint.json +{ + "comment": "Markdown Lint Rules", + "default": true, + "MD007": {"indent": 4}, + "MD013": false, + "MD024": false, + "MD025": {"front_matter_title": ""}, + "MD029": {"style": "one_or_ordered"}, + "MD033": false +} +EOF +} + +# ----------------------------- +# πŸ“„ Commit Lint Config File Creation +# ----------------------------- +commitlintrc_create() { + cat << EOF > .commitlintrc.json +{ + "rules": { + "body-leading-blank": [1, "always"], + "footer-leading-blank": [1, "always"], + "header-max-length": [2, "always", 72], + "scope-case": [2, "always", "upper-case"], + "scope-empty": [2, "never"], + "subject-case": [2, "never", ["start-case", "pascal-case", "upper-case"]], + "subject-empty": [2, "never"], + "subject-full-stop": [2, "never", "."], + "type-case": [2, "always", "lower-case"], + "type-empty": [2, "never"], + "type-enum": [2, "always", ["build","chore","ci","docs","feat","fix","perf","refactor","revert","style","test"]] + } +} +EOF +} + +# ----------------------------- +# πŸ”§ Config Files Checks +# ----------------------------- +commitlintrc_file_check() { + print_info "Checking .commitlintrc.json ..." + if [[ -f ".commitlintrc.json" ]]; then + print_pass "Already exists, please ensure it has the correct format." + else + print_warning ".pre-commit-config.yaml file is missing." + print_task "creating ..." + commitlintrc_create + fi +} + +markdownlint_file_check() { + print_info "Checking .markdownlint.json ..." + if [[ -f ".markdownlint.json" ]]; then + print_pass "Already exists, please ensure it has the correct format." + else + print_warning ".markdownlint.json file is missing." + print_task "creating ..." + markdownlint_create + fi +} + +# ----------------------------- +# πŸ”§ Pre Commit Hooks Overall Check +# ----------------------------- +pre_commit_hooks_check() { + print_info "Checking pre-commit hooks ..." + if [[ -f ".pre-commit-config.yaml" ]]; then + print_pass ".pre-commit-config.yaml already exists, please ensure it has the correct format." + print_task "Updating and installing hooks ..." + pre-commit autoupdate + pre-commit install + [[ $(grep -v '^\s*#' .pre-commit-config.yaml | grep -cE "commit-msg|commitlint") -gt 0 ]] && { + print_task "Installing commit-msg hook ..." + pre-commit install --hook-type commit-msg + commitlintrc_file_check + } + else + print_warning ".pre-commit-config.yaml is missing." + print_task "creating ..." + pre_commit_config_create + pre-commit autoupdate + pre-commit install + [[ $(grep -v '^\s*#' .pre-commit-config.yaml | grep -cE "commit-msg|commitlint") -gt 0 ]] && { + print_task "Installing commit-msg hook ..." + pre-commit install --hook-type commit-msg + commitlintrc_file_check + } + fi +} + +# ----------------------------- +# πŸš€ Execute Steps +# ----------------------------- +virtual_environment_check +pip_current_version_check +pip_latest_version_check +pip_status_check + +pre_commit_status_check +pre_commit_current_version_check +pre_commit_latest_version_check +pre_commit_version_check +pre_commit_hooks_check +markdownlint_file_check + +print_pass "πŸŽ‰ Setup Completed Successfully!"