Skip to content
Skillv1.0.0

ote-migration-workflow

Automated workflow for migrating OpenShift component repositories to OTE framework

by openshift-eng(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from openshift-eng/ai-helpers (plugins/ote-migration/skills/ote-migration-workflow/SKILL.md). Install upstream with npx skills add openshift-eng/ai-helpers --skill ote-migration-workflow. Copyright stays with the author.

OTE Migration Workflow Skill

This skill provides step-by-step implementation guidance for the complete OTE migration workflow.

When to Use This Skill

Use this skill when executing the /ote-migration:migrate command to automate the migration of OpenShift component repositories to the openshift-tests-extension (OTE) framework.

Prerequisites

  • Go toolchain (1.21+)
  • Git installed and configured
  • Access to openshift-tests-private repository:
    • Option 1: Existing local clone (with optional update)
    • Option 2: Git credentials to clone from git@github.com:openshift/openshift-tests-private.git
  • Target component repository:
    • Option 1: Local path to existing repository
    • Option 2: Git URL to clone repository

Overview

The migration is an 8-phase workflow that collects configuration, sets up repositories, creates structure, generates code, migrates tests, resolves dependencies, integrates with Docker, and provides documentation.

Workflow Summary:

ALL 8 PHASES ARE MANDATORY - EXECUTE EACH PHASE IN ORDER:

  1. User Input Collection (9 inputs - includes Dockerfile integration choice)
  2. Repository Setup (source and target)
  3. Structure Creation (directories and files)
  4. Code Generation (go.mod, main.go, Makefile, bindata.mk, fixtures.go)
  5. Test Migration (automated with rollback on failure)
  6. Dependency Resolution (go mod tidy + vendor + build verification)
  7. Dockerfile Integration (uses choice from Input 9)
  8. Final Summary and Next Steps

DO NOT skip Phase 7. After Phase 6 completes, proceed immediately to Phase 7.

Key Design Principles:

  • No sig filtering: All tests included without filtering logic
  • CMD at root (monorepo): cmd/<extension-name>-tests-ext/main.go (not under test/)
  • Simple annotations: [OTP] at beginning of Describe, [Level0] at beginning of test name only
  • Single go.mod (monorepo): All dependencies in root go.mod (no separate test module)
  • Vendor at root (monorepo): Only vendor/ at repository root
  • No compress/copy targets: Removed from root Makefile
  • 🚨 REQUIRED IMPORTS (DO NOT MODIFY): main.go MUST import both packages:
    • exutil "github.com/openshift/origin/test/extended/util" - provides the actual CLI type (exutil.CLI)
    • compat_otp "github.com/openshift/origin/test/extended/util/compat_otp" - provides helper functions (NewCLI(), KubeConfigPath())
    • CRITICAL: Use *exutil.CLI for type declarations, NOT *compat_otp.CLI (which doesn't exist)

Migration Phases

Phase 1: User Input Collection (9 inputs)

Collect all necessary information from the user before starting the migration.

CRITICAL INSTRUCTIONS:

  • Extension name (Input 4): AUTO-DETECT from target repository - do NOT ask user
  • All other inputs: Ask user explicitly using AskUserQuestion tool or direct prompts
  • WAIT for user response before proceeding to the next input or phase
  • Switch to target repository happens after Input 3 (before auto-detecting extension name)
  • Dockerfile integration (Input 10): Ask user choice - will be used in Phase 7

Variables collected (shown as <variable-name>) will be used throughout the migration.

Input 1: Directory Structure Strategy

Ask: "Which directory structure strategy do you want to use?"

Option 1: Monorepo strategy (integrate into existing repo)

  • Integrates into existing repository structure
  • Uses existing cmd/ and test/ directories
  • CMD location: cmd/extension/main.go (at repository root, NOT under test/)
  • Single go.mod: All dependencies in root go.mod (no separate test module)
  • Vendor location: vendor/ at root ONLY

Option 2: Single-module strategy (isolated directory)

  • Creates isolated tests-extension/ directory
  • Self-contained with single go.mod
  • CMD location: tests-extension/cmd/main.go
  • Vendor location: tests-extension/vendor/

User selects: 1 or 2

Store the selection in variable: <structure-strategy> (value: "monorepo" or "single-module")

Input 2: Working Directory (Workspace)

Ask: "What is the working directory path for migration workspace?

IMPORTANT: This is a temporary workspace for cloning repositories. Your target repository will be collected in the next step (Input 3), and that's where OTE files will be created."

Purpose:

  • Temporary location for cloning repositories that don't exist locally
  • Recommendation: Parent directory of your target repo or temporary directory

User provides the path:

  • Can be absolute or relative
  • Can be current directory (.)
  • Will create if doesn't exist

Store in variable: <working-dir>

Input 3: Target Repository

Ask: "What is the path to your target repository, or provide a Git URL to clone?"

  • Option 1: Local path - Use existing local repository (e.g., /home/user/repos/router)
  • Option 2: Git URL - Clone from remote repository (e.g., git@github.com:openshift/router.git)

Store in variable: <target-repo-path> or <target-repo-url>

Input 3a: Update Local Target Repository (if local target provided)

If a local target repository path was provided:

Ask: "Do you want to update the local target repository? (git fetch && git pull) [Y/n]:"

  • Default: Yes
  • Store in variable: <update-target> (value: "yes" or "no")

Input 3b: Validate and Switch to Target Repository

Step 1: Validate and update target repository

For local path:

# Validate target repository exists
if [ ! -d "$TARGET_REPO_PATH" ]; then
    echo "❌ ERROR: Target repository does not exist"
    exit 1
fi

# Check if git repository and update if requested
if [ -d "$TARGET_REPO_PATH/.git" ]; then
    cd "$TARGET_REPO_PATH"

    if [ "<update-target>" = "yes" ]; then
        CURRENT_BRANCH=$(git branch --show-current)
        TARGET_REMOTE=$(git remote -v | awk '{print $1}' | head -1)
        git fetch "$TARGET_REMOTE"
        git pull "$TARGET_REMOTE" "$CURRENT_BRANCH"
    fi
fi

For Git URL:

# Extract repository name
REPO_NAME=$(echo "$TARGET_REPO_URL" | sed -E 's|.*/([^/]+)\.git$|\1|')
cd "$WORKING_DIR"
git clone "$TARGET_REPO_URL" "$REPO_NAME"
TARGET_REPO_PATH="$WORKING_DIR/$REPO_NAME"

# Create feature branch
cd "$TARGET_REPO_PATH"
BRANCH_NAME="ote-migration-$(date +%Y%m%d)"
git checkout -b "$BRANCH_NAME"

Step 2: Switch working directory to target repository

cd "$TARGET_REPO_PATH"
WORKING_DIR="$TARGET_REPO_PATH"

echo "========================================="
echo "Switched to target repository"
echo "Working directory is now: $WORKING_DIR"
echo "========================================="

CRITICAL: From this point forward, all operations happen in the target repository.

Input 4: Extension Name (Auto-Detection)

DO NOT ask the user for this - auto-detect it from the target repository.

cd "$WORKING_DIR"

if [ -d ".git" ]; then
    DISCOVERED_REMOTE=$(git remote -v | head -1 | awk '{print $1}')
    if [ -n "$DISCOVERED_REMOTE" ]; then
        REMOTE_URL=$(git remote get-url "$DISCOVERED_REMOTE" 2>/dev/null)
        EXTENSION_NAME=$(echo "$REMOTE_URL" | sed 's/.*[:/]\([^/]*\)\/\([^/]*\)\.git$/\2/' | sed 's/\.git$//')
    else
        EXTENSION_NAME=$(basename "$WORKING_DIR")
    fi
else
    EXTENSION_NAME=$(basename "$WORKING_DIR")
fi

echo "Extension name auto-detected: $EXTENSION_NAME"

Store in variable: <extension-name>

Input 4a: Target Test Directory Name (Conditional - Monorepo Mode Only)

This input is ONLY asked if:

  1. Monorepo strategy is selected (from Input 1)
  2. Target repository already has test/e2e/ directory

Purpose: When migrating to a repository that already has test/e2e/, we need to create a subdirectory to avoid conflicts with existing tests.

Detection logic:

cd "$WORKING_DIR"

if [ "$STRUCTURE_STRATEGY" = "monorepo" ] && [ -d "test/e2e" ]; then
    echo "⚠️  Target repository already has test/e2e/ directory"
    echo "Tests will be migrated to a subdirectory under test/e2e/"

    # Ask for target test directory name
    read -p "What subdirectory name should be used under test/e2e/ for migrated tests? (default: extension): " TARGET_TEST_DIR_NAME
    TARGET_TEST_DIR_NAME=${TARGET_TEST_DIR_NAME:-extension}  # Default to "extension" if empty
else
    # No subdirectory needed - tests go directly in test/e2e/
    TARGET_TEST_DIR_NAME=""
fi

If test/e2e exists:

  • Prompts: "What subdirectory name should be used under test/e2e/ for migrated tests? (default: extension):"
  • Default: "extension"
  • Example: If you enter "router", tests will be at test/e2e/router/
  • Example: If you press Enter, tests will be at test/e2e/extension/
  • Store in variable: <target-test-dir> (default: "extension" if empty)

If test/e2e does NOT exist:

  • No prompt needed - tests go directly in test/e2e/
  • Store in variable: <target-test-dir> (empty string "")

Input 5: Local Source Repository (Optional)

Ask: "Do you have a local clone of openshift-tests-private? If yes, provide the path (or press Enter to clone):"

Store in variable: <local-source-path> (empty if user wants to clone)

Input 6: Update Local Source Repository (if local source provided)

If local source provided: Ask: "Do you want to update the local source repository? (git fetch && git pull) [Y/n]:"

Store in variable: <update-source> (value: "yes" or "no")

Input 7: Source Test Subfolder

Ask: "What is the test subfolder name under test/extended/?"

  • Example: "networking", "router", "storage"

Store in variable: <test-subfolder>

Input 8: Source Testdata Subfolder (Optional)

IMPORTANT: This determines which testdata fixtures are copied from the source OTE repository. The testdata files are embedded into bindata.go and accessed via FixturePath() calls in tests.

Ask: "What is the testdata subfolder name under test/extended/testdata/?"

Options:

  • Press Enter to use the same value as the test subfolder (Input 7)
  • Enter a subfolder name (e.g., "router", "networking") if different from test subfolder
  • Enter "none" if no testdata fixtures exist for these tests

Default: Same as Input 7 (recommended for most cases)

Examples:

  • If test subfolder is "router" and testdata is at test/extended/testdata/router/, press Enter
  • If test subfolder is "router" but testdata is at test/extended/testdata/edge/, enter "edge"
  • If no testdata files exist, enter "none"

AI MUST execute this verification before asking the user:

# List testdata subdirectories to help user answer
if [ -d "<source-repo>/test/extended/testdata" ]; then
    echo "Available testdata subdirectories:"
    ls -la "<source-repo>/test/extended/testdata/" | grep "^d" | grep -v "^\.$" | awk '{print $NF}'
else
    echo "No testdata directory found at <source-repo>/test/extended/testdata"
fi

Then present the discovered subdirectories to the user and ask for their choice.

Store in variable: <testdata-subfolder>

Input 9: Dockerfile Integration Choice

Ask: "Do you want to update Dockerfiles automatically, or do it manually?"

Options:

  1. Automated - Let the plugin update your Dockerfiles automatically (with backup)
  2. Manual - Get instructions to update Dockerfiles yourself

Store in variable: <dockerfile-choice> (value: "automated" or "manual")

Input 9a: Select Dockerfiles to Update (conditional - only if automated)

This input is ONLY asked if user chose "automated" in Input 9.

If user chose automated, search for all Dockerfiles in the target repository and ask user to select:

cd <working-dir>  # Should already be in target repository from Input 3

echo "Searching for Dockerfiles in target repository..."

# Search for all Dockerfiles recursively
DOCKERFILES=$(find . -type f \( -name "Dockerfile" -o -name "Dockerfile.*" \) ! -path "*/vendor/*" ! -path "*/.git/*" ! -path "*/tests-extension/*" 2>/dev/null)

if [ -z "$DOCKERFILES" ]; then
    echo "⚠️  No Dockerfiles found in repository"
    echo "You can add Dockerfiles later and integrate manually, or continue without Dockerfile integration"
    SELECTED_DOCKERFILES=""
else
    # Display found Dockerfiles
    echo ""
    echo "Found Dockerfiles:"
    echo "$DOCKERFILES" | nl -w2 -s'. '
    echo ""
fi

If Dockerfiles were found, ask user to select:

Ask: "Which Dockerfile(s) do you want to update?"

Options:

  • Enter a number (e.g., 1 for first Dockerfile)
  • Enter all to update all Dockerfiles
  • Enter none to skip Dockerfile integration

Example:

Found Dockerfiles:
 1. ./Dockerfile
 2. ./Dockerfile.rhel8
 3. ./build/Dockerfile

Which Dockerfile(s) do you want to update? (number, 'all', or 'none'):

Store user selection:

# Get user choice
CHOICE=<user-input>

if [ -z "$DOCKERFILES" ] || [ "$CHOICE" = "none" ]; then
    SELECTED_DOCKERFILES=""
    echo "Skipping Dockerfile integration"
elif [ "$CHOICE" = "all" ]; then
    SELECTED_DOCKERFILES="$DOCKERFILES"
    echo "Selected: All Dockerfiles"
else
    # Convert to array and get selected file
    DOCKERFILES_ARRAY=($DOCKERFILES)
    if [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le "${#DOCKERFILES_ARRAY[@]}" ]; then
        SELECTED_DOCKERFILES="${DOCKERFILES_ARRAY[$((CHOICE-1))]}"
        echo "Selected: $SELECTED_DOCKERFILES"
    else
        echo "❌ Invalid choice"
        exit 1
    fi
fi

Store in variable: <selected-dockerfiles> (space-separated list of Dockerfile paths, or empty if none)

Display Configuration Summary

Show all collected inputs for user confirmation before proceeding:

========================================
Migration Configuration Summary
========================================
Strategy:              <structure-strategy>
Workspace:             <working-dir>
Target Repository:     <target-repo-path>
Update Target Repo:    <update-target or "cloned from URL" or "N/A">
Extension Name:        <extension-name>
Target Test Directory: <target-test-dir or "test/e2e (no subdirectory)" or "N/A (single-module)">
Source Repository:     <local-source-path or "will clone">
Update Source Repo:    <update-source or "will clone" or "N/A">
Test Subfolder:        <test-subfolder>
Testdata Subfolder:    <testdata-subfolder>
Dockerfile Integration: <dockerfile-choice>
Selected Dockerfiles:  <selected-dockerfiles or "manual integration" or "none">
========================================

Example output (local target with existing test/e2e, automated Dockerfile):

========================================
Migration Configuration Summary
========================================
Strategy:              monorepo
Workspace:             /home/user/repos
Target Repository:     /home/user/repos/router
Update Target Repo:    yes
Extension Name:        router
Target Test Directory: test/e2e/extension
Source Repository:     /home/user/openshift-tests-private
Update Source Repo:    yes
Test Subfolder:        router
Testdata Subfolder:    router
Dockerfile Integration: automated
Selected Dockerfiles:  ./Dockerfile, ./Dockerfile.rhel8
========================================

Example output (local target without test/e2e, automated Dockerfile):

========================================
Migration Configuration Summary
========================================
Strategy:              monorepo
Workspace:             /home/user/repos
Target Repository:     /home/user/repos/mycomponent
Update Target Repo:    yes
Extension Name:        mycomponent
Target Test Directory: test/e2e (no subdirectory)
Source Repository:     /home/user/openshift-tests-private
Update Source Repo:    yes
Test Subfolder:        mycomponent
Testdata Subfolder:    mycomponent
Dockerfile Integration: automated
Selected Dockerfiles:  ./Dockerfile
========================================

Example output (cloned target, manual Dockerfile, single-module strategy):

========================================
Migration Configuration Summary
========================================
Strategy:              single-module
Workspace:             /tmp/migration
Target Repository:     /tmp/migration/router
Update Target Repo:    cloned from URL
Extension Name:        router
Target Test Directory: N/A (single-module)
Source Repository:     will clone
Update Source Repo:    N/A
Test Subfolder:        router
Testdata Subfolder:    router
Dockerfile Integration: manual
Selected Dockerfiles:  manual integration
========================================

Ask: "Proceed with migration? [Y/n]:"

Phase 1 Validation Checkpoint

MANDATORY VALIDATION:

# Verify extension name detected
if [ -z "$EXTENSION_NAME" ]; then
    echo "❌ ERROR: Extension name not detected"
    exit 1
fi

# Verify strategy selected
if [ -z "$STRUCTURE_STRATEGY" ]; then
    echo "❌ ERROR: Strategy not selected"
    exit 1
fi

# Verify target repository path collected
if [ -z "$TARGET_REPO_PATH" ]; then
    echo "❌ ERROR: Target repository path not collected"
    exit 1
fi

# Verify working directory switched to target
if [ "$WORKING_DIR" != "$TARGET_REPO_PATH" ]; then
    echo "❌ ERROR: Working directory not switched to target"
    exit 1
fi

echo "✅ Phase 1 Validation Complete"

Phase 2: Repository Setup

Step 1: Setup Source Repository

For local source:

SOURCE_REPO="<local-source-path>"

if [ "<update-source>" = "yes" ]; then
    cd "$SOURCE_REPO"
    CURRENT_BRANCH=$(git branch --show-current)

    # Checkout main/master if on different branch
    if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "master" ]; then
        if git show-ref --verify --quiet refs/heads/main; then
            git checkout main
            TARGET_BRANCH="main"
        else
            git checkout master
            TARGET_BRANCH="master"
        fi
    else
        TARGET_BRANCH="$CURRENT_BRANCH"
    fi

    SOURCE_REMOTE=$(git remote -v | awk '{print $1}' | head -1)
    git fetch "$SOURCE_REMOTE"
    git pull "$SOURCE_REMOTE" "$TARGET_BRANCH"
fi

For cloning:

cd <working-dir>

if [ -d "openshift-tests-private" ]; then
    cd openshift-tests-private
    SOURCE_REMOTE=$(git remote -v | grep 'openshift/openshift-tests-private' | head -1 | awk '{print $1}')
    git fetch "$SOURCE_REMOTE"
    git pull "$SOURCE_REMOTE" master || git pull "$SOURCE_REMOTE" main
    cd ..
else
    git clone git@github.com:openshift/openshift-tests-private.git openshift-tests-private
fi

SOURCE_REPO="openshift-tests-private"

Set source paths:

if [ -z "<test-subfolder>" ]; then
    SOURCE_TEST_PATH="$SOURCE_REPO/test/extended"
else
    SOURCE_TEST_PATH="$SOURCE_REPO/test/extended/<test-subfolder>"
fi

if [ "<testdata-subfolder>" = "none" ]; then
    SOURCE_TESTDATA_PATH=""
elif [ -z "<testdata-subfolder>" ]; then
    SOURCE_TESTDATA_PATH="$SOURCE_REPO/test/extended/testdata"
else
    SOURCE_TESTDATA_PATH="$SOURCE_REPO/test/extended/testdata/<testdata-subfolder>"
fi

Phase 3: Structure Creation

Step 1: Create Directory Structure

For Monorepo Strategy:

cd <working-dir>

# Set directory paths based on whether test/e2e already exists
if [ -n "$TARGET_TEST_DIR_NAME" ]; then
    # test/e2e exists - use subdirectory
    TEST_CODE_DIR="test/e2e/$TARGET_TEST_DIR_NAME"
    TESTDATA_DIR="test/e2e/$TARGET_TEST_DIR_NAME/testdata"
    echo "Using test subdirectory: test/e2e/$TARGET_TEST_DIR_NAME/"
else
    # No test/e2e - use test/e2e directly
    TEST_CODE_DIR="test/e2e"
    TESTDATA_DIR="test/e2e/testdata"
    echo "Using test directory: test/e2e/"
fi

# Create directories
# IMPORTANT: cmd follows pattern cmd/extension/, NOT under test/
mkdir -p "cmd/extension"
mkdir -p bin
mkdir -p "$TEST_CODE_DIR"
mkdir -p "$TESTDATA_DIR"

echo "✅ Created monorepo structure"
echo "   CMD directory: cmd/extension/"
echo "   Test code: $TEST_CODE_DIR"
echo "   Testdata: $TESTDATA_DIR"

For Single-Module Strategy:

cd <working-dir>
mkdir -p tests-extension

cd tests-extension
mkdir -p cmd
mkdir -p bin
mkdir -p test/e2e
mkdir -p test/e2e/testdata

echo "✅ Created single-module structure"

Step 2: Copy Test Files

For Monorepo:

cp -r "$SOURCE_TEST_PATH"/* "$TEST_CODE_DIR"/
echo "Copied $(find "$TEST_CODE_DIR" -name '*_test.go' | wc -l) test files"

For Single-Module:

cp -r "$SOURCE_TEST_PATH"/* test/e2e/
echo "Copied $(find test/e2e -name '*_test.go' | wc -l) test files"

Step 3: Copy Testdata

IMPORTANT: This step copies fixture files from the source OTE repository's testdata directory. If testdata files are not copied, bindata generation will only embed fixtures.go, causing runtime panics when tests load fixture files via FixturePath().

For Monorepo:

if [ -n "$SOURCE_TESTDATA_PATH" ] && [ "$SOURCE_TESTDATA_PATH" != "" ]; then
    echo "Copying testdata from: $SOURCE_TESTDATA_PATH"
    echo "Target testdata directory: $TESTDATA_DIR"

    if [ -n "<testdata-subfolder>" ] && [ "<testdata-subfolder>" != "none" ]; then
        # Copy with subfolder structure preserved
        mkdir -p "$TESTDATA_DIR/<testdata-subfolder>"
        cp -rv "$SOURCE_TESTDATA_PATH"/* "$TESTDATA_DIR/<testdata-subfolder>/" || {
            echo "❌ Failed to copy testdata files"
            exit 1
        }
        echo "✅ Copied testdata files to $TESTDATA_DIR/<testdata-subfolder>/"
        ls -la "$TESTDATA_DIR/<testdata-subfolder>/" | head -10
    else
        # Copy without subfolder (flatten)
        cp -rv "$SOURCE_TESTDATA_PATH"/* "$TESTDATA_DIR/" || {
            echo "❌ Failed to copy testdata files"
            exit 1
        }
        echo "✅ Copied testdata files to $TESTDATA_DIR/"
        ls -la "$TESTDATA_DIR/" | head -10
    fi
else
    echo "⚠️  No testdata files to copy (SOURCE_TESTDATA_PATH is empty or 'none')"
fi

For Single-Module:

if [ -n "$SOURCE_TESTDATA_PATH" ] && [ "$SOURCE_TESTDATA_PATH" != "" ]; then
    echo "Copying testdata from: $SOURCE_TESTDATA_PATH"
    echo "Target testdata directory: test/e2e/testdata"

    if [ -n "<testdata-subfolder>" ] && [ "<testdata-subfolder>" != "none" ]; then
        # Copy with subfolder structure preserved
        mkdir -p "test/e2e/testdata/<testdata-subfolder>"
        cp -rv "$SOURCE_TESTDATA_PATH"/* "test/e2e/testdata/<testdata-subfolder>/" || {
            echo "❌ Failed to copy testdata files"
            exit 1
        }
        echo "✅ Copied testdata files to test/e2e/testdata/<testdata-subfolder>/"
        ls -la "test/e2e/testdata/<testdata-subfolder>/" | head -10
    else
        # Copy without subfolder (flatten)
        cp -rv "$SOURCE_TESTDATA_PATH"/* test/e2e/testdata/ || {
            echo "❌ Failed to copy testdata files"
            exit 1
        }
        echo "✅ Copied testdata files to test/e2e/testdata/"
        ls -la "test/e2e/testdata/" | head -10
    fi
else
    echo "⚠️  No testdata files to copy (SOURCE_TESTDATA_PATH is empty or 'none')"
fi

# Verify testdata files were copied (excluding fixtures.go and bindata.go)
TESTDATA_FILE_COUNT=$(find "$TESTDATA_DIR" -type f ! -name "fixtures.go" ! -name "bindata.go" 2>/dev/null | wc -l)
if [ "$TESTDATA_FILE_COUNT" -eq 0 ]; then
    echo "⚠️  WARNING: No testdata fixture files found in $TESTDATA_DIR"
    echo "This may cause test failures if tests use FixturePath() to load fixtures."
    echo "Verify that testdata-subfolder input was correct."
fi

For Single-Module:

# Same validation for single-module
TESTDATA_FILE_COUNT=$(find test/e2e/testdata -type f ! -name "fixtures.go" ! -name "bindata.go" 2>/dev/null | wc -l)
if [ "$TESTDATA_FILE_COUNT" -eq 0 ]; then
    echo "⚠️  WARNING: No testdata fixture files found in test/e2e/testdata"
    echo "This may cause test failures if tests use FixturePath() to load fixtures."
    echo "Verify that testdata-subfolder input was correct."
fi

Phase 4: Code Generation

🚨 CRITICAL: DO NOT MODIFY IMPORTS 🚨

The generated main.go file contains REQUIRED imports that MUST NOT be changed:

// REQUIRED IMPORTS - DO NOT MODIFY:
import (
    exutil "github.com/openshift/origin/test/extended/util"  // Provides CLI type
    compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"  // Provides helper functions
    // ...
)

func main() {
    exutil.InitStandardFlags()  // Initialize flags
    // ...
    componentSpecs.AddBeforeAll(func() {
        if err := compat_otp.InitTest(false); err != nil {  // Initialize OTE framework
            panic(err)
        }
    })
}

Why these imports are required:

  • exutil provides InitStandardFlags() to register kubeconfig flags and the CLI type
  • compat_otp provides InitTest() to initialize the test framework and helper functions like NewCLI()
  • Both packages exist in github.com/openshift/origin and serve different purposes
  • The compat_otp package is a REAL package path, NOT a placeholder

DO NOT:

  • ❌ Change *exutil.CLI to *compat_otp.CLI (compat_otp.CLI type doesn't exist)
  • ❌ Remove either import - both are required
  • ❌ Remove the import aliases
  • ❌ "Fix" what you think are incorrect imports

Verification checks are included in the template generation to catch any modifications.

Step 1: Generate/Update go.mod Files

For Monorepo Strategy:

cd <working-dir>

# Add OTE test dependencies to root go.mod (single module approach)
echo "Adding OTE test dependencies to root go.mod..."

# Add dependencies
OTE_LATEST=$(git ls-remote https://github.com/openshift-eng/openshift-tests-extension.git refs/heads/main | awk '{print $1}')
OTE_SHORT="${OTE_LATEST:0:12}"

echo "Adding OTE dependency..."
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift-eng/openshift-tests-extension@$OTE_SHORT"; then
    echo "❌ Failed to get openshift-tests-extension"
    echo "Retrying..."
    sleep 2
    if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift-eng/openshift-tests-extension@$OTE_SHORT"; then
        echo "❌ Failed after retry - you may need to run manually: go get github.com/openshift-eng/openshift-tests-extension@latest"
        exit 1
    fi
fi
echo "✅ OTE dependency added"

echo "Adding origin dependency..."
# Use a known working version instead of @main to avoid breaking changes
ORIGIN_VERSION="v1.5.0-alpha.3.0.20260310231025-5d3fd0545b5d"
if ! GOTOOLCHAIN=auto GOSUMDB=off go get "github.com/openshift/origin@$ORIGIN_VERSION"; then
    echo "❌ Failed to get github.com/openshift/origin@$ORIGIN_VERSION"
    echo "Retrying..."
    sleep 2
    if ! GOTOOLCHAIN=auto GOSUMDB=off go get "github.com/openshift/origin@$ORIGIN_VERSION"; then
        echo "❌ Failed after retry - you may need to run manually: GOSUMDB=off go get github.com/openshift/origin@$ORIGIN_VERSION"
        exit 1
    fi
fi
echo "✅ Origin dependency added"

echo "Adding Ginkgo dependency..."
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/ginkgo/v2@latest; then
    echo "❌ Failed to get ginkgo"
    echo "Retrying..."
    sleep 2
    if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/ginkgo/v2@latest; then
        echo "❌ Failed after retry - you may need to run manually: go get github.com/onsi/ginkgo/v2@latest"
        exit 1
    fi
fi
echo "✅ Ginkgo dependency added"

echo "Adding Gomega dependency..."
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/gomega@latest; then
    echo "❌ Failed to get gomega"
    echo "Retrying..."
    sleep 2
    if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/gomega@latest; then
        echo "❌ Failed after retry - you may need to run manually: go get github.com/onsi/gomega@latest"
        exit 1
    fi
fi
echo "✅ Gomega dependency added"

# Pin opencontainers dependencies to compatible versions BEFORE go mod tidy
# This prevents go mod tidy from upgrading to incompatible versions
echo "Pinning opencontainers dependencies to compatible versions..."
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/cyphar/filepath-securejoin@v0.4.1
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/opencontainers/runtime-spec@v1.2.0
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/opencontainers/cgroups@v0.0.3
echo "✅ Pinned cyphar/filepath-securejoin to v0.4.1"
echo "✅ Pinned opencontainers/runtime-spec to v1.2.0"
echo "✅ Pinned opencontainers/cgroups to v0.0.3"

# Copy replace directives from openshift-tests-private to root go.mod
# IMPORTANT: Filter out openshift-tests-private itself to avoid importing entire test suite
echo "Copying replace directives from openshift-tests-private..."

if [ -n "$SOURCE_REPO" ]; then
    grep -A 1000 "^replace" "$SOURCE_REPO/go.mod" | grep -B 1000 "^)" | \
        grep -v "^replace" | grep -v "^)" | \
        grep -v "github.com/openshift/openshift-tests-private" > /tmp/replace_directives.txt

    # Check if replace block already exists in root go.mod
    if ! grep -q "^replace (" go.mod; then
        echo "" >> go.mod
        echo "replace (" >> go.mod
        cat /tmp/replace_directives.txt >> go.mod
        echo ")" >> go.mod
    else
        # Append to existing replace block (before closing parenthesis)
        # Find the line number of the closing ) for replace block
        REPLACE_CLOSE_LINE=$(grep -n "^replace (" go.mod | head -1 | cut -d: -f1)
        # Find next closing ) after replace (
        NEXT_CLOSE=$(tail -n +$((REPLACE_CLOSE_LINE + 1)) go.mod | grep -n "^)" | head -1 | cut -d: -f1)
        REPLACE_CLOSE_LINE=$((REPLACE_CLOSE_LINE + NEXT_CLOSE))

        # Insert before closing )
        head -n $((REPLACE_CLOSE_LINE - 1)) go.mod > /tmp/go.mod.tmp
        cat /tmp/replace_directives.txt >> /tmp/go.mod.tmp
        tail -n +$REPLACE_CLOSE_LINE go.mod >> /tmp/go.mod.tmp
        mv /tmp/go.mod.tmp go.mod
    fi
    rm -f /tmp/replace_directives.txt
fi

# Step 4b: Align Ginkgo version with OTE framework (newer version is backward compatible)
# IMPORTANT: Use OTE's Ginkgo version (December 2024), NOT OTP's older version (August 2024)
# The December 2024 fork is backward compatible with August 2024 code from OTP
echo "Aligning Ginkgo version with OTE framework..."
OTE_REPO="https://github.com/openshift-eng/openshift-tests-extension.git"
OTE_GINKGO_VERSION=$(git ls-remote "$OTE_REPO" refs/heads/main | xargs -I {} git ls-remote https://github.com/openshift-eng/openshift-tests-extension {} | git archive --remote=https://github.com/openshift-eng/openshift-tests-extension HEAD go.mod 2>/dev/null | tar -xO | grep "github.com/onsi/ginkgo/v2 =>" | awk '{print $NF}' 2>/dev/null || echo "v2.6.1-0.20241205171354-8006f302fd12")

# Fallback to known working version if detection fails
if [ -z "$OTE_GINKGO_VERSION" ]; then
    OTE_GINKGO_VERSION="v2.6.1-0.20241205171354-8006f302fd12"
    echo "ℹ️  Using fallback OTE Ginkgo version: $OTE_GINKGO_VERSION"
else
    echo "ℹ️  Detected OTE Ginkgo version: $OTE_GINKGO_VERSION"
fi

GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift/onsi-ginkgo/v2@$OTE_GINKGO_VERSION"
echo "✅ Ginkgo aligned to OTE framework version (backward compatible with OTP)"

echo "✅ Monorepo go.mod setup complete (single module with all dependencies)"

For Single-Module Strategy:

cd <working-dir>/tests-extension

# Extract Go version from target repo or use default
if [ -f "$TARGET_REPO/go.mod" ]; then
    GO_VERSION=$(grep '^go ' "$TARGET_REPO/go.mod" | awk '{print $2}')
else
    GO_VERSION="1.21"
fi

go mod init github.com/openshift/<extension-name>-tests-extension
sed -i "s/^go .*/go $GO_VERSION/" go.mod

# Add dependencies
OTE_LATEST=$(git ls-remote https://github.com/openshift-eng/openshift-tests-extension.git refs/heads/main | awk '{print $1}')
OTE_SHORT="${OTE_LATEST:0:12}"

echo "Adding OTE dependency..."
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift-eng/openshift-tests-extension@$OTE_SHORT"; then
    echo "❌ Failed to get openshift-tests-extension"
    echo "Retrying..."
    sleep 2
    if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift-eng/openshift-tests-extension@$OTE_SHORT"; then
        echo "❌ Failed after retry - you may need to run manually: go get github.com/openshift-eng/openshift-tests-extension@latest"
        exit 1
    fi
fi
echo "✅ OTE dependency added"

echo "Adding origin dependency..."
# Use a known working version instead of @main to avoid breaking changes
ORIGIN_VERSION="v1.5.0-alpha.3.0.20260310231025-5d3fd0545b5d"
if ! GOTOOLCHAIN=auto GOSUMDB=off go get "github.com/openshift/origin@$ORIGIN_VERSION"; then
    echo "❌ Failed to get github.com/openshift/origin@$ORIGIN_VERSION"
    echo "Retrying..."
    sleep 2
    if ! GOTOOLCHAIN=auto GOSUMDB=off go get "github.com/openshift/origin@$ORIGIN_VERSION"; then
        echo "❌ Failed after retry - you may need to run manually: GOSUMDB=off go get github.com/openshift/origin@$ORIGIN_VERSION"
        exit 1
    fi
fi
echo "✅ Origin dependency added"

echo "Adding Ginkgo dependency..."
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/ginkgo/v2@latest; then
    echo "❌ Failed to get ginkgo"
    echo "Retrying..."
    sleep 2
    if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/ginkgo/v2@latest; then
        echo "❌ Failed after retry - you may need to run manually: go get github.com/onsi/ginkgo/v2@latest"
        exit 1
    fi
fi
echo "✅ Ginkgo dependency added"

echo "Adding Gomega dependency..."
if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/gomega@latest; then
    echo "❌ Failed to get gomega"
    echo "Retrying..."
    sleep 2
    if ! GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/onsi/gomega@latest; then
        echo "❌ Failed after retry - you may need to run manually: go get github.com/onsi/gomega@latest"
        exit 1
    fi
fi
echo "✅ Gomega dependency added"

# Pin opencontainers dependencies to compatible versions BEFORE go mod tidy
# This prevents go mod tidy from upgrading to incompatible versions
echo "Pinning opencontainers dependencies to compatible versions..."
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/cyphar/filepath-securejoin@v0.4.1
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/opencontainers/runtime-spec@v1.2.0
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get github.com/opencontainers/cgroups@v0.0.3
echo "✅ Pinned cyphar/filepath-securejoin to v0.4.1"
echo "✅ Pinned opencontainers/runtime-spec to v1.2.0"
echo "✅ Pinned opencontainers/cgroups to v0.0.3"

# Copy replace directives
# IMPORTANT: Filter out openshift-tests-private itself to avoid importing entire test suite
SOURCE_PATH="../$SOURCE_REPO"

grep -A 1000 "^replace" "$SOURCE_PATH/go.mod" | grep -B 1000 "^)" | \
    grep -v "^replace" | grep -v "^)" | \
    grep -v "github.com/openshift/openshift-tests-private" > /tmp/replace_directives.txt

echo "" >> go.mod
echo "replace (" >> go.mod
cat /tmp/replace_directives.txt >> go.mod
echo ")" >> go.mod
rm -f /tmp/replace_directives.txt

# Step 4b: Align Ginkgo version with OTE framework (newer version is backward compatible)
# IMPORTANT: Use OTE's Ginkgo version (December 2024), NOT OTP's older version (August 2024)
# The December 2024 fork is backward compatible with August 2024 code from OTP
echo "Aligning Ginkgo version with OTE framework..."
OTE_REPO="https://github.com/openshift-eng/openshift-tests-extension.git"
OTE_GINKGO_VERSION=$(git ls-remote "$OTE_REPO" refs/heads/main | xargs -I {} git ls-remote https://github.com/openshift-eng/openshift-tests-extension {} | git archive --remote=https://github.com/openshift-eng/openshift-tests-extension HEAD go.mod 2>/dev/null | tar -xO | grep "github.com/onsi/ginkgo/v2 =>" | awk '{print $NF}' 2>/dev/null || echo "v2.6.1-0.20241205171354-8006f302fd12")

# Fallback to known working version if detection fails
if [ -z "$OTE_GINKGO_VERSION" ]; then
    OTE_GINKGO_VERSION="v2.6.1-0.20241205171354-8006f302fd12"
    echo "ℹ️  Using fallback OTE Ginkgo version: $OTE_GINKGO_VERSION"
else
    echo "ℹ️  Detected OTE Ginkgo version: $OTE_GINKGO_VERSION"
fi

GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go get "github.com/openshift/onsi-ginkgo/v2@$OTE_GINKGO_VERSION"
echo "✅ Ginkgo aligned to OTE framework version (backward compatible with OTP)"

# Generate minimal go.sum
GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go mod download || echo "⚠️  Will retry in Phase 6"

cd ..

Step 2: Generate Extension Binary (main.go)

For Monorepo Strategy:

IMPORTANT:

  • CMD Location: cmd/extension/main.go (at repository root, NOT under test/)
  • NO sig filtering logic
  • Single module approach: imports test package from same module
cd <working-dir>
MODULE_NAME=$(grep '^module ' go.mod | awk '{print $2}')

# Re-derive variables from Phase 1/3 using IDENTICAL logic
# (Variables don't persist between phases - need to re-calculate)

# EXTENSION_NAME: Use same logic as Phase 1 Input 4
if [ -d ".git" ]; then
    DISCOVERED_REMOTE=$(git remote -v | head -1 | awk '{print $1}')
    if [ -n "$DISCOVERED_REMOTE" ]; then
        REMOTE_URL=$(git remote get-url "$DISCOVERED_REMOTE" 2>/dev/null)
        EXTENSION_NAME=$(echo "$REMOTE_URL" | sed 's/.*[:/]\([^/]*\)\/\([^/]*\)\.git$/\2/' | sed 's/\.git$//')
    else
        EXTENSION_NAME=$(basename "$(pwd)")
    fi
else
    EXTENSION_NAME=$(basename "$(pwd)")
fi

# TARGET_TEST_DIR_NAME: Detect from filesystem (user-created directory from Phase 3)
TARGET_TEST_DIR_NAME=""
if [ -d "test/e2e" ]; then
    # Check if test/e2e has subdirectories besides testdata
    SUBDIRS=$(find test/e2e -mindepth 1 -maxdepth 1 -type d ! -name testdata 2>/dev/null)
    if [ -n "$SUBDIRS" ]; then
        # Has subdirectories - find the one with Go test files
        for dir in $SUBDIRS; do
            if ls "$dir"/*_test.go >/dev/null 2>&1; then
                TARGET_TEST_DIR_NAME=$(basename "$dir")
                break
            fi
        done
    fi
fi

# Determine test import path based on whether test/e2e has subdirectory
if [ -n "$TARGET_TEST_DIR_NAME" ]; then
    # test/e2e exists with subdirectory (e.g., github.com/openshift/router/test/e2e/extension)
    TEST_IMPORT="$MODULE_NAME/test/e2e/$TARGET_TEST_DIR_NAME"
    TEST_FILTER_PATH="/test/e2e/$TARGET_TEST_DIR_NAME/"
    echo "Tests are at: test/e2e/$TARGET_TEST_DIR_NAME/"
else
    # No subdirectory - use test/e2e directly (e.g., github.com/openshift/router/test/e2e)
    TEST_IMPORT="$MODULE_NAME/test/e2e"
    TEST_FILTER_PATH="/test/e2e/"
    echo "Tests are at: test/e2e/"
fi

# Create main.go at cmd/extension/main.go
cat > "cmd/extension/main.go" << 'EOF'
package main

import (
    "fmt"
    "os"
    "regexp"
    "strings"

    "github.com/spf13/cobra"
    "k8s.io/component-base/logs"

    "github.com/openshift-eng/openshift-tests-extension/pkg/cmd"
    e "github.com/openshift-eng/openshift-tests-extension/pkg/extension"
    et "github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests"
    g "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo"
    "github.com/openshift/origin/test/extended/util"
    compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"
    framework "k8s.io/kubernetes/test/e2e/framework"

    // Import testdata package from same module
    _ "<TEST_IMPORT>/testdata"

    // Import test packages from same module
    _ "<TEST_IMPORT>"
)

func main() {
    // Initialize test framework flags (required for kubeconfig, provider, etc.)
    util.InitStandardFlags()
    framework.AfterReadingAllFlags(&framework.TestContext)

    logs.InitLogs()
    defer logs.FlushLogs()

    registry := e.NewRegistry()
    ext := e.NewExtension("openshift", "payload", "<extension-name>")

    // Register test suites (parallel, serial, disruptive, all)
    registerSuites(ext)

    // Build test specs from Ginkgo
    // Note: ModuleTestsOnly() is applied by default, which filters out /vendor/ and k8s.io/kubernetes tests
    allSpecs, err := g.BuildExtensionTestSpecsFromOpenShiftGinkgoSuite()
    if err != nil {
        panic(fmt.Sprintf("couldn't build extension test specs from ginkgo: %+v", err.Error()))
    }

    // Filter to only include tests from this module's test directory
    // Excludes tests from /go/pkg/mod/ (module cache) and /vendor/
    componentSpecs := allSpecs.Select(func(spec *et.ExtensionTestSpec) bool {
        for _, loc := range spec.CodeLocations {
            // Include tests from local test directory (not from module cache or vendor)
            if strings.Contains(loc, "<TEST_FILTER_PATH>") && !strings.Contains(loc, "/go/pkg/mod/") && !strings.Contains(loc, "/vendor/") {
                return true
            }
        }
        return false
    })

    // Initialize test framework before all tests
    componentSpecs.AddBeforeAll(func() {
        if err := compat_otp.InitTest(false); err != nil {
            panic(err)
        }
        // Set testsStarted = true to allow OTP functions like oc.Run() to work
        // WithCleanup sets this flag and it remains true for all subsequent tests
        util.WithCleanup(func() {
            // Empty function - we just need WithCleanup to set testsStarted = true
        })
    })

    // Process all specs
    componentSpecs.Walk(func(spec *et.ExtensionTestSpec) {
        // Apply platform filters based on Platform: labels
        for label := range spec.Labels {
            if strings.HasPrefix(label, "Platform:") {
                platformName := strings.TrimPrefix(label, "Platform:")
                spec.Include(et.PlatformEquals(platformName))
            }
        }

        // Apply platform filters based on [platform:xxx] in test names
        re := regexp.MustCompile(`\[platform:([a-z]+)\]`)
        if match := re.FindStringSubmatch(spec.Name); match != nil {
            platform := match[1]
            spec.Include(et.PlatformEquals(platform))
        }

        // Set lifecycle to Informing
        spec.Lifecycle = et.LifecycleInforming
    })

    // Add filtered component specs to extension
    ext.AddSpecs(componentSpecs)

    registry.Register(ext)

    root := &cobra.Command{
        Long: "<Extension Name> Tests",
    }

    root.AddCommand(cmd.DefaultExtensionCommands(registry)...)

    if err := func() error {
        return root.Execute()
    }(); err != nil {
        os.Exit(1)
    }
}

// registerSuites registers test suites with proper categorization
func registerSuites(ext *e.Extension) {
    suites := []e.Suite{
        {
            Name: "<extension-name>/conformance/parallel",
            Parents: []string{
                "openshift/conformance/parallel",
            },
            Description: "Parallel conformance tests (Level0, non-serial, non-disruptive)",
            Qualifiers: []string{
                `name.contains("[Level0]") && !(name.contains("[Serial]") || name.contains("[Disruptive]"))`,
            },
        },
        {
            Name: "<extension-name>/conformance/serial",
            Parents: []string{
                "openshift/conformance/serial",
            },
            Description: "Serial conformance tests (must run sequentially)",
            Qualifiers: []string{
                `name.contains("[Level0]") && name.contains("[Serial]") && !name.contains("[Disruptive]")`,
            },
        },
        {
            Name:        "<extension-name>/disruptive",
            Parents:     []string{"openshift/disruptive"},
            Description: "Disruptive tests (may affect cluster state)",
            Qualifiers: []string{
                `name.contains("[Disruptive]")`,
            },
        },
        {
            Name:        "<extension-name>/non-disruptive",
            Description: "All non-disruptive tests (safe for development clusters)",
            Qualifiers: []string{
                `!name.contains("[Disruptive]")`,
            },
        },
        {
            Name:        "<extension-name>/all",
            Description: "All <extension-name> tests",
            // No qualifiers means all tests from this extension will be included
        },
    }

    for _, suite := range suites {
        ext.AddSuite(suite)
    }
}
EOF

# Replace placeholders
sed -i "s|<TEST_IMPORT>|$TEST_IMPORT|g" "cmd/extension/main.go"
sed -i "s|<TEST_FILTER_PATH>|$TEST_FILTER_PATH|g" "cmd/extension/main.go"
sed -i "s|<extension-name>|$EXTENSION_NAME|g" "cmd/extension/main.go"
sed -i "s|<Extension Name>|${EXTENSION_NAME^}|g" "cmd/extension/main.go"
sed -i "s|<MODULE_PATH>|$MODULE_NAME|g" "cmd/extension/main.go"

echo "✅ Created cmd/extension/main.go"

# CRITICAL VERIFICATION: Imports must be EXACTLY as templated
echo "🔍 Verifying critical imports in cmd/extension/main.go..."
if ! grep -q 'compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"' "cmd/extension/main.go"; then
    echo "❌ CRITICAL ERROR: compat_otp import is missing or modified"
    echo "   The import MUST be: compat_otp \"github.com/openshift/origin/test/extended/util/compat_otp\""
    echo "   DO NOT change this to exutil or any other alias"
    exit 1
fi
if ! grep -q '"github.com/openshift/origin/test/extended/util"' "cmd/extension/main.go"; then
    echo "❌ CRITICAL ERROR: util import is missing"
    echo "   Both util and compat_otp imports are REQUIRED"
    exit 1
fi
if ! grep -q 'util\.InitStandardFlags()' "cmd/extension/main.go"; then
    echo "❌ CRITICAL ERROR: util.InitStandardFlags() call is missing or modified"
    echo "   MUST use 'util.InitStandardFlags()', NOT 'exutil.InitStandardFlags()'"
    exit 1
fi
if ! grep -q 'compat_otp\.InitTest' "cmd/extension/main.go"; then
    echo "❌ CRITICAL ERROR: compat_otp.InitTest() call is missing or modified"
    echo "   MUST use 'compat_otp.InitTest(false)', NOT 'exutil.InitTest()' or 'util.InitTest()'"
    exit 1
fi
echo "✅ All critical imports and function calls verified"

For Single-Module Strategy:

cd <working-dir>/tests-extension

cat > cmd/main.go << 'EOF'
package main

import (
    "fmt"
    "os"
    "regexp"
    "strings"

    "github.com/spf13/cobra"
    "k8s.io/component-base/logs"

    "github.com/openshift-eng/openshift-tests-extension/pkg/cmd"
    e "github.com/openshift-eng/openshift-tests-extension/pkg/extension"
    et "github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests"
    g "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo"
    "github.com/openshift/origin/test/extended/util"
    compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"
    framework "k8s.io/kubernetes/test/e2e/framework"

    // Import testdata package from this module
    _ "github.com/openshift/<extension-name>-tests-extension/test/e2e/testdata"

    // Import test packages from this module
    _ "github.com/openshift/<extension-name>-tests-extension/test/e2e"
)

func main() {
    // Initialize test framework flags (required for kubeconfig, provider, etc.)
    util.InitStandardFlags()
    framework.AfterReadingAllFlags(&framework.TestContext)

    logs.InitLogs()
    defer logs.FlushLogs()

    registry := e.NewRegistry()
    ext := e.NewExtension("openshift", "payload", "<extension-name>")

    // Register test suites (parallel, serial, disruptive, all)
    registerSuites(ext)

    // Build test specs from Ginkgo
    // Note: ModuleTestsOnly() is applied by default, which filters out /vendor/ and k8s.io/kubernetes tests
    allSpecs, err := g.BuildExtensionTestSpecsFromOpenShiftGinkgoSuite()
    if err != nil {
        panic(fmt.Sprintf("couldn't build extension test specs from ginkgo: %+v", err.Error()))
    }

    // Filter to only include tests from this module's test/e2e/ directory
    // Excludes tests from /go/pkg/mod/ (module cache) and /vendor/
    componentSpecs := allSpecs.Select(func(spec *et.ExtensionTestSpec) bool {
        for _, loc := range spec.CodeLocations {
            // Include tests from local test/e2e/ directory (not from module cache or vendor)
            if strings.Contains(loc, "/test/e2e/") && !strings.Contains(loc, "/go/pkg/mod/") && !strings.Contains(loc, "/vendor/") {
                return true
            }
        }
        return false
    })

    // Initialize test framework before all tests
    componentSpecs.AddBeforeAll(func() {
        if err := compat_otp.InitTest(false); err != nil {
            panic(err)
        }
        // Set testsStarted = true to allow OTP functions like oc.Run() to work
        // WithCleanup sets this flag and it remains true for all subsequent tests
        util.WithCleanup(func() {
            // Empty function - we just need WithCleanup to set testsStarted = true
        })
    })

    // Process all specs
    componentSpecs.Walk(func(spec *et.ExtensionTestSpec) {
        // Apply platform filters based on Platform: labels
        for label := range spec.Labels {
            if strings.HasPrefix(label, "Platform:") {
                platformName := strings.TrimPrefix(label, "Platform:")
                spec.Include(et.PlatformEquals(platformName))
            }
        }

        // Apply platform filters based on [platform:xxx] in test names
        re := regexp.MustCompile(`\[platform:([a-z]+)\]`)
        if match := re.FindStringSubmatch(spec.Name); match != nil {
            platform := match[1]
            spec.Include(et.PlatformEquals(platform))
        }

        // Set lifecycle to Informing
        spec.Lifecycle = et.LifecycleInforming
    })

    // Add filtered component specs to extension
    ext.AddSpecs(componentSpecs)

    registry.Register(ext)

    root := &cobra.Command{
        Long: "<Extension Name> Tests",
    }

    root.AddCommand(cmd.DefaultExtensionCommands(registry)...)

    if err := func() error {
        return root.Execute()
    }(); err != nil {
        os.Exit(1)
    }
}

// registerSuites registers test suites with proper categorization
func registerSuites(ext *e.Extension) {
    suites := []e.Suite{
        {
            Name: "<extension-name>/conformance/parallel",
            Parents: []string{
                "openshift/conformance/parallel",
            },
            Description: "Parallel conformance tests (Level0, non-serial, non-disruptive)",
            Qualifiers: []string{
                `name.contains("[Level0]") && !(name.contains("[Serial]") || name.contains("[Disruptive]"))`,
            },
        },
        {
            Name: "<extension-name>/conformance/serial",
            Parents: []string{
                "openshift/conformance/serial",
            },
            Description: "Serial conformance tests (must run sequentially)",
            Qualifiers: []string{
                `name.contains("[Level0]") && name.contains("[Serial]") && !name.contains("[Disruptive]")`,
            },
        },
        {
            Name:        "<extension-name>/disruptive",
            Parents:     []string{"openshift/disruptive"},
            Description: "Disruptive tests (may affect cluster state)",
            Qualifiers: []string{
                `name.contains("[Disruptive]")`,
            },
        },
        {
            Name:        "<extension-name>/non-disruptive",
            Description: "All non-disruptive tests (safe for development clusters)",
            Qualifiers: []string{
                `!name.contains("[Disruptive]")`,
            },
        },
        {
            Name:        "<extension-name>/all",
            Description: "All <extension-name> tests",
            // No qualifiers means all tests from this extension will be included
        },
    }

    for _, suite := range suites {
        ext.AddSuite(suite)
    }
}
EOF

sed -i "s|<extension-name>|$EXTENSION_NAME|g" cmd/main.go
sed -i "s|<Extension Name>|${EXTENSION_NAME^}|g" cmd/main.go
sed -i "s|<Extension Name>|${EXTENSION_NAME^}|g" cmd/main.go

echo "✅ Created cmd/main.go"

# CRITICAL VERIFICATION: Imports must be EXACTLY as templated
echo "🔍 Verifying critical imports in cmd/main.go..."
if ! grep -q 'compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"' "cmd/main.go"; then
    echo "❌ CRITICAL ERROR: compat_otp import is missing or modified"
    echo "   The import MUST be: compat_otp \"github.com/openshift/origin/test/extended/util/compat_otp\""
    echo "   DO NOT change this to exutil or any other alias"
    exit 1
fi
if ! grep -q '"github.com/openshift/origin/test/extended/util"' "cmd/main.go"; then
    echo "❌ CRITICAL ERROR: util import is missing"
    echo "   Both util and compat_otp imports are REQUIRED"
    exit 1
fi
if ! grep -q 'util\.InitStandardFlags()' "cmd/main.go"; then
    echo "❌ CRITICAL ERROR: util.InitStandardFlags() call is missing or modified"
    echo "   MUST use 'util.InitStandardFlags()', NOT 'exutil.InitStandardFlags()'"
    exit 1
fi
if ! grep -q 'compat_otp\.InitTest' "cmd/main.go"; then
    echo "❌ CRITICAL ERROR: compat_otp.InitTest() call is missing or modified"
    echo "   MUST use 'compat_otp.InitTest(false)', NOT 'exutil.InitTest()' or 'util.InitTest()'"
    exit 1
fi
echo "✅ All critical imports and function calls verified"

Step 3: Create bindata.mk

For Monorepo:

cd <working-dir>

# Re-derive directory paths from Phase 3
# (Variables don't persist between phases - need to re-calculate)
if [ -d "test/e2e" ]; then
    # Check if test/e2e has subdirectories besides testdata
    SUBDIRS=$(find test/e2e -mindepth 1 -maxdepth 1 -type d ! -name testdata 2>/dev/null)
    if [ -n "$SUBDIRS" ]; then
        TESTDATA_DIR=""
        # Has subdirectories - find the one with testdata
        for dir in $SUBDIRS; do
            if [ -d "$dir/testdata" ]; then
                TESTDATA_DIR="$dir/testdata"
                break
            fi
        done
        # Fallback if no matching subdir had testdata
        if [ -z "$TESTDATA_DIR" ]; then
            TESTDATA_DIR="test/e2e/testdata"
        fi
    else
        # No subdirectories - use test/e2e/testdata directly
        TESTDATA_DIR="test/e2e/testdata"
    fi
else
    echo "❌ Cannot find test/e2e directory"
    exit 1
fi

echo "Using testdata directory: $TESTDATA_DIR"

# bindata.mk location: at root for single-module monorepo approach
cat > "bindata.mk" << 'EOF'
TESTDATA_PATH := <TESTDATA_DIR>
GOPATH ?= $(shell go env GOPATH)
GO_BINDATA := $(GOPATH)/bin/go-bindata

$(GO_BINDATA):
	@echo "Installing go-bindata..."
	@GOFLAGS= go install github.com/go-bindata/go-bindata/v3/go-bindata@latest

.PHONY: update-bindata
update-bindata: $(GO_BINDATA)
	@echo "Generating bindata for testdata files..."
	$(GO_BINDATA) \
		-nocompress \
		-nometadata \
		-prefix "<TESTDATA_DIR>" \
		-pkg testdata \
		-o <TESTDATA_DIR>/bindata.go \
		<TESTDATA_DIR>/...
	@gofmt -s -w <TESTDATA_DIR>/bindata.go
	@echo "✅ Bindata generated successfully"

.PHONY: verify-bindata
verify-bindata: update-bindata
	@echo "Verifying bindata is up to date..."
	git diff --exit-code $(TESTDATA_PATH)/bindata.go || (echo "❌ Bindata is out of date" && exit 1)
	@echo "✅ Bindata is up to date"

.PHONY: bindata
bindata: clean-bindata update-bindata

.PHONY: clean-bindata
clean-bindata:
	@echo "Cleaning bindata..."
	@rm -f $(TESTDATA_PATH)/bindata.go
EOF

# Replace placeholders
sed -i "s|<TESTDATA_DIR>|$TESTDATA_DIR|g" "bindata.mk"

echo "✅ Created bindata.mk at root"

For Single-Module:

cd <working-dir>/tests-extension

cat > test/e2e/bindata.mk << 'EOF'
TESTDATA_PATH := testdata
GOPATH ?= $(shell go env GOPATH)
GO_BINDATA := $(GOPATH)/bin/go-bindata

$(GO_BINDATA):
	@echo "Installing go-bindata..."
	@GOFLAGS= go install github.com/go-bindata/go-bindata/v3/go-bindata@latest

.PHONY: update-bindata
update-bindata: $(GO_BINDATA)
	@echo "Generating bindata..."
	@mkdir -p $(TESTDATA_PATH)
	$(GO_BINDATA) -nocompress -nometadata \
		-pkg testdata -o $(TESTDATA_PATH)/bindata.go -prefix "testdata" $(TESTDATA_PATH)/...
	@gofmt -s -w $(TESTDATA_PATH)/bindata.go
	@echo "✅ Bindata generated successfully"

.PHONY: verify-bindata
verify-bindata: update-bindata
	@echo "Verifying bindata is up to date..."
	git diff --exit-code $(TESTDATA_PATH)/bindata.go || (echo "❌ Bindata is out of date" && exit 1)
	@echo "✅ Bindata is up to date"

.PHONY: bindata
bindata: clean-bindata update-bindata

.PHONY: clean-bindata
clean-bindata:
	@rm -f $(TESTDATA_PATH)/bindata.go
EOF

echo "✅ Created test/e2e/bindata.mk"

Step 4: Create/Update Makefile

For Monorepo Strategy:

IMPORTANT: Do NOT add tests-ext-compress or tests-ext-copy targets

cd <working-dir>

# Re-derive EXTENSION_NAME using IDENTICAL logic as Phase 1
# (Variables don't persist between phases - need to re-calculate)
if [ -d ".git" ]; then
    DISCOVERED_REMOTE=$(git remote -v | head -1 | awk '{print $1}')
    if [ -n "$DISCOVERED_REMOTE" ]; then
        REMOTE_URL=$(git remote get-url "$DISCOVERED_REMOTE" 2>/dev/null)
        EXTENSION_NAME=$(echo "$REMOTE_URL" | sed 's/.*[:/]\([^/]*\)\/\([^/]*\)\.git$/\2/' | sed 's/\.git$//')
    else
        EXTENSION_NAME=$(basename "$(pwd)")
    fi
else
    EXTENSION_NAME=$(basename "$(pwd)")
fi

if [ ! -f "Makefile" ]; then
    echo "❌ ERROR: No root Makefile found"
    exit 1
fi

if grep -q "tests-ext-build" Makefile; then
    echo "⚠️  OTE targets already exist, skipping..."
else
    # Single module approach - build from root
    cat >> Makefile << EOF

# OTE test extension binary configuration
TESTS_EXT_BINARY := bin/$EXTENSION_NAME-tests-ext

.PHONY: tests-ext-build
tests-ext-build:
	@echo "Building OTE test extension binary..."
	@\$(MAKE) -f bindata.mk update-bindata
	@mkdir -p bin
	GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go build -mod=vendor -o \$(TESTS_EXT_BINARY) ./cmd/extension
	@echo "✅ Extension binary built: \$(TESTS_EXT_BINARY)"

.PHONY: extension
extension: tests-ext-build

.PHONY: clean-extension
clean-extension:
	@echo "Cleaning extension binary..."
	@rm -f \$(TESTS_EXT_BINARY)
	@\$(MAKE) -f bindata.mk clean-bindata 2>/dev/null || true
EOF

    echo "✅ Root Makefile updated with OTE targets"
fi

For Single-Module:

cd <working-dir>/tests-extension

cat > Makefile << EOF
BINARY := bin/$EXTENSION_NAME-tests-ext

.PHONY: build
build:
	@echo "Building extension binary..."
	@cd test/e2e && \$(MAKE) -f bindata.mk update-bindata
	@mkdir -p bin
	GOTOOLCHAIN=auto GOSUMDB=sum.golang.org go build -o \$(BINARY) ./cmd
	@echo "✅ Binary built: \$(BINARY)"

.PHONY: clean
clean:
	@rm -f \$(BINARY)
	@cd test/e2e && \$(MAKE) -f bindata.mk clean-bindata

.PHONY: help
help:
	@echo "Available targets:"
	@echo "  build  - Build extension binary"
	@echo "  clean  - Remove binaries and bindata"
EOF

echo "✅ Created Makefile"

Step 5: Create fixtures.go

Create testdata/fixtures.go helper file:

For Monorepo:

cd <working-dir>

cat > "$TESTDATA_DIR/fixtures.go" << 'EOF'
package testdata

import (
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
    "sort"
    "strings"
)

var (
    fixtureDir string
)

func init() {
    var err error
    fixtureDir, err = ioutil.TempDir("", "testdata-fixtures-")
    if err != nil {
        panic(fmt.Sprintf("failed to create fixture directory: %v", err))
    }
    // Ensure fixture directory has proper permissions for all users
    if err := os.Chmod(fixtureDir, 0755); err != nil {
        panic(fmt.Sprintf("failed to set fixture directory permissions: %v", err))
    }
}

func FixturePath(elem ...string) string {
    relativePath := filepath.Join(elem...)
    targetPath

*Truncated - read the full file at https://github.com/openshift-eng/ai-helpers/blob/72044d97c253f2225f6ed929796ffe26a0f4edb8/plugins/ote-migration/skills/ote-migration-workflow/SKILL.

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/openshift-eng-ai-helpers-ote-migration-workflow/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

openshift-eng-ai-helpers-ote-migration-workflow.ocm.jsonjson
{
  "ocm": "1",
  "id": "openshift-eng-ai-helpers-ote-migration-workflow",
  "kind": "skill",
  "name": "ote-migration-workflow",
  "description": "Automated workflow for migrating OpenShift component repositories to OTE framework",
  "publisher": "openshift-eng",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "general"
    ],
    "tags": [
      "skill-md",
      "github"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "Automated workflow for migrating OpenShift component repositories to OTE framework"
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "github",
      "repository": "https://github.com/openshift-eng/ai-helpers",
      "path": "plugins/ote-migration/skills/ote-migration-workflow/SKILL.md",
      "ref": "72044d97c253f2225f6ed929796ffe26a0f4edb8",
      "url": "https://github.com/openshift-eng/ai-helpers/blob/72044d97c253f2225f6ed929796ffe26a0f4edb8/plugins/ote-migration/skills/ote-migration-workflow/SKILL.md",
      "key": "openshift-eng/ai-helpers/plugins/ote-migration/skills/ote-migration-workflow/SKILL.md"
    }
  },
  "instructions": "# OTE Migration Workflow Skill\n\nThis skill provides step-by-step implementation guidance for the complete OTE migration workflow.\n\n## When to Use This Skill\n\nUse this skill when executing the `/ote-migration:migrate` command to automate the migration of OpenShift component repositories to the openshift-tests-extension (OTE) framework.\n\n## Prerequisites\n\n- Go toolchain (1.21+)\n- Git installed and configured\n- Access to openshift-tests-private repository:\n  - **Option 1**: Existing local clone (with optional update)\n  - **Option 2**: Git credentials to clone from `git@github.com:openshift/opensh",
  "cost": {
    "context_tokens": 31325
  }
}

Fetch it by URL: GET /api/v1/registry/openshift-eng-ai-helpers-ote-migration-workflow/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.