From 4dd8699f2a343b543e680964e7d5258e81969969 Mon Sep 17 00:00:00 2001 From: Marco Braga Date: Mon, 8 Dec 2025 11:58:43 -0300 Subject: [PATCH 1/5] NO-JIRA: fix/CVE-2025-58058: github.com/ulikunitz/xz (#199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://quay.io/repository/opct/opct/manifest/sha256:0cb8605376c996718c1f17ae9546a9995e3a697330b9dbfd3d9bb4115664ef0e?tab=vulnerabilities&fixable=true https://github.com/ulikunitz/xz/security/advisories/GHSA-jc7w-c686-c4v9 **What this PR does / why we need it**: **Which issue(s) this PR fixes** *(optional, use `fixes #(, fixes #, ...)` format, where issue_number might be a GitHub issue, or a Jira story*: Fixes # **Checklist** - [ ] Subject and description added to both, commit and PR. - [x] Relevant issues have been referenced. - [ ] This change includes docs. - [ ] This change includes unit tests. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6ebabbb9..870f67b2 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 - github.com/ulikunitz/xz v0.5.12 + github.com/ulikunitz/xz v0.5.15 github.com/vmware-tanzu/sonobuoy v0.57.3 golang.org/x/sync v0.12.0 k8s.io/api v0.34.1 diff --git a/go.sum b/go.sum index 29d07b6c..933e2724 100644 --- a/go.sum +++ b/go.sum @@ -168,8 +168,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= -github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vmware-tanzu/sonobuoy v0.57.3 h1:9M2SkUDP6JXE1lBYCrqnBOTl35A89Sh/LvON+Hmwwrg= github.com/vmware-tanzu/sonobuoy v0.57.3/go.mod h1:O97H72QEWgbMDmItVB4+rg5ZNxxKN+FvBe5tB2QhmuA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= From 84905ae160c24e94643a58bbabd33d7b65dd0258 Mon Sep 17 00:00:00 2001 From: Marco Braga Date: Mon, 8 Dec 2025 16:23:20 -0300 Subject: [PATCH 2/5] NO-JIRA: docs: update CLAUDE.md with v0.6.1 release lessons learned (#200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR updates CLAUDE.md to reflect lessons learned from the v0.6.1 release process and removes references to the broken auto-release-tag workflow. ## Changes **Removed**: - `Improved PR-Based Release Workflow` section (lines 854-1082) - References to `.github/workflows/auto-release-tag.yaml` from release checklist - Automated tag creation workflow file (already deleted in main) **Updated**: - Release checklist to reflect manual tag creation process - Last Updated date to 2025-12-08 **Added**: - **Lessons Learned from v0.6.1 Release** section documenting: - What went wrong: Automated workflow issues, golangci-lint-action@v7 compatibility - What worked: Manual tags, rebase workflow, plugin releases - Key takeaways: Keep it simple, don't change what works - Recommended approach: Manual tag creation - **What's Next** section with: - Short-term improvements (fix golangci-lint, standardize formats) - Medium-term improvements (CI stability, version management) - Long-term opportunities (release tooling, testing, documentation) - Anti-patterns to avoid based on v0.6.1 experience ## Rationale The auto-release-tag workflow proved unreliable during v0.6.1 release: - Complex workflow was difficult to debug when it failed - Failed silently when PR description didn't match expected format - Required specific PR format that was easy to forget - Blocked release for several hours during troubleshooting Manual tag creation is: - Simple and predictable - Easy to troubleshoot - Provides full control over changelog content - Works consistently for both CLI and Plugins repositories For infrequent tasks like releases (~monthly), manual processes are more maintainable than complex automation. ## Testing - [x] CLAUDE.md follows existing formatting standards - [x] All auto-release-tag references removed - [x] Manual release process documented clearly - [x] Lessons learned capture key issues and solutions πŸ€– Claude Code Assistant --------- Co-authored-by: Claude --- .github/workflows/auto-release-tag.yaml | 279 ---------------- .github/workflows/go.yaml | 204 ++++++------ .github/workflows/pre_reviewer.yaml | 2 +- CLAUDE.md | 413 +++++++++++------------- 4 files changed, 284 insertions(+), 614 deletions(-) delete mode 100644 .github/workflows/auto-release-tag.yaml diff --git a/.github/workflows/auto-release-tag.yaml b/.github/workflows/auto-release-tag.yaml deleted file mode 100644 index 39625748..00000000 --- a/.github/workflows/auto-release-tag.yaml +++ /dev/null @@ -1,279 +0,0 @@ -name: Automatic Release Tag Creation - -# This workflow automates the creation of release tags when a release PR is merged. -# -# Guardrails: -# 1. Only runs when PR targets release-* branch -# 2. Source branch must be 'main' -# 3. Only users in OWNERS (approvers list) can create release PRs -# 4. Tag only created when PR is merged (not just closed) -# 5. Version format must be vX.Y.Z -# 6. Prevents duplicate tags -# -# Usage: -# 1. Create PR from main to release-X.Y -# 2. Add "Release: vX.Y.Z" to PR description -# 3. Merge PR β†’ Tag is automatically created -# -# Manual trigger (for recovery): -# gh workflow run auto-release-tag.yaml -f version=v0.6.1 -f release_branch=release-0.6 - -on: - # Trigger 1: When release PR is merged - pull_request: - types: [closed] - branches: - - 'release-**' # Matches release-0.6, release-v0.6, etc. - - # Trigger 2: Manual dispatch for recovery - workflow_dispatch: - inputs: - version: - description: 'Version to release (e.g., v0.6.1)' - required: true - type: string - release_branch: - description: 'Release branch (e.g., release-0.6)' - required: true - type: string - -jobs: - validate-and-create-tag: - # Only run if PR was merged (not closed without merge) - if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: read - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Need full history for changelog - ref: ${{ github.event.pull_request.base.ref || inputs.release_branch }} - - - name: Parse version from PR body or input - id: parse_version - env: - PR_BODY: ${{ github.event.pull_request.body }} - PR_TITLE: ${{ github.event.pull_request.title }} - EVENT_NAME: ${{ github.event_name }} - INPUT_VERSION: ${{ inputs.version }} - run: | - if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then - VERSION="$INPUT_VERSION" - else - # Extract version from PR body (expects: Release: vX.Y.Z or Release: vX.Y.Z-prerelease) - # SemVer regex: v + MAJOR.MINOR.PATCH + optional pre-release (alpha, beta, rc) + optional build metadata - # Use environment variable to safely handle PR body (prevents script injection) - VERSION=$(echo "$PR_BODY" | grep -oP 'Release:\s*\Kv[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?' || echo "") - - # Fallback: try PR title - if [ -z "$VERSION" ]; then - VERSION=$(echo "$PR_TITLE" | grep -oP 'v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?' || echo "") - fi - fi - - if [ -z "$VERSION" ]; then - echo "Error: Could not parse version from PR. Add 'Release: vX.Y.Z' to PR description." - echo "Supported formats: v0.6.1, v0.6.1-rc.0, v0.6.1-alpha.1, v0.6.1-beta.2+build.123" - exit 1 - fi - - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "πŸ“¦ Detected version: $VERSION" - - - name: Validate PR source and target branches - if: github.event_name == 'pull_request' - env: - BASE_BRANCH: ${{ github.event.pull_request.base.ref }} - HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} - run: | - # Use environment variables to safely handle potentially untrusted branch names - - # Guardrail 1: Target must be release-X.Y branch - if ! [[ "$BASE_BRANCH" =~ ^release- ]]; then - echo "❌ Error: Target branch must be a release branch (release-X.Y)" - echo " Found: $BASE_BRANCH" - exit 1 - fi - - # Guardrail 2: Source must be main branch - if [[ "$HEAD_BRANCH" != "main" ]]; then - echo "❌ Error: Source branch must be 'main'" - echo " Found: $HEAD_BRANCH" - exit 1 - fi - - echo "βœ… Branch validation passed: $HEAD_BRANCH β†’ $BASE_BRANCH" - - - name: Load and validate OWNERS - id: owners - run: | - # Parse OWNERS file for approvers - if [ ! -f "OWNERS" ]; then - echo "❌ Error: OWNERS file not found" - exit 1 - fi - - # Extract approvers list (YAML format) - APPROVERS=$(awk '/^approvers:/,/^[a-z]/' OWNERS | grep '^-' | sed 's/^- //' | tr '\n' ',' | sed 's/,$//') - echo "approvers=$APPROVERS" >> "$GITHUB_OUTPUT" - echo "πŸ“‹ Approvers: $APPROVERS" - - - name: Validate PR author permissions - if: github.event_name == 'pull_request' - run: | - PR_AUTHOR="${{ github.event.pull_request.user.login }}" - APPROVERS="${{ steps.owners.outputs.approvers }}" - - # Guardrail 3: Only OWNERS can create release PRs - if ! echo ",$APPROVERS," | grep -q ",$PR_AUTHOR,"; then - echo "❌ Error: User '$PR_AUTHOR' is not in OWNERS approvers list" - echo " Approvers: $APPROVERS" - exit 1 - fi - - echo "βœ… Permission check passed: $PR_AUTHOR is an approver" - - - name: Validate version format - run: | - VERSION="${{ steps.parse_version.outputs.version }}" - - # Validate SemVer format: vMAJOR.MINOR.PATCH[-PRERELEASE][+BUILD] - # Examples: v0.6.1, v0.6.1-rc.0, v0.6.1-alpha.1, v0.6.1-beta.2+build.123 - if ! [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then - echo "❌ Error: Version must match SemVer format vX.Y.Z[-PRERELEASE][+BUILD]" - echo " Examples: v0.6.1, v0.6.1-rc.0, v0.6.1-alpha.1, v0.6.1-beta.2+build.123" - echo " Found: $VERSION" - exit 1 - fi - - echo "βœ… Version format validated: $VERSION" - - - name: Check if tag already exists - run: | - VERSION="${{ steps.parse_version.outputs.version }}" - - if git rev-parse "$VERSION" >/dev/null 2>&1; then - echo "❌ Error: Tag $VERSION already exists" - git show "$VERSION" --no-patch - exit 1 - fi - - echo "βœ… Tag does not exist: $VERSION" - - - name: Verify PR is merged (not just closed) - if: github.event_name == 'pull_request' - run: | - if [[ "${{ github.event.pull_request.merged }}" != "true" ]]; then - echo "❌ Error: PR was closed without merging" - exit 1 - fi - - echo "βœ… PR was successfully merged" - - - name: Generate changelog - id: changelog - run: | - VERSION="${{ steps.parse_version.outputs.version }}" - - # Get previous tag - PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") - - # Generate changelog - if [ -n "$PREV_TAG" ]; then - echo "πŸ“ Generating changelog from $PREV_TAG to HEAD" - CHANGELOG=$(git log ${PREV_TAG}..HEAD \ - --pretty=format:"- %s (%h)" \ - --no-merges \ - --grep='^feat:' --grep='^fix:' --grep='^chore:' --grep='^refactor:' \ - --extended-regexp) - else - echo "πŸ“ No previous tag found, using recent commits" - CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges | head -20) - fi - - # Save to multiline output - echo "changelog<> $GITHUB_OUTPUT - echo "$CHANGELOG" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT - - - name: Create and push tag - run: | - VERSION="${{ steps.parse_version.outputs.version }}" - PREV_TAG="${{ steps.changelog.outputs.prev_tag }}" - CHANGELOG="${{ steps.changelog.outputs.changelog }}" - - # Configure git - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - # Create tag message - cat > tag_message.txt <> $GITHUB_STEP_SUMMARY <> $GITHUB_ENV - - - name: Set vars for main - if: github.ref == 'refs/heads/main' - run: |- - echo "BUILD_VERSION=latest" >> $GITHUB_ENV - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - registry: quay.io - username: ${{ secrets.QUAY_OPCT_USER }} - password: ${{ secrets.QUAY_OPCT_PASS }} - - - name: "Build and push: quay.io/opct/opct" - uses: docker/build-push-action@v6 - with: - platforms: ${{ env.PLATFORMS }} - push: ${{ env.PUSH }} - provenance: false - labels: | - quay.expires-after=${BUILD_EXPIRATION} - build-args: | - QUAY_EXPIRATION=${BUILD_EXPIRATION} - RELEASE_TAG=${{ env.BUILD_VERSION }} - tags: quay.io/opct/opct:${{ env.BUILD_VERSION }} - file: ./hack/Containerfile - - - name: Install dependencies - if: startsWith(github.ref, 'refs/tags/') - run: | - sudo apt-get update - sudo apt-get install make git -y - - - name: Build (all OS) for Github Release - if: startsWith(github.ref, 'refs/tags/') - run: | - make linux-amd64-container - make build-linux-amd64 - make build-windows-amd64 - make build-darwin-amd64 - make build-darwin-arm64 - - # https://github.com/mikepenz/release-changelog-builder-action#configuration - - name: Build Changelog when tag is pushed - if: startsWith(github.ref, 'refs/tags/') - id: github_release - uses: mikepenz/release-changelog-builder-action@v3.7.0 - with: - configuration: ".github/workflows/changelog-configuration.json" - - # https://github.com/softprops/action-gh-release - - name: Create Release on Github when tag is pushed - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v0.1.15 - env: - VERSION: ${{ steps.vars.outputs.tag }} - REPO: quay.io/opct/opct - with: - prerelease: true - files: | - build/opct-darwin-amd64 - build/opct-darwin-amd64.sum - build/opct-darwin-arm64 - build/opct-darwin-arm64.sum - build/opct-linux-amd64 - build/opct-linux-amd64.sum - build/opct-windows-amd64.exe - build/opct-windows-amd64.exe.sum - body: | - ## Changelog - ${{steps.github_release.outputs.changelog}} - - ## Images - - [quay.io/opct/opct:${{ env.VERSION }}](${{ env.REPO_URL }}) + if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + needs: e2e + env: + PLATFORMS: linux/amd64 + BUILD_EXPIRATION: never + PUSH: true + REPO_URL: https://quay.io/repository/opct/opct?tab=tags + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set vars for tag + if: startsWith(github.ref, 'refs/tags/') + run: |- + echo "BUILD_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Set vars for main + if: github.ref == 'refs/heads/main' + run: |- + echo "BUILD_VERSION=latest" >> $GITHUB_ENV + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + registry: quay.io + username: ${{ secrets.QUAY_OPCT_USER }} + password: ${{ secrets.QUAY_OPCT_PASS }} + + - name: "Build and push: quay.io/opct/opct" + uses: docker/build-push-action@v6 + with: + platforms: ${{ env.PLATFORMS }} + push: ${{ env.PUSH }} + provenance: false + labels: | + quay.expires-after=${BUILD_EXPIRATION} + build-args: | + QUAY_EXPIRATION=${BUILD_EXPIRATION} + RELEASE_TAG=${{ env.BUILD_VERSION }} + tags: quay.io/opct/opct:${{ env.BUILD_VERSION }} + file: ./hack/Containerfile + + - name: Install dependencies + if: startsWith(github.ref, 'refs/tags/') + run: | + sudo apt-get update + sudo apt-get install make git -y + + - name: Build (all OS) for Github Release + if: startsWith(github.ref, 'refs/tags/') + run: | + make linux-amd64-container + make build-linux-amd64 + make build-windows-amd64 + make build-darwin-amd64 + make build-darwin-arm64 + + # https://github.com/mikepenz/release-changelog-builder-action#configuration + - name: Build Changelog when tag is pushed + if: startsWith(github.ref, 'refs/tags/') + id: github_release + uses: mikepenz/release-changelog-builder-action@v3.7.0 + with: + configuration: ".github/workflows/changelog-configuration.json" + + # https://github.com/softprops/action-gh-release + - name: Create Release on Github when tag is pushed + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v0.1.15 + env: + VERSION: ${{ steps.vars.outputs.tag }} + REPO: quay.io/opct/opct + with: + prerelease: true + files: | + build/opct-darwin-amd64 + build/opct-darwin-amd64.sum + build/opct-darwin-arm64 + build/opct-darwin-arm64.sum + build/opct-linux-amd64 + build/opct-linux-amd64.sum + build/opct-windows-amd64.exe + build/opct-windows-amd64.exe.sum + body: | + ## Changelog + ${{steps.github_release.outputs.changelog}} + + ## Images + - [quay.io/opct/opct:${{ env.VERSION }}](${{ env.REPO_URL }}) diff --git a/.github/workflows/pre_reviewer.yaml b/.github/workflows/pre_reviewer.yaml index 0c0f3198..7377e08b 100644 --- a/.github/workflows/pre_reviewer.yaml +++ b/.github/workflows/pre_reviewer.yaml @@ -25,7 +25,7 @@ jobs: with: github_token: ${{ secrets.github_token }} reporter: github-pr-review - #level: warning + # level: warning locale: "US" # reviewdog / suggester: https://github.com/reviewdog/action-suggester diff --git a/CLAUDE.md b/CLAUDE.md index 2b7e7e7b..ac66c0ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -851,263 +851,119 @@ git push origin release-0.7 # 2. Follow normal release process with new branches ``` -### Improved PR-Based Release Workflow +### Release Checklist -**Problem with manual rebase**: The traditional process requires manually rebasing `main` to `release-X.Y`, which can: -- Introduce merge conflicts -- Risk force-pushing to release branches -- Require careful git knowledge - -**Better approach**: Use Pull Requests from `main` to `release-X.Y` with automated tag creation. - -#### Benefits - -βœ… **Reviewable**: Changes visible in PR before merging -βœ… **Safe**: No force-push required -βœ… **Automated**: CI validates before merge, tags created automatically -βœ… **Auditable**: PR history and automation logs preserved -βœ… **AI-assisted**: Claude can create the PR for you -βœ… **Secure**: Guardrails enforce permissions and branch rules - -#### Quick Start Prompts for Developers - -Use these prompts to get Claude's help with releases: - -**For OPCT CLI release v0.6.1:** -``` -I want to release OPCT CLI v0.6.1. Please: -1. Create a PR from main to release-0.6 -2. Include "Release: v0.6.1" in the PR description -3. Use proper commit message format - -The plugin images are already released at v0.6.1. -``` - -**For Plugins release v0.6.1:** -``` -I want to release OPCT Plugins v0.6.1. Please: -1. Create a PR from main to release-v0.6 -2. Include "Release: v0.6.1" in the PR description -3. Use proper commit message format -``` - -**For complete release (both repos):** -``` -I want to release both OPCT Plugins and CLI at v0.6.1. -Please guide me through the complete process: -1. Plugins release PR -2. CLI version bump PR (updating pkg/types.go) -3. CLI release PR - -Note: Tag creation is automated when PRs are merged. -``` - -#### Automated Tag Creation Workflow - -**Workflow file**: `.github/workflows/auto-release-tag.yaml` - -This workflow automates tag creation when a release PR is merged to a `release-*` branch. - -**Security guardrails enforced**: - -1. βœ… **Branch validation**: Only runs when PR targets `release-*` branch from `main` -2. βœ… **Permission check**: Only users in `OWNERS` file (approvers list) can create release PRs -3. βœ… **Merge requirement**: Tag only created when PR is **merged** (not just closed) -4. βœ… **Version validation**: Ensures version format is `vX.Y.Z` -5. βœ… **Duplicate prevention**: Fails if tag already exists -6. βœ… **Audit trail**: All validations logged in GitHub Actions - -**How it works**: - -1. Create PR from `main` to `release-X.Y` -2. Add `Release: vX.Y.Z` to PR description (or PR title) -3. Review and merge PR -4. **Workflow automatically creates and pushes tag** - -**PR description format** (required for version detection): - -```markdown -Release: v0.6.1 - -## Changes -- Feature: Add retry logic to cluster operator validation -- Fix: Correct error handling in validation flow - -## Checklist -- [x] Plugin images released at v0.6.1 (plugins only) -- [x] CI passing on main -- [x] Version bump PR merged (CLI only) -``` - -**Manual trigger** (for recovery if automation fails): - -```bash -# Via GitHub UI -# 1. Go to Actions β†’ "Automatic Release Tag Creation" -# 2. Click "Run workflow" -# 3. Enter version (v0.6.1) and release branch (release-0.6) - -# Or via CLI -gh workflow run auto-release-tag.yaml \ - -f version=v0.6.1 \ - -f release_branch=release-0.6 -``` - -#### Updated Release Process (PR-Based) +**Pre-release**: +- [ ] All changes merged to `main` in both repositories +- [ ] CI passing on `main` branch +- [ ] Version numbers decided (e.g., `v0.6.1`) -**OPCT CLI Release v0.6.1:** +**Plugins release**: +- [ ] Update `main` branch: `git checkout main && git pull` +- [ ] Update release branch: `git checkout release-v0.6 && git pull origin release-v0.6` +- [ ] Rebase from main: `git rebase main` +- [ ] Push release branch: `git push origin release-v0.6` +- [ ] Create annotated tag with comprehensive changelog (see tag creation example above) +- [ ] Push tag: `git push origin v0.6.1` +- [ ] Monitor CI build: `gh run watch` +- [ ] Verify images in registry: `skopeo list-tags docker://quay.io/opct/plugin-openshift-tests | grep v0.6.1` -**Step 1: Version bump PR** (updates plugin references) -```bash -git checkout main -git pull origin main -git checkout -b release/bump-v0.6.1 +**CLI release**: +- [ ] Create version bump PR updating `pkg/types.go` with new plugin versions +- [ ] Get PR reviewed and merged to `main` +- [ ] Update `main` branch: `git checkout main && git pull` +- [ ] Update release branch: `git checkout release-0.6 && git pull origin release-0.6` +- [ ] Rebase from main: `git rebase main` +- [ ] Push release branch: `git push origin release-0.6` +- [ ] Create annotated tag with comprehensive changelog (see tag creation example above) +- [ ] Push tag: `git push origin v0.6.1` +- [ ] Monitor CI build: `gh run watch` +- [ ] Verify CLI image in registry: `skopeo list-tags docker://quay.io/opct/opct | grep v0.6.1` +- [ ] Verify GitHub release created with binaries -# Edit pkg/types.go to reference v0.6.1 plugin images -# Update: PluginsImage, CollectorImage, MustGatherMonitoringImage +### Lessons Learned from v0.6.1 Release -git add pkg/types.go -git commit -m "chore: bump version to v0.6.1" -git push origin release/bump-v0.6.1 +**Date**: 2025-12-06 -# Create PR to main -gh pr create --base main --head release/bump-v0.6.1 \ - --title "chore: bump version to v0.6.1" \ - --body "Prepare for v0.6.1 release by updating plugin image versions." +#### What Went Wrong -# Wait for review, then merge -``` +1. **Automated tag creation workflow was unreliable** + - `.github/workflows/auto-release-tag.yaml` had multiple issues + - Complex workflow with security guardrails that were difficult to debug + - Failed silently when PR description didn't match expected format + - Required specific PR format that was easy to forget -**Step 2: Create release PR** (instead of manual rebase) -```bash -git checkout main -git pull origin main -git checkout -b release/prepare-v0.6.1 +2. **golangci-lint-action@v7 broke tag builds** + - Upgrade from v6 to v7 introduced breaking change with `only-new-issues` flag + - Flag fails on tag builds (no base commit to compare against) + - Error: `failed to fetch push patch: RequestError [HttpError]: Not Found` + - Blocked release for several hours while troubleshooting -git push origin release/prepare-v0.6.1 +3. **Over-engineering solutions** + - Attempted conditional `only-new-issues` logic: `${{ !startsWith(github.ref, 'refs/tags/') }}` + - Created multiple PRs trying to fix the same issue + - Added complexity instead of simplifying -# Create PR from main to release-0.6 -gh pr create --base release-0.6 --head main \ - --title "Release v0.6.1" \ - --body "Release: v0.6.1 +4. **Force-pushing to release branches caused confusion** + - Multiple force-pushes to `release-0.6` during troubleshooting + - Lost track of which commits were in which branch + - Made it harder to understand the actual state -## Changes -- Add retry logic to cluster operator validation -- Fix validation timeout handling +#### What Worked -## Checklist -- [x] Plugin images released at v0.6.1 -- [x] CI passing on main -- [x] Version bump PR merged" +1. **Manual tag creation is simple and reliable** + - Direct `git tag -a` with comprehensive changelog + - No complex workflows to debug + - Immediate feedback if something fails -# Review PR, then merge -# β†’ Tag v0.6.1 is AUTOMATICALLY created! -``` +2. **Rebase workflow for release branches** + - `git rebase main` on release branches works well + - Keeps release branch clean and up-to-date + - Easy to understand what changes are being released -**Step 3: Monitor automation** -```bash -# Watch the workflow run -gh run watch +3. **Plugin images released successfully** + - Plugins v0.6.1 built and published without issues + - Simpler workflow, fewer dependencies -# Verify tag was created -git fetch --tags -git tag -l | grep v0.6.1 +#### Key Takeaways -# Check CI build -gh run list --limit 5 -``` +1. **Keep it simple**: Manual processes are better than complex automation for infrequent tasks (releases happen ~monthly) +2. **Don't change what works**: The v0.6.0 release process worked fine with manual tags +3. **Test workflows thoroughly before relying on them**: Automated workflows need extensive testing +4. **Understand root causes before implementing fixes**: golangci-lint-action version upgrade was the real issue +5. **Use `continue-on-error` for non-critical CI steps**: Prevents one failing linter from blocking entire release -**OPCT Plugins Release v0.6.1:** +#### Recommended Approach Going Forward -**Step 1: Create release PR** +**Manual tag creation** (current approach): ```bash -git checkout main -git pull origin main - -# Create PR from main to release-v0.6 -gh pr create --base release-v0.6 --head main \ - --title "Release v0.6.1" \ - --body "Release: v0.6.1 - -## Changes -- Update openshift-tests plugin dependencies -- Fix collector plugin error handling +# 1. Update and rebase release branch +git checkout release-X.Y +git rebase main +git push origin release-X.Y -## Checklist -- [x] CI passing on main" +# 2. Create annotated tag with changelog +git tag -a vX.Y.Z -m "Release vX.Y.Z -# Review PR, then merge -# β†’ Tag v0.6.1 is AUTOMATICALLY created! -``` +[Comprehensive changelog here] -**Step 2: Verify images** -```bash -# Wait for CI to build images -sleep 60 +πŸ€– Claude Code Assistant +Co-Authored-By: Claude " -# Check images were published -skopeo list-tags docker://quay.io/opct/plugin-openshift-tests | grep v0.6.1 -skopeo list-tags docker://quay.io/opct/plugin-artifacts-collector | grep v0.6.1 -skopeo list-tags docker://quay.io/opct/must-gather-monitoring | grep v0.6.1 +# 3. Push tag to trigger CI +git push origin vX.Y.Z ``` -#### Troubleshooting - -**Issue**: Workflow doesn't trigger after PR merge - -**Solution**: -- Check PR description includes `Release: vX.Y.Z` -- Verify PR was merged to `release-*` branch -- Check workflow runs in GitHub Actions tab +**Advantages**: +- βœ… Simple and predictable +- βœ… Full control over changelog content +- βœ… Easy to troubleshoot if CI fails +- βœ… No dependencies on complex workflows +- βœ… Works the same way for both CLI and Plugins -**Issue**: Permission check fails - -**Solution**: -- Ensure PR author is in `OWNERS` file under `approvers:` section -- Only maintainers can create release PRs - -**Issue**: Tag already exists - -**Solution**: -- Check if tag was already created: `git tag -l` -- Use manual trigger with different version number -- Delete existing tag if incorrect: `git tag -d vX.Y.Z && git push origin :refs/tags/vX.Y.Z` - -**Issue**: Version not detected from PR - -**Solution**: -- Add `Release: vX.Y.Z` to PR description (first line recommended) -- Or include version in PR title: `Release v0.6.1` -- Format must be exactly `vX.Y.Z` (e.g., `v0.6.1`) - -### Release Checklist (Updated with Automation) - -**Pre-release**: -- [ ] All changes merged to `main` in both repositories -- [ ] CI passing on `main` branch -- [ ] Version numbers decided (e.g., `v0.6.1`) -- [ ] Automated workflow installed: `.github/workflows/auto-release-tag.yaml` exists - -**Plugins release**: -- [ ] Update `main` branch: `git checkout main && git pull` -- [ ] Create release PR: `gh pr create --base release-v0.6 --head main` with `Release: v0.6.1` in description -- [ ] Review and merge PR β†’ **Tag automatically created** -- [ ] Monitor workflow: `gh run watch` -- [ ] Verify tag created: `git fetch --tags && git tag -l | grep v0.6.1` -- [ ] Verify CI builds and publishes images -- [ ] Verify images in registry: `skopeo list-tags docker://quay.io/opct/plugin-openshift-tests | grep v0.6.1` - -**CLI release**: -- [ ] Create version bump PR updating `pkg/types.go` with new plugin versions -- [ ] Get PR reviewed and merged to `main` -- [ ] Update `main` branch: `git checkout main && git pull` -- [ ] Create release PR: `gh pr create --base release-0.6 --head main` with `Release: v0.6.1` in description -- [ ] Review and merge PR β†’ **Tag automatically created** -- [ ] Monitor workflow: `gh run watch` -- [ ] Verify tag created: `git fetch --tags && git tag -l | grep v0.6.1` -- [ ] Verify CI builds and publishes CLI image and binaries -- [ ] Verify image in registry: `skopeo list-tags docker://quay.io/opct/opct | grep v0.6.1` -- [ ] Verify GitHub release created with binaries +**Disadvantages**: +- ⚠️ Requires manual steps (but releases are infrequent) +- ⚠️ Requires git knowledge (but maintainers should have this) --- @@ -1229,6 +1085,99 @@ Co-Authored-By: Claude --- +## What's Next + +This section outlines potential improvements and opportunities for the OPCT project based on lessons learned. + +### Short-Term Improvements + +**1. Fix golangci-lint-action compatibility** +- **Issue**: v7 action broke tag builds with `only-new-issues` flag +- **Options**: + - Revert to `golangci/golangci-lint-action@v6` (simple, proven to work) + - Remove `only-new-issues` flag entirely (lint full codebase on every build) + - Use `continue-on-error: true` for tag builds (allows release to proceed) +- **Recommended**: Revert to v6 until v7 compatibility is confirmed + +**2. Standardize release tag message format** +- Create template for comprehensive changelogs +- Include sections: Bug Fixes, New Features, Enhancements, Dependencies +- Reference PR numbers for traceability +- Document in this file for future releases + +**3. Document common CI failures** +- Create troubleshooting guide for CI build failures +- Include common errors and solutions +- Add to developer documentation + +### Medium-Term Improvements + +**1. Improve CI stability** +- Audit all CI workflows for reliability +- Identify non-critical jobs that can use `continue-on-error` +- Ensure critical failures are clearly distinguished from warnings +- Test workflows on feature branches before merging to main + +**2. Release automation considerations** +- **Do not** implement automated tag creation workflows (proven unreliable) +- **Consider** simple release checklist automation (PR templates, issue templates) +- **Focus** on improving manual process documentation and tooling +- **Validate** that any automation is thoroughly tested before adoption + +**3. Version management** +- Consider using `VERSION` file in repository root +- Automate version bumps in `pkg/types.go` via script +- Add validation to ensure version consistency across files + +### Long-Term Opportunities + +**1. Release process tooling** +- Create simple CLI tool for release tasks (`opct-release`) +- Features: + - Interactive changelog generation from git history + - Automated version consistency checks + - Release checklist validation + - Tag creation with standardized format +- **Important**: Keep tool simple, avoid complex automation + +**2. Testing improvements** +- Expand unit test coverage +- Add integration tests for critical workflows +- Implement pre-release testing checklist +- Consider automated smoke tests for releases + +**3. Documentation enhancements** +- Create video walkthrough of release process +- Add diagrams for release workflows +- Document rollback procedures +- Expand troubleshooting guides + +### Anti-Patterns to Avoid + +Based on v0.6.1 experience: + +❌ **Complex GitHub Actions workflows for infrequent tasks** +- Releases happen ~monthly, automation overhead not worth it +- Hard to debug when they fail +- Manual process provides better control + +❌ **Changing workflows without thorough testing** +- Test on feature branches first +- Validate in dry-run mode +- Document expected behavior + +❌ **Over-engineering solutions** +- Simple solutions are better for maintainability +- Don't add conditional logic unless absolutely necessary +- Prefer established patterns over novel approaches + +❌ **Force-pushing to shared branches** +- Causes confusion and potential data loss +- Use feature branches and PRs instead +- Only force-push to personal branches + +--- + ## Document Maintenance This document should be updated when: @@ -1237,5 +1186,5 @@ This document should be updated when: - AI assistants encounter repeated questions or issues - Development workflows change significantly -**Last Updated**: 2025-12-05 +**Last Updated**: 2025-12-08 **Maintainer**: OPCT Development Team From b09a1fb8431cd0271c697cba41738793a659423e Mon Sep 17 00:00:00 2001 From: Marco Braga Date: Mon, 9 Feb 2026 11:48:33 -0300 Subject: [PATCH 3/5] OPCT-366: docs/pre-install: added decision graph to user guide (#179) **What this PR does / why we need it**: The changes introduced in this PR documents the **current valid** scenarios for users applying to VCSP. Recommendations must be aligned with PTAM and Program manager. Added decision tree to the User Guide to ensure users are installing correctly. Screenshot From 2025-10-27 13-35-59 **Which issue(s) this PR fixes** *(optional, use `fixes #(, fixes #, ...)` format, where issue_number might be a GitHub issue, or a Jira story*: Fixes # **Checklist** - [ ] Subject and description added to both, commit and PR. - [ ] Relevant issues have been referenced. - [ ] This change includes docs. - [ ] This change includes unit tests. --- docs/guides/cluster-validation/index.md | 43 ++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/docs/guides/cluster-validation/index.md b/docs/guides/cluster-validation/index.md index 33d4eecc..5a9ee916 100644 --- a/docs/guides/cluster-validation/index.md +++ b/docs/guides/cluster-validation/index.md @@ -95,6 +95,8 @@ on your infrastructure as if it were a production environment. Ensure that each feature of your infrastructure that you plan to support with OpenShift is configured in the cluster (e.g. Load Balancers, Storage, special hardware). +### OpenShift topologies + OpenShift supports the following topologies: - Highly available OpenShift Container Platform cluster (**HA**): Three control plane nodes with any number of compute nodes. @@ -114,15 +116,46 @@ OPCT is tested in the following topologies. Any topology flagged as TBD is not c or have the expected results for a formal validation process when applying to Red Hat OpenShift programs for Partners. +### OpenShift Platform Type + OpenShift Platform Type supported by OPCT on Red Hat OpenShift validation program: | Platform Type | Installation method | Documentation | | -- | -- | -- | -| `External` | `openshift-install` | [OpenShift Product][ocp-agn] [Providers][ocp-prov] | -| `None`* | Assisted Installer: `User-managed` network mode | [OpenShift Product][ai-none] | -| `External` | Agent Based Installer | [OpenShift Product][abi-external] | +| `External` | `openshift-install` | [OpenShift Product][ocp-agn](1), [Providers Guide][ocp-prov](2) | +| `None`* | Assisted Installer: `User-managed` network mode | [OpenShift Product][ai-none](3) | +| `External` | Agent Based Installer | [OpenShift Product][abi-external](4) | + +!!! warning "Platform `None` limitation" + Platform type `None` should be used only when required to install OpenShift cluster with Assisted Installer using `User-Managed` networking mode, otherwise use options with platform type `External`. + +```mermaid +%%{init: {"flowchart": {"useMaxWidth": false}}}%% + +flowchart TD + Start{Choose Installation Method} + Start --> A{Are you using
Assisted Service?} -*platform type `None` should be used only when required to install OpenShift cluster with Assisted Installer using `User-Managed` networking mode, otherwise use options with platform type `External`. + A -->|Yes| B{Is Connected
Installation?} + A -->|No| C["> Install tool: openhift-install
> Platform type: External
> platformName: _CHANGE_ME_"] + + C -->CQ{"Are you deploying Cloud Controller Manager (CCM)?"} + + B -->|Yes| D1["> Install tool: Assisted Installer
> Platform type: None
> Network mode: User-Managed

Documentation: OpenShift Product AI(3)"] + B -->|No| D2["> Install tool: Agent-Based Installer
> Platform type: External
> platformName: _CHANGE_ME_

Documentation: OpenShift Product ABI(4)"] + + CQ -->|No| E1["Documentation:
> OpenShift Agnostic Installation(1)
> Provider Onboarding Guide(2)"] + CQ -->|Yes| E2["> Set cloudControllerManager to External
> Create CCM manifests"] + + E2 --> E1 +``` + +Documentation Reference: + +- 1) [OpenShift Product with Installer][ocp-agn] +- 2) [Providers Guide][ocp-prov] +- 3) [OpenShift Product with AI][ai-none] +- 4) [OpenShift Product with ABI][abi-external] [ocp-agn]: https://docs.openshift.com/container-platform/latest/installing/installing_platform_agnostic/installing-platform-agnostic.html [ocp-prov]: https://docs.providers.openshift.org/platform-external/installing/ @@ -135,6 +168,8 @@ OpenShift Platform Type supported by OPCT on Red Hat OpenShift validation progra leading to failures in platform-specific e2e tests requiring special configuration or credentials. +### OpenShift And OPCT Versions + The matrix below describes the OpenShift and OPCT versions supported: | OPCT [version][releases] | OCP tested versions | OPCT Execution mode | From 350c74a38a25f5e2a449a833e21d2b38bad9c054 Mon Sep 17 00:00:00 2001 From: Marco Braga Date: Wed, 4 Mar 2026 13:35:01 -0300 Subject: [PATCH 4/5] NO-JIRA: doc: set the supported version based in OCPBUGS-77783 (#202) **What this PR does / why we need it**: Set the boundaries of supported version based in the current issue blocking validations 4.20+ described in https://issues.redhat.com/browse/OCPBUGS-77783 **Which issue(s) this PR fixes** *(optional, use `fixes #(, fixes #, ...)` format, where issue_number might be a GitHub issue, or a Jira story*: Fixes # **Checklist** - [ ] Subject and description added to both, commit and PR. - [ ] Relevant issues have been referenced. - [ ] This change includes docs. - [ ] This change includes unit tests. --- docs/guides/cluster-validation/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/guides/cluster-validation/index.md b/docs/guides/cluster-validation/index.md index 5a9ee916..65888d3a 100644 --- a/docs/guides/cluster-validation/index.md +++ b/docs/guides/cluster-validation/index.md @@ -174,13 +174,15 @@ The matrix below describes the OpenShift and OPCT versions supported: | OPCT [version][releases] | OCP tested versions | OPCT Execution mode | | -- | -- | -- | -| v0.6.1 | 4.16-4.20 | regular, upgrade, disconnected | +| v0.6.1+ | 4.16-4.19 | regular, upgrade, disconnected | | v0.5.x-v0.6.0 | 4.14-4.19 | regular, upgrade, disconnected | | v0.4.x | 4.10-4.13 | regular, upgrade, disconnected | | v0.3.x | 4.9-4.12 | regular, upgrade | | v0.2.x | 4.9-4.11 | regular | | v0.1.x | 4.9-4.11 | regular | +> Validations on OpenShift 4.20+ is currently blocked. See https://issues.redhat.com/browse/OCPBUGS-77783 for more information. + It is highly recommended to use the latest OPCT version. ### Standard Environment From 32b45e559336630e1463a691f0d8eb7f76de6e52 Mon Sep 17 00:00:00 2001 From: Marco Braga Date: Wed, 4 Mar 2026 17:13:45 -0300 Subject: [PATCH 5/5] NO-JIRA: enhance version command with cluster version (#201) **What this PR does / why we need it**: Enhance version command with more details about the environment, including plugins image versions and cluster version. Enhancing version command to improve the stdout, specially when assets isn't collected: ~~~sh $ make build-linux-amd64 GOOS=linux GOARCH=amd64 go build -o /home/me/go/src/github.com/mtulio/opct/build/opct-linux-amd64 -ldflags '-s -w -X github.com/redhat-openshift-ecosystem/opct/pkg/version.commit=b1b2642f -X github.com/redhat-openshift-ecosystem/opct/pkg/version.version=0.0.0' /home/me/go/src/github.com/mtulio/opct $ ./build/opct-linux-amd64 version OPCT CLI: 0.0.0+b1b2642f Images versions: sonobuoy:v0.57.3 (library v0.57.3) plugin-openshift-tests:v0.6.1 plugin-artifacts-collector:v0.6.1 must-gather-monitoring:v0.6.1 Cluster version: unknown (KUBECONFIG is not set) $ make build-linux-amd64 RELEASE_TAG=1.0.0 GOOS=linux GOARCH=amd64 go build -o /home/me/go/src/github.com/mtulio/opct/build/opct-linux-amd64 -ldflags '-s -w -X github.com/redhat-openshift-ecosystem/opct/pkg/version.commit=b1b2642f -X github.com/redhat-openshift-ecosystem/opct/pkg/version.version=1.0.0' /home/me/go/src/github.com/mtulio/opct $ ./build/opct-linux-amd64 version OPCT CLI: 1.0.0 Images versions: sonobuoy:v0.57.3 (library v0.57.3) plugin-openshift-tests:v0.6.1 plugin-artifacts-collector:v0.6.1 must-gather-monitoring:v0.6.1 Cluster version: unknown (KUBECONFIG is not set) $ export KUBECONFIG=$HOME/openshift/deploy/install-dir-v7/auth/kubeconfig $ ./build/opct-linux-amd64 version OPCT CLI: 1.0.0 Images versions: sonobuoy:v0.57.3 (library v0.57.3) plugin-openshift-tests:v0.6.1 plugin-artifacts-collector:v0.6.1 must-gather-monitoring:v0.6.1 Cluster version: OpenShift: 4.22.0-ec.2 Kubernetes: v1.34.2 ~~~ **Which issue(s) this PR fixes** *(optional, use `fixes #(, fixes #, ...)` format, where issue_number might be a GitHub issue, or a Jira story*: Fixes # **Checklist** - [x] Subject and description added to both, commit and PR. - [ ] Relevant issues have been referenced. - [ ] This change includes docs. - [ ] This change includes unit tests. --- pkg/version/cluster.go | 72 +++++++++++++++ pkg/version/cluster_test.go | 163 +++++++++++++++++++++++++++++++++ pkg/version/version.go | 33 +++++-- pkg/version/version_test.go | 178 ++++++++++++++++++++++++++++++++++++ 4 files changed, 440 insertions(+), 6 deletions(-) create mode 100644 pkg/version/cluster.go create mode 100644 pkg/version/cluster_test.go create mode 100644 pkg/version/version_test.go diff --git a/pkg/version/cluster.go b/pkg/version/cluster.go new file mode 100644 index 00000000..0bc74308 --- /dev/null +++ b/pkg/version/cluster.go @@ -0,0 +1,72 @@ +package version + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/redhat-openshift-ecosystem/opct/pkg/client" + "github.com/spf13/viper" + "k8s.io/client-go/kubernetes" + + coclient "github.com/openshift/client-go/config/clientset/versioned" + log "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ClusterVersion struct { + OpenShift string `json:"openshift"` + Kubernetes string `json:"kubernetes"` +} + +func GetClusterVersion() (*ClusterVersion, error) { + fmt.Printf("Cluster version:") + if !viper.IsSet("kubeconfig") { + fmt.Printf(" unknown (KUBECONFIG is not set)\n") + return nil, errors.New("KUBECONFIG is not set") + } + + var cli *client.Client + var err error + cli, err = client.NewClient() + if err != nil { + log.WithError(err).Error("pre-run failed when creating clients") + os.Exit(1) + } + oc, err := coclient.NewForConfig(cli.RestConfig) + if err != nil { + log.WithError(err).Error("pre-run failed when creating clients") + os.Exit(1) + } + + // Get OpenShift version + cv, err := oc.ConfigV1().ClusterVersions().Get(context.TODO(), "version", metav1.GetOptions{}) + if err != nil { + log.Warnf("Failed to get cluster version, defaulting to kubernetes/conformance suite with openshift-tests: %v", err) + os.Exit(1) + } + + versionString := " unknown (unable to get cluster version)" + version := cv.Status.Desired.Version + if version != "" { + versionString = version + } + + // Retrieve kubernetes version + kubeClient, err := kubernetes.NewForConfig(cli.RestConfig) + if err != nil { + log.WithError(err).Error("pre-run failed when creating clients") + os.Exit(1) + } + kubeVersion, err := kubeClient.Discovery().ServerVersion() + if err != nil { + log.WithError(err).Error("pre-run failed when creating clients") + os.Exit(1) + } + + return &ClusterVersion{ + OpenShift: versionString, + Kubernetes: kubeVersion.String(), + }, nil +} diff --git a/pkg/version/cluster_test.go b/pkg/version/cluster_test.go new file mode 100644 index 00000000..9a97efb3 --- /dev/null +++ b/pkg/version/cluster_test.go @@ -0,0 +1,163 @@ +package version + +import ( + "bytes" + "io" + "os" + "strings" + "testing" + + "github.com/spf13/viper" +) + +func TestClusterVersion_Fields(t *testing.T) { + // Test ClusterVersion struct field assignment + cv := ClusterVersion{ + OpenShift: "4.15.0", + Kubernetes: "v1.28.0", + } + + if cv.OpenShift != "4.15.0" { + t.Errorf("ClusterVersion.OpenShift = %q, want %q", cv.OpenShift, "4.15.0") + } + + if cv.Kubernetes != "v1.28.0" { + t.Errorf("ClusterVersion.Kubernetes = %q, want %q", cv.Kubernetes, "v1.28.0") + } +} + +func TestGetClusterVersion_NoKubeconfig(t *testing.T) { + // Save original viper state + originalKubeconfig := viper.Get("kubeconfig") + defer func() { + if originalKubeconfig != nil { + viper.Set("kubeconfig", originalKubeconfig) + } else { + // Reset viper to clean state + viper.Reset() + } + }() + + // Ensure kubeconfig is not set + viper.Reset() + + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + result, err := GetClusterVersion() + + // Restore stdout + if closeErr := w.Close(); closeErr != nil { + t.Fatalf("Failed to close pipe writer: %v", closeErr) + } + os.Stdout = oldStdout + + // Read captured output + var buf bytes.Buffer + if _, copyErr := io.Copy(&buf, r); copyErr != nil { + t.Fatalf("Failed to copy pipe output: %v", copyErr) + } + output := buf.String() + + // Verify error is returned + if err == nil { + t.Error("GetClusterVersion() expected error when KUBECONFIG is not set, got nil") + } + + expectedError := "KUBECONFIG is not set" + if err.Error() != expectedError { + t.Errorf("GetClusterVersion() error = %q, want %q", err.Error(), expectedError) + } + + // Verify result is nil + if result != nil { + t.Errorf("GetClusterVersion() result = %v, want nil", result) + } + + // Verify output contains expected message + expectedOutputs := []string{ + "Cluster version:", + "unknown (KUBECONFIG is not set)", + } + + for _, expected := range expectedOutputs { + if !strings.Contains(output, expected) { + t.Errorf("GetClusterVersion() output missing %q\nGot: %s", expected, output) + } + } +} + +func TestGetClusterVersion_WithKubeconfigSet(t *testing.T) { + // This test verifies behavior when KUBECONFIG is set but connection fails + // Note: Full integration testing would require mocking the Kubernetes client, + // which is not straightforward with the current implementation due to: + // 1. Direct os.Exit() calls that terminate the test process + // 2. Tight coupling with client creation + // 3. No dependency injection for client interfaces + // + // For comprehensive testing, the GetClusterVersion function would need refactoring to: + // - Accept client interfaces as dependencies + // - Return errors instead of calling os.Exit() + // - Separate output formatting from business logic + + t.Skip("Skipping integration test - requires refactoring to support dependency injection") + + // Future improvement: Refactor GetClusterVersion to accept interfaces: + // func GetClusterVersion(configClient ConfigClientInterface, kubeClient KubeClientInterface) (*ClusterVersion, error) +} + +func TestClusterVersion_ZeroValue(t *testing.T) { + // Test zero value initialization + var cv ClusterVersion + + if cv.OpenShift != "" { + t.Errorf("Zero value ClusterVersion.OpenShift = %q, want empty string", cv.OpenShift) + } + + if cv.Kubernetes != "" { + t.Errorf("Zero value ClusterVersion.Kubernetes = %q, want empty string", cv.Kubernetes) + } +} + +func TestClusterVersion_Equality(t *testing.T) { + cv1 := ClusterVersion{ + OpenShift: "4.15.0", + Kubernetes: "v1.28.0", + } + + cv2 := ClusterVersion{ + OpenShift: "4.15.0", + Kubernetes: "v1.28.0", + } + + cv3 := ClusterVersion{ + OpenShift: "4.16.0", + Kubernetes: "v1.29.0", + } + + // Test equality + if cv1 != cv2 { + t.Errorf("Expected cv1 == cv2, but they are not equal") + } + + // Test inequality + if cv1 == cv3 { + t.Errorf("Expected cv1 != cv3, but they are equal") + } +} + +// Note: Additional test coverage would require refactoring GetClusterVersion to: +// 1. Remove direct os.Exit() calls - return errors instead +// 2. Accept client interfaces as parameters (dependency injection) +// 3. Separate output formatting (fmt.Printf) from business logic +// +// Example refactored signature: +// func GetClusterVersion(ctx context.Context, client ClientInterface) (*ClusterVersion, error) +// +// This would enable: +// - Mocking client responses +// - Testing error paths without process termination +// - Testing business logic independently of I/O +// - Improved testability and maintainability diff --git a/pkg/version/version.go b/pkg/version/version.go index c0236d11..c6da2b0b 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -4,8 +4,10 @@ package version import ( "fmt" + "os" "github.com/redhat-openshift-ecosystem/opct/pkg" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/vmware-tanzu/sonobuoy/pkg/buildinfo" ) @@ -29,21 +31,40 @@ type VersionContext struct { } func (vc *VersionContext) String() string { - return fmt.Sprintf("OPCT CLI: %s+%s", vc.Version, vc.Commit) + if vc.Version == "0.0.0" { + return fmt.Sprintf("OPCT CLI: %s+%s", vc.Version, vc.Commit) + } + return fmt.Sprintf("OPCT CLI: %s", vc.Version) } -func (vc *VersionContext) StringPlugins() string { - return fmt.Sprintf("OPCT Plugins: %s", pkg.PluginsImage) +func (vc *VersionContext) stringPluginsImage() { + fmt.Printf("Images versions:") + fmt.Printf("\n %s (library %s)", pkg.SonobuoyImage, buildinfo.Version) + fmt.Printf("\n %s", pkg.PluginsImage) + fmt.Printf("\n %s", pkg.CollectorImage) + fmt.Printf("\n %s", pkg.MustGatherMonitoringImage) + fmt.Println("") } func NewCmdVersion() *cobra.Command { return &cobra.Command{ Use: "version", - Short: "Print provider validation tool version", + Short: "Print opct CLI version", Run: func(cmd *cobra.Command, args []string) { fmt.Println(Version.String()) - fmt.Println(Version.StringPlugins()) - fmt.Printf("Sonobuoy: %s\n", buildinfo.Version) + Version.stringPluginsImage() + // get cluster version if KUBECONFIG is set + clusterVersion, err := GetClusterVersion() + if err != nil { + if err.Error() == "KUBECONFIG is not set" { + os.Exit(0) + } + log.WithError(err).Error("failed when getting cluster version") + os.Exit(1) + } + fmt.Println("") + fmt.Printf(" OpenShift: %s\n", clusterVersion.OpenShift) + fmt.Printf(" Kubernetes: %s\n", clusterVersion.Kubernetes) }, } } diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go new file mode 100644 index 00000000..02632228 --- /dev/null +++ b/pkg/version/version_test.go @@ -0,0 +1,178 @@ +package version + +import ( + "bytes" + "io" + "os" + "strings" + "testing" + + "github.com/redhat-openshift-ecosystem/opct/pkg" + "github.com/vmware-tanzu/sonobuoy/pkg/buildinfo" +) + +func TestVersionContext_String(t *testing.T) { + tests := []struct { + name string + version VersionContext + want string + }{ + { + name: "development version with commit", + version: VersionContext{ + Name: "test-project", + Version: "0.0.0", + Commit: "abc123def", + }, + want: "OPCT CLI: 0.0.0+abc123def", + }, + { + name: "release version without commit", + version: VersionContext{ + Name: "test-project", + Version: "1.2.3", + Commit: "abc123def", + }, + want: "OPCT CLI: 1.2.3", + }, + { + name: "unknown version", + version: VersionContext{ + Name: "test-project", + Version: "unknown", + Commit: "unknown", + }, + want: "OPCT CLI: unknown", + }, + { + name: "semver with v prefix", + version: VersionContext{ + Name: "test-project", + Version: "v0.6.1", + Commit: "commit-hash", + }, + want: "OPCT CLI: v0.6.1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.version.String() + if got != tt.want { + t.Errorf("VersionContext.String() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestVersionContext_stringPluginsImage(t *testing.T) { + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + vc := VersionContext{ + Name: "test-project", + Version: "1.0.0", + Commit: "abc123", + } + + vc.stringPluginsImage() + + // Restore stdout + if err := w.Close(); err != nil { + t.Fatalf("Failed to close pipe writer: %v", err) + } + os.Stdout = oldStdout + + // Read captured output + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + t.Fatalf("Failed to copy pipe output: %v", err) + } + output := buf.String() + + // Verify output contains expected plugin images + expectedStrings := []string{ + "Images versions:", + pkg.SonobuoyImage, + buildinfo.Version, + pkg.PluginsImage, + pkg.CollectorImage, + pkg.MustGatherMonitoringImage, + } + + for _, expected := range expectedStrings { + if !strings.Contains(output, expected) { + t.Errorf("stringPluginsImage() output missing %q\nGot: %s", expected, output) + } + } +} + +func TestNewCmdVersion(t *testing.T) { + cmd := NewCmdVersion() + + if cmd == nil { + t.Fatal("NewCmdVersion() returned nil") + } + + if cmd.Use != "version" { + t.Errorf("NewCmdVersion().Use = %q, want %q", cmd.Use, "version") + } + + if cmd.Short == "" { + t.Error("NewCmdVersion().Short is empty") + } + + if cmd.Run == nil { + t.Error("NewCmdVersion().Run is nil") + } + + // Verify the command has expected properties + expectedShort := "Print opct CLI version" + if cmd.Short != expectedShort { + t.Errorf("NewCmdVersion().Short = %q, want %q", cmd.Short, expectedShort) + } +} + +func TestVersionGlobalVariable(t *testing.T) { + // Test that the global Version variable is properly initialized + if Version.Name == "" { + t.Error("Version.Name is empty") + } + + if Version.Version == "" { + t.Error("Version.Version is empty") + } + + if Version.Commit == "" { + t.Error("Version.Commit is empty") + } + + // Verify default values + expectedName := "openshift-provider-cert" + if Version.Name != expectedName { + t.Errorf("Version.Name = %q, want %q", Version.Name, expectedName) + } +} + +func TestVersionContext_Fields(t *testing.T) { + // Test struct field marshaling + vc := VersionContext{ + Name: "test-name", + Version: "test-version", + Commit: "test-commit", + } + + if vc.Name != "test-name" { + t.Errorf("VersionContext.Name = %q, want %q", vc.Name, "test-name") + } + + if vc.Version != "test-version" { + t.Errorf("VersionContext.Version = %q, want %q", vc.Version, "test-version") + } + + if vc.Commit != "test-commit" { + t.Errorf("VersionContext.Commit = %q, want %q", vc.Commit, "test-commit") + } +}