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 diff --git a/docs/guides/cluster-validation/index.md b/docs/guides/cluster-validation/index.md index 33d4eecc..65888d3a 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?} + + A -->|Yes| B{Is Connected
Installation?} + A -->|No| C["> Install tool: openhift-install
> Platform type: External
> platformName: _CHANGE_ME_"] -*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`. + 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,17 +168,21 @@ 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 | | -- | -- | -- | -| 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 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= 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") + } +}