diff --git a/.github/workflows/update-kube-stack-version.yml b/.github/workflows/update-kube-stack-version.yml index f29fe1ff2b..d7d69ebccb 100644 --- a/.github/workflows/update-kube-stack-version.yml +++ b/.github/workflows/update-kube-stack-version.yml @@ -1,10 +1,16 @@ -name: Update Kube-Stack Version +name: Update Kube Stack Version on: schedule: # Run every Monday at 9:00 AM UTC - cron: '0 9 * * 1' - workflow_dispatch: # Allow manual triggering + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run (check for updates without creating PR)' + required: false + default: false + type: boolean jobs: update-kube-stack-version: @@ -15,47 +21,86 @@ jobs: uses: actions/checkout@v5 with: token: ${{ secrets.GITHUB_TOKEN }} - + fetch-depth: 0 + - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.13' - - - name: Set up Git configuration + + - name: Install dependencies run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - - name: Run kube-stack version update script + python -m pip install --upgrade pip + pip install requests + + - name: Run update script + id: update-script run: | - cd scripts - python update_kube_stack_version.py --prepare-git - + echo "Running kube-stack-version update script..." + python scripts/update_kube_stack_version.py + echo "Script completed successfully" + - name: Check for changes - id: verify-changed-files + id: check-changes run: | - if [ -n "$(git status --porcelain)" ]; then - echo "changed=true" >> $GITHUB_OUTPUT + if git diff --quiet HEAD -- docset.yml; then + echo "No changes detected in docset.yml" + echo "has_changes=false" >> $GITHUB_OUTPUT else - echo "changed=false" >> $GITHUB_OUTPUT + echo "Changes detected in docset.yml" + echo "has_changes=true" >> $GITHUB_OUTPUT + + # Show the diff for logging + echo "Changes:" + git diff HEAD -- docset.yml fi - + - name: Create Pull Request - if: steps.verify-changed-files.outputs.changed == 'true' + if: steps.check-changes.outputs.has_changes == 'true' && github.event.inputs.dry_run != 'true' uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "chore: update kube-stack version" - title: "chore: update kube-stack version" + commit-message: 'chore: update kube-stack-version' + title: 'chore: update kube-stack-version' body: | - This PR automatically updates the kube-stack version in `docset.yml` based on the latest version from the elastic-agent repository. + This PR automatically updates the `kube-stack-version` in `docset.yml` based on the latest version from the elastic-agent repository. **Changes:** - - Updated kube-stack version in docset.yml + - Updated `kube-stack-version` to the latest value from elastic-agent repository + + **Generated by:** [Update Kube Stack Version workflow](https://github.com/${{ github.repository }}/actions/workflows/update-kube-stack-version.yml) - This PR was created automatically by the weekly kube-stack version update workflow. - branch: update-kube-stack-version-${{ github.run_id }} + This is an automated update. Please review the changes before merging. + branch: update-kube-stack-version delete-branch: true labels: | automated - documentation + chore + kube-stack-version + + - name: Dry run summary + if: github.event.inputs.dry_run == 'true' + run: | + echo "## Dry Run Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.check-changes.outputs.has_changes }}" == "true" ]; then + echo "✅ **Changes detected** - A PR would be created in a real run" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Changes that would be made:**" >> $GITHUB_STEP_SUMMARY + echo '```diff' >> $GITHUB_STEP_SUMMARY + git diff HEAD -- docset.yml >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ **No changes needed** - kube-stack-version is already up to date" >> $GITHUB_STEP_SUMMARY + fi + + - name: Summary + if: github.event.inputs.dry_run != 'true' + run: | + echo "## Update Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.check-changes.outputs.has_changes }}" == "true" ]; then + echo "✅ **PR Created** - Changes detected and pull request created" >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ **No changes needed** - kube-stack-version is already up to date" >> $GITHUB_STEP_SUMMARY + fi diff --git a/docset.yml b/docset.yml index a904166be5..ba002386d7 100644 --- a/docset.yml +++ b/docset.yml @@ -295,5 +295,5 @@ subs: intake-apis: https://www.elastic.co/docs/api/doc/observability-serverless/ models-app: "Trained Models" agent-builder: "Elastic Agent Builder" - kube-stack-version: 0.6.3 + kube-stack-version: 0.9.1 diff --git a/scripts/update_kube_stack_version.py b/scripts/update_kube_stack_version.py index b66e59e160..a7c3d0bd96 100755 --- a/scripts/update_kube_stack_version.py +++ b/scripts/update_kube_stack_version.py @@ -1,241 +1,226 @@ #!/usr/bin/env python3 """ -Kube-Stack Version Update Script - -This script automatically updates the kube-stack version in the docset.yml file -by fetching the latest collector version from elastic-agent repository tags -and then retrieving the corresponding kube-stack version. - -Usage: - python update_kube_stack_version.py [--dry-run] - -Options: - --dry-run Show what would be updated without making changes +Script to update the kube-stack-version in docset.yml based on the latest version +from the elastic-agent repository. + +This script: +1. Retrieves the latest semver from the elastic-agent repository +2. Reads the k8s.go file from the elastic-agent repository +3. Extracts the KubeStackChartVersion value +4. Updates the kube-stack-version in docset.yml """ -import urllib.request import re import sys -import argparse -import subprocess -import os -import datetime +import requests +import time from pathlib import Path +from typing import Optional -def fetch_url_content(url): - """Fetch content from a URL""" - try: - print(f"Attempting to fetch: {url}") - with urllib.request.urlopen(url, timeout=30) as response: - content = response.read().decode('utf-8') - return content - except urllib.error.URLError as e: - print(f"Failed to retrieve content: {e.reason}") - return None - except Exception as e: - print(f"Unexpected error fetching URL: {e}") - return None - - -def get_latest_collector_version(): - """Get the latest semantic version from elastic-agent repository tags""" - try: - print("Fetching latest collector version from elastic-agent repository...") - - # Run git command to get the latest semantic version tag - cmd = ['git', 'ls-remote', '--tags', 'https://github.com/elastic/elastic-agent.git'] - result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=60) - - if not result.stdout.strip(): - print("No output from git ls-remote command") - return None - - # Extract version tags and find the latest semantic version - tags = [] - for line in result.stdout.splitlines(): - if 'refs/tags/v' in line: - tag = line.split('refs/tags/')[-1] - # Match semantic version pattern (vX.Y.Z) - if re.match(r'^v[0-9]+\.[0-9]+\.[0-9]+$', tag): - tags.append(tag) - - if not tags: - print("No semantic version tags found") - return None +def get_latest_elastic_agent_version() -> str: + """ + Retrieve the latest semantic version from the elastic-agent repository by fetching all tags + and finding the highest version with retry logic. + + Returns: + str: The latest version tag (e.g., 'v8.12.0') + + Raises: + Exception: If unable to fetch version information after retries + """ + url = "https://api.github.com/repos/elastic/elastic-agent/tags" + max_retries = 3 + retry_delay = 2 # seconds + + for attempt in range(max_retries): + try: + print(f"Fetching elastic-agent tags (attempt {attempt + 1}/{max_retries})") + response = requests.get(url, timeout=30) + response.raise_for_status() - # Sort tags by version and get the latest - def version_key(tag): - # Remove 'v' prefix and split by dots - version_parts = tag[1:].split('.') - return tuple(int(part) for part in version_parts) - - latest_tag = max(tags, key=version_key) - version = latest_tag[1:] # Remove 'v' prefix - - print(f"Latest collector version: {version}") - return version - - except subprocess.TimeoutExpired: - print("Timeout while fetching tags from elastic-agent repository") - return None - except subprocess.CalledProcessError as e: - print(f"Error fetching tags from elastic-agent repository: {e}") - if e.stderr: - print(f"Error details: {e.stderr}") - return None - except Exception as e: - print(f"Error getting latest collector version: {e}") - return None + tags_data = response.json() + if not tags_data: + raise Exception("No tags found in repository") + + # Extract version tags matching pattern vX.Y.Z + version_pattern = re.compile(r'^v(\d+)\.(\d+)\.(\d+)$') + versions = [] + + for tag in tags_data: + tag_name = tag.get('name', '') + if version_pattern.match(tag_name): + # Extract version components for sorting + match = version_pattern.match(tag_name) + major, minor, patch = map(int, match.groups()) + versions.append((major, minor, patch, tag_name)) + + if not versions: + raise Exception("No valid version tags found") + + # Sort by version components and get the latest + versions.sort(key=lambda x: (x[0], x[1], x[2])) + latest_version = versions[-1][3] + + print(f"Latest elastic-agent version: {latest_version}") + return latest_version + + except requests.RequestException as e: + if attempt < max_retries - 1: + print(f"Attempt {attempt + 1} failed: {e}") + print(f"Retrying in {retry_delay} seconds...") + time.sleep(retry_delay) + retry_delay *= 2 # Exponential backoff + else: + raise Exception(f"Failed to fetch tags after {max_retries} attempts: {e}") + except Exception as e: + if attempt < max_retries - 1: + print(f"Attempt {attempt + 1} failed: {e}") + print(f"Retrying in {retry_delay} seconds...") + time.sleep(retry_delay) + retry_delay *= 2 # Exponential backoff + else: + raise Exception(f"Error retrieving version after {max_retries} attempts: {e}") -def get_collector_version(): - """Get the latest collector version from elastic-agent repository tags""" - return get_latest_collector_version() +def fetch_k8s_go_content(version: str) -> str: + """ + Fetch the content of the k8s.go file from the elastic-agent repository with retry logic. + + Args: + version (str): The version tag to fetch + + Returns: + str: The content of the k8s.go file + + Raises: + Exception: If unable to fetch the file content after retries + """ + url = f"https://raw.githubusercontent.com/elastic/elastic-agent/{version}/testing/integration/k8s/k8s.go" + max_retries = 3 + retry_delay = 2 # seconds + + for attempt in range(max_retries): + try: + print(f"Fetching k8s.go from version {version} (attempt {attempt + 1}/{max_retries})") + response = requests.get(url, timeout=30) + response.raise_for_status() + + print(f"Successfully fetched k8s.go from version {version}") + return response.text + + except requests.RequestException as e: + if attempt < max_retries - 1: + print(f"Attempt {attempt + 1} failed: {e}") + print(f"Retrying in {retry_delay} seconds...") + time.sleep(retry_delay) + retry_delay *= 2 # Exponential backoff + else: + raise Exception(f"Failed to fetch k8s.go file after {max_retries} attempts: {e}") -def get_kube_stack_version(version='main'): - """Extract KubeStackChartVersion from elastic-agent repository""" - # Try different URL formats for the k8s.go file - # First try with the version as-is (in case it already has 'v' prefix) - url = f'https://raw.githubusercontent.com/elastic/elastic-agent/{version}/testing/integration/k8s/k8s.go' - print(f"Trying k8s.go URL: {url}") - content = fetch_url_content(url) - - # If first attempt fails and version doesn't start with 'v', try with 'v' prefix - if content is None and not version.startswith('v') and version != 'main': - url = f'https://raw.githubusercontent.com/elastic/elastic-agent/v{version}/testing/integration/k8s/k8s.go' - print(f"Retrying k8s.go with URL: {url}") - content = fetch_url_content(url) +def extract_kube_stack_version(content: str) -> str: + """ + Extract the KubeStackChartVersion from the k8s.go file content. - # If that fails too, try with main branch - if content is None: - url = 'https://raw.githubusercontent.com/elastic/elastic-agent/main/testing/integration/k8s/k8s.go' - print(f"Falling back to main branch for k8s.go: {url}") - content = fetch_url_content(url) + Args: + content (str): The content of the k8s.go file + + Returns: + str: The KubeStackChartVersion value + + Raises: + Exception: If the version pattern is not found + """ + # Pattern to match KubeStackChartVersion = "version" + pattern = r'KubeStackChartVersion\s*=\s*"([^"]+)"' - if content is None: - print(f"Could not fetch k8s.go from any URL") - return None - - # Look for the KubeStackChartVersion line - lines = content.splitlines() - for line in lines: - if 'KubeStackChartVersion' in line and '=' in line: - # Extract the version from the line like: KubeStackChartVersion = "0.6.3" - match = re.search(r'KubeStackChartVersion\s*=\s*"([^"]+)"', line) - if match: - return match.group(1) + match = re.search(pattern, content) + if not match: + raise Exception("KubeStackChartVersion pattern not found in k8s.go file") - print("Could not find KubeStackChartVersion in k8s.go") - return None + version = match.group(1) + print(f"Extracted KubeStackChartVersion: {version}") + return version -def update_docset_kube_stack_version(version, docset_path, dry_run=False): - """Update the kube-stack-version substitution in docset.yml""" +def update_docset_yml(kube_stack_version: str, docset_path: Path) -> None: + """ + Update the kube-stack-version in the docset.yml file. + + Args: + kube_stack_version (str): The new version to set + docset_path (Path): Path to the docset.yml file + + Raises: + Exception: If unable to update the file + """ try: + # Read the current docset.yml content with open(docset_path, 'r', encoding='utf-8') as file: content = file.read() - # Replace the kube-stack-version line - pattern = r'(kube-stack-version:\s*)[0-9]+\.[0-9]+\.[0-9]+' - replacement = f'\\g<1>{version}' - new_content = re.sub(pattern, replacement, content) + # Pattern to match kube-stack-version: (with 2 spaces at the beginning) + pattern = r'( kube-stack-version:\s*)([^\s\n]+)' + + # Replace the version + new_content = re.sub(pattern, rf'\g<1>{kube_stack_version}', content) - if new_content != content: - if dry_run: - print(f"[DRY RUN] Would update kube-stack-version to {version} in {docset_path}") - return True + if new_content == content: + # Check if the version is already correct + current_match = re.search(pattern, content) + if current_match and current_match.group(2) == kube_stack_version: + print(f"kube-stack-version is already set to {kube_stack_version}") + return else: - with open(docset_path, 'w', encoding='utf-8') as file: - file.write(new_content) - print(f"Updated kube-stack-version to {version} in {docset_path}") - return True - else: - print(f"kube-stack-version already up to date: {version}") - return False - + raise Exception("kube-stack-version pattern not found in docset.yml") + + # Write the updated content back to the file + with open(docset_path, 'w', encoding='utf-8') as file: + file.write(new_content) + + print(f"Successfully updated kube-stack-version to {kube_stack_version} in docset.yml") + except Exception as e: - print(f"Error updating {docset_path}: {e}") - return False + raise Exception(f"Failed to update docset.yml: {e}") -def prepare_git_changes(version, dry_run=False): - """Prepare git changes for PR creation (used by GitHub Actions)""" - if dry_run: - print(f"[DRY RUN] Would prepare git changes for kube-stack version {version}") - return True - +def main(): + """Main function to orchestrate the update process.""" try: - # Get the script directory and construct paths relative to it + print("Starting kube-stack-version update process...") + + # Get the script directory and project root script_dir = Path(__file__).parent - docset_path = script_dir.parent / 'docset.yml' + project_root = script_dir.parent + docset_path = project_root / "docset.yml" + + # Verify docset.yml exists + if not docset_path.exists(): + raise Exception(f"docset.yml not found at {docset_path}") + + # Step 1: Get latest elastic-agent version + print("\n1. Retrieving latest elastic-agent version...") + latest_version = get_latest_elastic_agent_version() + + # Step 2: Fetch k8s.go content + print("\n2. Fetching k8s.go file content...") + k8s_content = fetch_k8s_go_content(latest_version) - # Add and commit changes - subprocess.run(['git', 'add', str(docset_path)], check=True) - subprocess.run(['git', 'commit', '-m', f'chore: update kube-stack version to {version} [skip ci]'], check=True) + # Step 3: Extract KubeStackChartVersion + print("\n3. Extracting KubeStackChartVersion...") + kube_stack_version = extract_kube_stack_version(k8s_content) - print(f"Git changes prepared for kube-stack version {version}") - return True + # Step 4: Update docset.yml + print("\n4. Updating docset.yml...") + update_docset_yml(kube_stack_version, docset_path) + + print(f"\n✅ Successfully updated kube-stack-version to {kube_stack_version}") - except subprocess.CalledProcessError as e: - print(f"Error preparing git changes: {e}") - return False except Exception as e: - print(f"Error preparing git changes: {e}") - return False - - -def main(): - parser = argparse.ArgumentParser(description='Update kube-stack version in docset.yml') - parser.add_argument('--dry-run', action='store_true', - help='Show what would be updated without making changes') - parser.add_argument('--prepare-git', action='store_true', default=False, - help='Prepare git changes for PR creation (used by GitHub Actions)') - args = parser.parse_args() - - # Get the script directory and construct paths relative to it - script_dir = Path(__file__).parent - docset_path = script_dir.parent / 'docset.yml' - - print(f"Using docset.yml path: {docset_path}") - - # Get the latest collector version from elastic-agent repository - col_version = get_collector_version() - if col_version is None: - print("Error: Could not determine collector version") + print(f"\n❌ Error: {e}") sys.exit(1) - - print(f"Collector version: {col_version}") - - # Get the kube-stack version from elastic-agent repository - kube_stack_version = get_kube_stack_version(col_version) - if kube_stack_version is None: - print("Error: Could not fetch kube-stack version") - sys.exit(1) - - print(f"Found kube-stack version: {kube_stack_version}") - - # Update the docset.yml file - success = update_docset_kube_stack_version(kube_stack_version, docset_path, args.dry_run) - - if success: - if args.prepare_git: - # Prepare git changes for GitHub Actions PR creation - git_success = prepare_git_changes(kube_stack_version, args.dry_run) - if git_success: - print("Kube-stack version update and git changes prepared successfully") - sys.exit(0) - else: - print("Kube-stack version updated but git preparation failed") - sys.exit(1) - else: - print("Kube-stack version update completed successfully") - sys.exit(0) - else: - print("No update was needed - kube-stack version is already up to date") - sys.exit(0) -if __name__ == '__main__': - main() +if __name__ == "__main__": + main() \ No newline at end of file