From 13dfd516d46b09e5ea690bcd058ff9535d2cabcc Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Tue, 3 Mar 2026 08:36:34 -0500 Subject: [PATCH] Validate plugin version update with changes --- .github/workflows/validate-plugins.yml | 33 +++- scripts/README.md | 56 ++++++ scripts/validate-version-bump.sh | 262 +++++++++++++++++++++++++ 3 files changed, 348 insertions(+), 3 deletions(-) create mode 100755 scripts/validate-version-bump.sh diff --git a/.github/workflows/validate-plugins.yml b/.github/workflows/validate-plugins.yml index 6742170..ebc3c93 100644 --- a/.github/workflows/validate-plugins.yml +++ b/.github/workflows/validate-plugins.yml @@ -110,6 +110,20 @@ jobs: read -ra PLUGINS_ARRAY <<< "$CHANGED_PLUGINS" ./scripts/validate-marketplace.sh "${PLUGINS_ARRAY[@]}" 2>&1 | tee /tmp/marketplace-validation.log + - name: Validate version bump + id: version-bump + if: steps.changed-files.outputs.has_components == 'true' + continue-on-error: true + env: + BASE_REF: ${{ github.base_ref }} + CHANGED_PLUGINS: ${{ steps.changed-files.outputs.changed_plugins }} + run: | + set -o pipefail + git fetch origin "$BASE_REF" + echo "Checking version bumps for plugins with component changes: $CHANGED_PLUGINS" + read -ra PLUGINS_ARRAY <<< "$CHANGED_PLUGINS" + ./scripts/validate-version-bump.sh "origin/$BASE_REF" "${PLUGINS_ARRAY[@]}" 2>&1 | tee /tmp/version-bump-validation.log + - name: Log in to Azure if: steps.changed-files.outputs.has_components == 'true' uses: bitwarden/gh-actions/azure-login@main @@ -217,6 +231,7 @@ jobs: SHOULD_RUN: ${{ steps.relevance.outputs.should_run }} STRUCTURE_RESULT: ${{ steps.structure.outcome }} MARKETPLACE_RESULT: ${{ steps.marketplace.outcome }} + VERSION_BUMP_RESULT: ${{ steps.version-bump.outcome }} COMPONENTS_RESULT: ${{ steps.components.outcome }} HAS_COMPONENTS: ${{ steps.changed-files.outputs.has_components }} CHANGED_PLUGINS: ${{ steps.changed-files.outputs.changed_plugins }} @@ -240,8 +255,10 @@ jobs: echo "Marketplace validation: skipped (no plugin files changed)" fi if [[ "$HAS_COMPONENTS" == "true" ]]; then + echo "Version bump validation: $VERSION_BUMP_RESULT" echo "Component & security validation (AI-driven): $COMPONENTS_RESULT" else + echo "Version bump validation: skipped (no component files changed)" echo "Component & security validation (AI-driven): skipped (no component files changed)" fi echo "" @@ -263,6 +280,14 @@ jobs: echo "" fi + if [[ -f /tmp/version-bump-validation.log ]] && [[ "$VERSION_BUMP_RESULT" == "failure" ]]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "📋 Version Bump Validation Details:" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + cat /tmp/version-bump-validation.log + echo "" + fi + if [[ "$COMPONENTS_RESULT" == "failure" ]]; then echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "📋 Component & Security Validation Details:" @@ -283,9 +308,11 @@ jobs: fi fi - # Only fail on components if it actually ran and failed - if [[ "$HAS_COMPONENTS" == "true" ]] && [[ "$COMPONENTS_RESULT" == "failure" ]]; then - FAILED=true + # Only fail on version bump / components if they actually ran and failed + if [[ "$HAS_COMPONENTS" == "true" ]]; then + if [[ "$VERSION_BUMP_RESULT" == "failure" ]] || [[ "$COMPONENTS_RESULT" == "failure" ]]; then + FAILED=true + fi fi if [[ "$FAILED" == "true" ]]; then diff --git a/scripts/README.md b/scripts/README.md index d8f8d1e..8faae6e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -7,6 +7,7 @@ Utility scripts for maintaining the Bitwarden AI Plugins Marketplace. - [bump-plugin-version.sh](#bump-plugin-versionsh) - Automate version bumping - [validate-plugin-structure.sh](#validate-plugin-structuresh) - Validate plugin structure - [validate-marketplace.sh](#validate-marketplacesh) - Validate marketplace.json +- [validate-version-bump.sh](#validate-version-bumpsh) - Validate version bumps for changed plugins - [GitHub Actions Integration](#github-actions-integration) - [Local Development Workflow](#local-development-workflow) @@ -216,6 +217,54 @@ Total errors found: 0 --- +## validate-version-bump.sh + +Validates that changed plugins include a version bump and changelog update. + +### Usage + +```bash +# Compare against a base branch +./scripts/validate-version-bump.sh origin/main plugin-name + +# Check multiple plugins +./scripts/validate-version-bump.sh origin/main bitwarden-code-review bitwarden-software-engineer + +# Accept plugins/ path prefix +./scripts/validate-version-bump.sh origin/main plugins/bitwarden-code-review +``` + +### Checks Performed + +- **Version bump** - Compares the version in `plugin.json` between the base ref and HEAD. The new version must be strictly greater than the base version (any of major, minor, or patch). +- **Changelog update** - Verifies that `CHANGELOG.md` was modified in the diff between the base ref and HEAD. + +### Exit Codes + +- `0` - All plugins have proper version bumps +- `1` - One or more plugins are missing a version bump or changelog update + +### Example Output + +``` +🔍 Validating version bumps for changed plugins... + +📦 Checking bitwarden-software-engineer... + ❌ bitwarden-software-engineer: Version not bumped (still 0.3.0). Plugin component files were changed — run ./scripts/bump-plugin-version.sh bitwarden-software-engineer + ❌ bitwarden-software-engineer: CHANGELOG.md not updated. Add an entry describing what changed. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +📊 Version Bump Validation Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Plugins checked: 1 +Total errors: 2 + +❌ Version bump validation failed with 2 error(s) +``` + +--- + ## Plugin Requirements Reference ### Required Files @@ -313,6 +362,13 @@ description: Skill description - `AGENT.md` files (if agents exist) - Use the version bump script: `./scripts/bump-plugin-version.sh plugin-name X.Y.Z` +**"Version not bumped" / "CHANGELOG.md not updated"** + +- Plugin component files (agents, skills, hooks) were changed without a version bump +- Run `./scripts/bump-plugin-version.sh ` to bump all version files +- Add a changelog entry under the appropriate category (Added, Changed, Fixed, etc.) +- Commit the version bump alongside your code changes + **"Missing YAML frontmatter"** - Ensure files start with `---` and end with `---` diff --git a/scripts/validate-version-bump.sh b/scripts/validate-version-bump.sh new file mode 100755 index 0000000..74cc7b1 --- /dev/null +++ b/scripts/validate-version-bump.sh @@ -0,0 +1,262 @@ +#!/usr/bin/env bash +# +# Validate that changed plugins include a version bump. +# +# When plugin component files (agents, skills, hooks) are modified, the plugin +# version must be bumped in plugin.json and the CHANGELOG.md must be updated. +# This enforces the repository policy that all plugin changes include a version +# bump and changelog entry. +# +# Usage: +# ./validate-version-bump.sh plugin1 [plugin2 ...] +# ./validate-version-bump.sh origin/main plugins/plugin1 plugins/plugin2 +# +# Arguments: +# base-ref Git ref to compare against (e.g., origin/main) +# plugin... One or more plugin names or paths to check + +set -uo pipefail + +# Colors for output +RED='\033[91m' +GREEN='\033[92m' +YELLOW='\033[93m' +BLUE='\033[94m' +BOLD='\033[1m' +RESET='\033[0m' + +# Counters +TOTAL_ERRORS=0 + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +PLUGINS_DIR="$REPO_ROOT/plugins" + +# Source shared path sanitization library +source "$SCRIPT_DIR/lib/path-sanitization.sh" + +# Function to print colored output +print_header() { + echo -e "${BOLD}$1${RESET}" +} + +print_section() { + echo -e "${BLUE}$1${RESET}" +} + +print_success() { + echo -e " ${GREEN}✅ $1${RESET}" +} + +print_error() { + echo -e " ${RED}❌ $1${RESET}" + ((TOTAL_ERRORS++)) +} + +print_warning() { + echo -e " ${YELLOW}⚠️ $1${RESET}" +} + +# Function to extract version from plugin.json at a given git ref +# Args: +# $1 - git ref (e.g., origin/main) +# $2 - plugin name +# Returns: +# Version string on stdout, or empty if not found +get_version_at_ref() { + local ref="$1" + local plugin_name="$2" + local plugin_json_path="plugins/$plugin_name/.claude-plugin/plugin.json" + + # Try to read the file at the given ref + local content + if content=$(git show "$ref:$plugin_json_path" 2>/dev/null); then + echo "$content" | jq -r '.version // empty' 2>/dev/null + fi +} + +# Function to check if CHANGELOG.md was modified for a plugin +# Args: +# $1 - base ref +# $2 - plugin name +# Returns: +# 0 if modified, 1 if not +changelog_was_modified() { + local base_ref="$1" + local plugin_name="$2" + local changelog_path="plugins/$plugin_name/CHANGELOG.md" + + git diff --name-only "$base_ref...HEAD" -- "$changelog_path" | grep -q . +} + +# Function to compare semver strings +# Returns 0 if new_version > old_version, 1 otherwise +is_version_bumped() { + local old_version="$1" + local new_version="$2" + + if [[ "$old_version" == "$new_version" ]]; then + return 1 + fi + + # Split versions into components + local old_major old_minor old_patch + IFS='.' read -r old_major old_minor old_patch <<< "$old_version" + local new_major new_minor new_patch + IFS='.' read -r new_major new_minor new_patch <<< "$new_version" + + # Compare major.minor.patch + if (( new_major > old_major )); then + return 0 + elif (( new_major == old_major )); then + if (( new_minor > old_minor )); then + return 0 + elif (( new_minor == old_minor )); then + if (( new_patch > old_patch )); then + return 0 + fi + fi + fi + + return 1 +} + +# Function to validate version bump for a single plugin +validate_plugin_version_bump() { + local base_ref="$1" + local plugin_name="$2" + local has_errors=0 + + # Get version at base ref + local base_version + base_version=$(get_version_at_ref "$base_ref" "$plugin_name") + + # Get current version + local plugin_json="$PLUGINS_DIR/$plugin_name/.claude-plugin/plugin.json" + local current_version="" + if [[ -f "$plugin_json" ]]; then + current_version=$(jq -r '.version // empty' "$plugin_json" 2>/dev/null) + fi + + if [[ -z "$base_version" ]]; then + # New plugin — no base version to compare against + print_success "$plugin_name: New plugin (no base version to compare)" + return 0 + fi + + if [[ -z "$current_version" ]]; then + print_error "$plugin_name: Could not read current version from plugin.json" + return 1 + fi + + # Check if version was bumped + if is_version_bumped "$base_version" "$current_version"; then + print_success "$plugin_name: Version bumped $base_version -> $current_version" + else + print_error "$plugin_name: Version not bumped (still $base_version). Plugin component files were changed — run ./scripts/bump-plugin-version.sh $plugin_name " + has_errors=1 + fi + + # Check if CHANGELOG.md was updated + if changelog_was_modified "$base_ref" "$plugin_name"; then + print_success "$plugin_name: CHANGELOG.md updated" + else + print_error "$plugin_name: CHANGELOG.md not updated. Add an entry describing what changed." + has_errors=1 + fi + + return $has_errors +} + +# Main execution +main() { + print_header "🔍 Validating version bumps for changed plugins..." + echo "" + + if [[ $# -lt 2 ]]; then + echo -e "${RED}Usage: $0 [plugin2 ...]${RESET}" + echo "" + echo "Arguments:" + echo " base-ref Git ref to compare against (e.g., origin/main)" + echo " plugin... One or more plugin names or plugins/ paths" + exit 1 + fi + + local base_ref="$1" + shift + + # Validate base ref exists + if ! git rev-parse --verify "$base_ref" >/dev/null 2>&1; then + echo -e "${RED}❌ Invalid base ref: $base_ref${RESET}" + exit 1 + fi + + # Parse plugin arguments + local plugins=() + for arg in "$@"; do + local plugin_name="" + + # Extract plugin name from path formats like "plugins/foo" or just "foo" + arg="${arg#./}" + if [[ "$arg" =~ ^plugins/([a-zA-Z0-9_-]+)$ ]]; then + plugin_name="${BASH_REMATCH[1]}" + elif [[ "$arg" =~ ^[a-zA-Z0-9_-]+$ ]]; then + plugin_name="$arg" + else + print_warning "Skipping invalid plugin argument: $arg" + continue + fi + + if [[ -d "$PLUGINS_DIR/$plugin_name" ]]; then + plugins+=("$plugin_name") + else + print_warning "Plugin directory not found: $plugin_name" + fi + done + + # Remove duplicates + if [[ "${#plugins[@]}" -gt 0 ]]; then + array_from_lines plugins < <(printf '%s\n' "${plugins[@]}" | sort -u) + fi + + if [[ "${#plugins[@]}" -eq 0 ]]; then + echo -e "${YELLOW}⚠️ No valid plugins to check${RESET}" + exit 0 + fi + + # Validate each plugin + for plugin_name in "${plugins[@]}"; do + print_section "📦 Checking $plugin_name..." + validate_plugin_version_bump "$base_ref" "$plugin_name" || true + echo "" + done + + # Print summary + print_header "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_header "📊 Version Bump Validation Summary" + print_header "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Plugins checked: ${#plugins[@]}" + echo "Total errors: $TOTAL_ERRORS" + echo "" + + if [[ $TOTAL_ERRORS -eq 0 ]]; then + echo -e "${GREEN}✅ All changed plugins have proper version bumps${RESET}" + exit 0 + else + echo -e "${RED}❌ Version bump validation failed with $TOTAL_ERRORS error(s)${RESET}" + echo "" + echo -e "${YELLOW}To fix these issues:${RESET}" + echo "1. Determine the appropriate version bump (major, minor, or patch)" + echo "2. Run: ./scripts/bump-plugin-version.sh " + echo "3. Add a changelog entry under the appropriate category" + echo "4. Commit the version bump alongside your changes" + echo "" + echo -e "${YELLOW}For more information, see: scripts/README.md${RESET}" + exit 1 + fi +} + +# Run main function +main "$@"