diff --git a/.githooks/README.md b/.githooks/README.md new file mode 100644 index 00000000000..7b2ed17e601 --- /dev/null +++ b/.githooks/README.md @@ -0,0 +1,47 @@ +# Git Hooks for Azure CLI Extension Development + +## Setup + +Please run the following command to enable the hooks. + +```bash +azdev setup -c {azure_cli_repo_path} -r {azure_cli_extension_repo_path} + +# if you install azdev which version is less than 0.1.84, you need to run the following command to enable the hooks +git config --local core.hooksPath .githooks +``` + +## Usage + +Every time you git commit or git push, please make sure you have activated the python environment and completed the azdev setup. + +If you want to skip the verification, you can add `--no-verify` to the git command. + +## Note + +### pre-commit + +The pre-commit hook (`pre-commit.ps1`) performs the following checks: + +1. Verifies that azdev is active in your current environment +2. Runs `azdev scan` on all staged files to detect potential secrets +3. If any secrets are detected, the commit will be blocked + - You can use `azdev mask` to remove secrets before committing + - Alternatively, use `git commit --no-verify` to bypass the check + +### pre-push + +The pre-push hooks (`pre-push.sh` for bash and `pre-push.ps1` for PowerShell) perform several quality checks: + +1. Verifies that azdev is active in your current environment +2. Confirms azure-cli is installed in editable mode +3. Checks if your branch needs rebasing against upstream/dev + - If rebasing is needed, displays instructions and provides a 5-second window to cancel +4. Runs the following quality checks on changed files: + - `azdev lint`: Checks for linting issues + - `azdev style`: Verifies code style compliance + - `azdev test`: Runs tests for modified code +5. If any check fails, the push will be blocked + - Use `git push --no-verify` to bypass these checks (not recommended) + +The hooks support both Windows (PowerShell) and Unix-like systems (Bash), automatically using the appropriate script for your environment. diff --git a/.githooks/azdev_active.ps1 b/.githooks/azdev_active.ps1 new file mode 100644 index 00000000000..b49cd26324a --- /dev/null +++ b/.githooks/azdev_active.ps1 @@ -0,0 +1,39 @@ +# Check if in the python environment +$pythonPath = (Get-Command python -ErrorAction SilentlyContinue).Path +Write-Host "PYTHON_PATH: $pythonPath" + +if (-not $pythonPath) { + Write-Host "Error: Python not found in PATH" -ForegroundColor Red + exit 1 +} + +$pythonEnvFolder = Split-Path -Parent (Split-Path -Parent $pythonPath) +$pythonActiveFile = Join-Path $pythonEnvFolder "Scripts\activate.ps1" + +if (-not (Test-Path $pythonActiveFile)) { + Write-Host "Python active file does not exist: $pythonActiveFile" -ForegroundColor Red + Write-Host "Error: Please activate the python environment first." -ForegroundColor Red + exit 1 +} + +# Construct the full path to the .azdev\env_config directory +$azdevEnvConfigFolder = Join-Path $env:USERPROFILE ".azdev\env_config" +Write-Host "AZDEV_ENV_CONFIG_FOLDER: $azdevEnvConfigFolder" + +# Check if the directory exists +if (-not (Test-Path $azdevEnvConfigFolder)) { + Write-Host "AZDEV_ENV_CONFIG_FOLDER does not exist: $azdevEnvConfigFolder" -ForegroundColor Red + Write-Host "Error: azdev environment is not completed, please run 'azdev setup' first." -ForegroundColor Red + exit 1 +} + +$configFile = Join-Path $azdevEnvConfigFolder ($pythonEnvFolder.Substring(2) + "\config") +if (-not (Test-Path $configFile)) { + Write-Host "CONFIG_FILE does not exist: $configFile" -ForegroundColor Red + Write-Host "Error: azdev environment is not completed, please run 'azdev setup' first." -ForegroundColor Red + exit 1 +} + +Write-Host "CONFIG_FILE: $configFile" + +exit 0 \ No newline at end of file diff --git a/.githooks/azdev_active.sh b/.githooks/azdev_active.sh new file mode 100644 index 00000000000..433e8caae46 --- /dev/null +++ b/.githooks/azdev_active.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Check if in the python environment +PYTHON_FILE=$(which python) +printf "PYTHON_PATH: %s\n" "$PYTHON_FILE" + +if [ -z "$PYTHON_FILE" ]; then + printf "\033[0;31mError: Python not found in PATH\033[0m\n" + exit 1 +fi + +PYTHON_ENV_FOLDER=$(dirname "$PYTHON_FILE") +PYTHON_ACTIVE_FILE="$PYTHON_ENV_FOLDER/activate" + +if [ ! -f "$PYTHON_ACTIVE_FILE" ]; then + printf "Python active file does not exist: %s\n" "$PYTHON_ACTIVE_FILE" + printf "\033[0;31mError: Please activate the python environment first.\033[0m\n" + exit 1 +fi + +# Construct the full path to the .azdev/env_config directory +AZDEV_ENV_CONFIG_FOLDER="$HOME/.azdev/env_config" +printf "AZDEV_ENV_CONFIG_FOLDER: %s\n" "$AZDEV_ENV_CONFIG_FOLDER" + +# Check if the directory exists +if [ ! -d "$AZDEV_ENV_CONFIG_FOLDER" ]; then + printf "AZDEV_ENV_CONFIG_FOLDER does not exist: %s\n" "$AZDEV_ENV_CONFIG_FOLDER" + printf "\033[0;31mError: azdev environment is not completed, please run 'azdev setup' first.\033[0m\n" + exit 1 +fi + +PYTHON_ENV_FOLDER=$(dirname "$PYTHON_ENV_FOLDER") + +CONFIG_FILE="$AZDEV_ENV_CONFIG_FOLDER${PYTHON_ENV_FOLDER}/config" +if [ ! -f "$CONFIG_FILE" ]; then + printf "CONFIG_FILE does not exist: %s\n" "$CONFIG_FILE" + printf "\033[0;31mError: azdev environment is not completed, please run 'azdev setup' first.\033[0m\n" + exit 1 +fi + +printf "CONFIG_FILE: %s\n" "$CONFIG_FILE" diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000000..a488a8455c9 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,13 @@ +#!/usr/bin/env sh +":" //; if command -v pwsh >/dev/null 2>&1; then pwsh -ExecutionPolicy Bypass -File .githooks/pre-commit.ps1; else sh .githooks/pre-commit.sh; fi; exit $? # Try PowerShell Core first, then sh on Unix +":" //; exit # Skip rest on Unix + +@echo off +powershell -NoProfile -Command "if (Get-Command powershell -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }" +if %errorlevel% equ 0 ( + powershell -ExecutionPolicy Bypass -File .githooks\pre-commit.ps1 +) else ( + echo Error: PowerShell is not available. Please install PowerShell. + exit /b 1 +) +exit /b %errorlevel% diff --git a/.githooks/pre-commit.ps1 b/.githooks/pre-commit.ps1 new file mode 100644 index 00000000000..33af0e1eb1f --- /dev/null +++ b/.githooks/pre-commit.ps1 @@ -0,0 +1,43 @@ +#!/usr/bin/env pwsh +Write-Host "Running pre-commit hook in powershell..." -ForegroundColor Green + +# run azdev_active script +$scriptPath = Join-Path $PSScriptRoot "azdev_active.ps1" +. $scriptPath +if ($LASTEXITCODE -ne 0) { + exit 1 +} + +# Run command azdev scan +Write-Host "Running azdev scan..." -ForegroundColor Green + +# Check if we have a previous commit to compare against +if (git rev-parse --verify HEAD 2>$null) { + Write-Host "Using HEAD as the previous commit" + $against = "HEAD" +} +else { + # Initial commit: diff against an empty tree object + Write-Host "Using empty tree object as the previous commit" + $against = $(git hash-object -t tree /dev/null) +} + +$hasSecrets = 0 +$files = $(git diff --cached --name-only --diff-filter=AM $against) + +foreach ($file in $files) { + # Check if the file contains secrets + $detected = $(azdev scan -f $file | ConvertFrom-Json).secrets_detected + if ($detected -eq "True") { + Write-Host "Detected secrets from $file. You can run 'azdev mask' to remove secrets before commit." -ForegroundColor Red + $hasSecrets = 1 + } +} + +if ($hasSecrets -eq 1) { + Write-Host "Secret detected. If you want to skip that, run add '--no-verify' in the end of 'git commit' command." -ForegroundColor Red + exit 1 +} + +Write-Host "Pre-commit hook passed." -ForegroundColor Green +exit 0 diff --git a/.githooks/pre-commit.sh b/.githooks/pre-commit.sh new file mode 100644 index 00000000000..7a69ad338b5 --- /dev/null +++ b/.githooks/pre-commit.sh @@ -0,0 +1,38 @@ +#!/bin/bash +printf "\033[0;32mRunning pre-commit hook in bash ...\033[0m\n" + +# run azdev_active script +SCRIPT_PATH="$(dirname "$0")/azdev_active.sh" +. "$SCRIPT_PATH" +if [ $? -ne 0 ]; then + exit 1 +fi + +# Run command azdev scan +printf "\033[0;32mRunning azdev scan...\033[0m\n" + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + printf "Using HEAD as the previous commit\n" + against=HEAD +else + printf "Using empty tree object as the previous commit\n" + against=$(git hash-object -t tree /dev/null) +fi +has_secrets=0 +for FILE in `git diff --cached --name-only --diff-filter=AM $against` ; do + # Check if the file contains secrets + detected=$(azdev scan -f "$FILE" | python -c "import sys, json; print(json.load(sys.stdin)['secrets_detected'])") + if [ "$detected" = "True" ]; then + printf "\033[0;31mDetected secrets from %s, You can run 'azdev mask' to remove secrets before commit.\033[0m\n" "$FILE" + has_secrets=1 + fi +done + +if [ $has_secrets -eq 1 ]; then + printf "\033[0;31mSecret detected. If you want to skip that, run add '--no-verify' in the end of 'git commit' command.\033[0m\n" + exit 1 +fi + +printf "\033[0;32mPre-commit hook passed.\033[0m\n" +exit 0 diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 00000000000..7cc1783291a --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,13 @@ +#!/usr/bin/env sh +":" //; if command -v pwsh >/dev/null 2>&1; then pwsh -ExecutionPolicy Bypass -File .githooks/pre-push.ps1; else sh .githooks/pre-push.sh; fi; exit $? # Try PowerShell Core first, then sh on Unix +":" //; exit # Skip rest on Unix + +@echo off +powershell -NoProfile -Command "if (Get-Command powershell -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }" +if %errorlevel% equ 0 ( + powershell -ExecutionPolicy Bypass -File .githooks\pre-push.ps1 +) else ( + echo Error: PowerShell is not available. Please install PowerShell. + exit /b 1 +) +exit /b %errorlevel% diff --git a/.githooks/pre-push.ps1 b/.githooks/pre-push.ps1 new file mode 100644 index 00000000000..329e4f9053f --- /dev/null +++ b/.githooks/pre-push.ps1 @@ -0,0 +1,109 @@ +Write-Host "Running pre-push hook in powershell..." -ForegroundColor Green + +# run azdev_active script and capture its output +$scriptPath = Join-Path $PSScriptRoot "azdev_active.ps1" +. $scriptPath +if ($LASTEXITCODE -ne 0) { + exit 1 +} + +# Check if azure-cli is installed in editable mode +$pipShowOutput = pip show azure-cli 2>&1 +$editableLocation = if ($pipShowOutput) { + $match = $pipShowOutput | Select-String "Editable project location: (.+)" + if ($match) { + $match.Matches.Groups[1].Value + } +} +if ($editableLocation) { + # get the parent of parent directory of the editable location + $AZURE_CLI_FOLDER = Split-Path -Parent (Split-Path -Parent $editableLocation) +} else { + Write-Host "Error: azure-cli is not installed in editable mode. Please install it in editable mode using `azdev setup`." -ForegroundColor Red + exit 1 +} + +# Get extension repo paths and join them with spaces +$Extensions = (azdev extension repo list -o tsv) -join ' ' + +# Fetch upstream/dev branch +Write-Host "Fetching upstream/dev branch..." -ForegroundColor Green +git fetch upstream dev +if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Failed to fetch upstream/dev branch. Please run 'git remote add upstream https://github.com/Azure/azure-cli.git' first." -ForegroundColor Red + exit 1 +} + +# Check if current branch needs rebasing +$mergeBase = git merge-base HEAD upstream/dev +$upstreamHead = git rev-parse upstream/dev +if ($mergeBase -ne $upstreamHead) { + Write-Host "" + Write-Host "Your branch is not up to date with upstream/dev. Please run the following commands to rebase and setup:" -ForegroundColor Yellow + Write-Host "+++++++++++++++++++++++++++++++++++++++++++++++++++++++" -ForegroundColor Yellow + Write-Host "git rebase upstream/dev" -ForegroundColor Yellow + if ($Extensions) { + Write-Host "azdev setup -c $AZURE_CLI_FOLDER -r $Extensions" -ForegroundColor Yellow + } else { + Write-Host "azdev setup -c $AZURE_CLI_FOLDER" -ForegroundColor Yellow + } + Write-Host "+++++++++++++++++++++++++++++++++++++++++++++++++++++++" -ForegroundColor Yellow + Write-Host "" + Write-Host "You have 5 seconds to stop the push (Ctrl+C)..." -ForegroundColor Yellow + for ($i = 5; $i -gt 0; $i--) { + Write-Host "`rTime remaining: $i seconds..." -NoNewline -ForegroundColor Yellow + Start-Sleep -Seconds 1 + } + Write-Host "`rContinuing without rebase..." +} + +# get the current branch name +$currentBranch = git branch --show-current + +# Run command azdev lint +Write-Host "Running azdev lint..." -ForegroundColor Green +azdev linter --min-severity medium --repo ./ --src $currentBranch --tgt $mergeBase +if ($LASTEXITCODE -ne 0) { + Write-Host "Error: azdev lint check failed." -ForegroundColor Red + exit 1 +} + +# Run command azdev style +Write-Host "Running azdev style..." -ForegroundColor Green +azdev style --repo ./ --src $currentBranch --tgt $mergeBase +if ($LASTEXITCODE -ne 0) { + $error_msg = azdev style --repo ./ --src $currentBranch --tgt $mergeBase 2>&1 + if ($error_msg -like "*No modules*") { + Write-Host "Pre-push hook passed." -ForegroundColor Green + exit 0 + } + Write-Host "Error: azdev style check failed." -ForegroundColor Red + exit 1 +} + +# Run command azdev test +Write-Host "Running azdev test..." -ForegroundColor Green +azdev test --repo ./ --src $currentBranch --tgt $mergeBase --discover --no-exitfirst --xml-path test_results.xml 2>$null +if ($LASTEXITCODE -ne 0) { + Write-Host "Error: azdev test check failed." -ForegroundColor Red + exit 1 +} else { + # remove test_results.xml file + Remove-Item test_results.xml +} + +Write-Host "Pre-push hook passed." -ForegroundColor Green + +if ($mergeBase -ne $upstreamHead) { + Write-Host "" + Write-Host "Your branch is not up to date with upstream/dev. Please run the following commands to rebase code and setup:" -ForegroundColor Yellow + Write-Host "+++++++++++++++++++++++++++++++++++++++++++++++++++++++" -ForegroundColor Yellow + Write-Host "git rebase upstream/dev" -ForegroundColor Yellow + if ($Extensions) { + Write-Host "azdev setup -c $AZURE_CLI_FOLDER -r $Extensions" -ForegroundColor Yellow + } else { + Write-Host "azdev setup -c $AZURE_CLI_FOLDER" -ForegroundColor Yellow + } + Write-Host "+++++++++++++++++++++++++++++++++++++++++++++++++++++++" -ForegroundColor Yellow +} +exit 0 diff --git a/.githooks/pre-push.sh b/.githooks/pre-push.sh new file mode 100644 index 00000000000..19362cddd2e --- /dev/null +++ b/.githooks/pre-push.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +printf "\033[0;32mRunning pre-push hook in bash ...\033[0m\n" + +# run azdev_active script and save its output +SCRIPT_PATH="$(dirname "$0")/azdev_active.sh" +. "$SCRIPT_PATH" +if [ $? -ne 0 ]; then + exit 1 +fi + +# Check if azure-cli is installed in editable mode +PIP_SHOW_OUTPUT=$(pip show azure-cli 2>&1) +if echo "$PIP_SHOW_OUTPUT" | grep -q "Editable project location:"; then + EDITABLE_LOCATION=$(echo "$PIP_SHOW_OUTPUT" | grep "Editable project location:" | sed 's/Editable project location: //') + # get the parent of parent directory of the editable location + AZURE_CLI_FOLDER=$(dirname "$(dirname "$EDITABLE_LOCATION")") +else + printf "\033[0;31mError: azure-cli is not installed in editable mode. Please install it in editable mode using `azdev setup`.\033[0m\n" + exit 1 +fi + +# Get extension repo paths and join them with spaces +EXTENSIONS=$(azdev extension repo list -o tsv | tr '\n' ' ') + +# Fetch upstream/dev branch +printf "\033[0;32mFetching upstream/dev branch...\033[0m\n" +git fetch upstream dev +if [ $? -ne 0 ]; then + printf "\033[0;31mError: Failed to fetch upstream/dev branch. Please run 'git remote add upstream https://github.com/Azure/azure-cli.git' first.\033[0m\n" + exit 1 +fi + +# Check if current branch needs rebasing +MERGE_BASE=$(git merge-base HEAD upstream/dev) +UPSTREAM_HEAD=$(git rev-parse upstream/dev) + +if [ "$MERGE_BASE" != "$UPSTREAM_HEAD" ]; then + printf "\n" + printf "\033[1;33mYour branch is not up to date with upstream/dev. Please run the following commands to rebase and setup:\033[0m\n" + printf "\033[1;33m+++++++++++++++++++++++++++++++++++++++++++++++++++++++\033[0m\n" + printf "\033[1;33mgit rebase upstream/dev\033[0m\n" + + # Get extension repo paths + EXTENSIONS=$(azdev extension repo list -o tsv | tr '\n' ' ') + if [ -n "$EXTENSIONS" ]; then + printf "\033[1;33mazdev setup -c %s -r %s\033[0m\n" "$AZURE_CLI_FOLDER" "$EXTENSIONS" + else + printf "\033[1;33mazdev setup -c %s\033[0m\n" "$AZURE_CLI_FOLDER" + fi + printf "\033[1;33m+++++++++++++++++++++++++++++++++++++++++++++++++++++++\033[0m\n" + printf "\n" + printf "\033[1;33mYou have 5 seconds to stop the push (Ctrl+C)...\033[0m\n" + + # Using a C-style for loop instead of seq + i=5 + while [ $i -ge 1 ]; do + printf "\r\033[K\033[1;33mTime remaining: %d seconds...\033[0m" $i + sleep 1 + i=$((i-1)) + done + printf "\r\033[K\033[1;33mContinuing without rebase...\033[0m\n" +fi + +# get the current branch name +CURRENT_BRANCH=$(git branch --show-current) + +# Run command azdev lint +printf "\033[0;32mRunning azdev lint...\033[0m\n" +azdev linter --min-severity medium --repo ./ --src $CURRENT_BRANCH --tgt $MERGE_BASE +if [ $? -ne 0 ]; then + printf "\033[0;31mError: azdev lint check failed.\033[0m\n" + exit 1 +fi + +# Run command azdev style +printf "\033[0;32mRunning azdev style...\033[0m\n" +azdev style --repo ./ --src $CURRENT_BRANCH --tgt $MERGE_BASE +if [ $? -ne 0 ]; then + error_msg=$(azdev style --repo ./ --src $CURRENT_BRANCH --tgt $MERGE_BASE 2>&1) + if echo "$error_msg" | grep -q "No modules"; then + printf "\033[0;32mPre-push hook passed.\033[0m\n" + exit 0 + fi + printf "\033[0;31mError: azdev style check failed.\033[0m\n" + exit 1 +fi + +# Run command azdev test +printf "\033[0;32mRunning azdev test...\033[0m\n" +azdev test --repo ./ --src $CURRENT_BRANCH --tgt $MERGE_BASE --discover --no-exitfirst --xml-path test_results.xml 2>/dev/null +if [ $? -ne 0 ]; then + printf "\033[0;31mError: azdev test check failed.\033[0m\n" + exit 1 +else + # remove the test_results.xml file + rm -f test_results.xml +fi + +printf "\033[0;32mPre-push hook passed.\033[0m\n" + +if [ "$MERGE_BASE" != "$UPSTREAM_HEAD" ]; then + printf "\n" + printf "\033[1;33mYour branch is not up to date with upstream/dev. Please run the following commands to rebase and setup:\033[0m\n" + printf "\033[1;33m+++++++++++++++++++++++++++++++++++++++++++++++++++++++\033[0m\n" + printf "\033[1;33mgit rebase upstream/dev\033[0m\n" + + # Get extension repo paths + EXTENSIONS=$(azdev extension repo list -o tsv | tr '\n' ' ') + if [ -n "$EXTENSIONS" ]; then + printf "\033[1;33mazdev setup -c %s -r %s\033[0m\n" "$AZURE_CLI_FOLDER" "$EXTENSIONS" + else + printf "\033[1;33mazdev setup -c %s\033[0m\n" "$AZURE_CLI_FOLDER" + fi + printf "\033[1;33m+++++++++++++++++++++++++++++++++++++++++++++++++++++++\033[0m\n" +fi +exit 0 + diff --git a/.gitignore b/.gitignore index 8fa1d3acf5e..9c6b9c0edd0 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,6 @@ az_command_coverage.txt #cmd coverage cmd_coverage/* + +# Ignore test results +test_results.xml