test-plan-publish

Publish test plan artifacts to GitHub — creates a branch, commits all artifacts, and opens a PR with optional reviewer assignment. Use after test plan review to make artifacts available for team colla

SKILL.md

GitHub

Test Plan Publisher

Publish test plan artifacts to GitHub by creating a branch, committing all files, and opening a pull request.

Prerequisites

You must work from a personal fork, not the upstream repository:

  1. Fork opendatahub-io/opendatahub-test-plans to your GitHub account
  2. Clone your fork: git clone https://github.com/YOUR-USERNAME/opendatahub-test-plans ~/Code/opendatahub-test-plans
  3. Create test plan: /test-plan-create RHAISTRAT-XXX
  4. Generate test cases: /test-plan-create-cases
  5. Publish: /test-plan-publish - creates PR from your fork → upstream

The skill will verify you're working from a fork and fail if you're in the upstream repository.

Usage

/test-plan-publish [FEATURE_DIR] [--repo <owner/repo>] [--reviewers <user1,user2>]

Examples:

  • /test-plan-publish tool_calling_metadata
  • /test-plan-publish tool_calling_metadata --reviewers alice,bob
  • /test-plan-publish tool_calling_metadata --repo org/test-plans-repo
  • /test-plan-publish (auto-detects feature directory from prior /test-plan-create run)

Inputs

From arguments

Parse $ARGUMENTS to extract:

  1. First argument (optional): Test plan source - can be:
    • Local directory path: mcp_catalog or /path/to/mcp_catalog
    • GitHub branch: https://github.com/org/repo/tree/test-plan/RHAISTRAT-400
    • GitHub PR: https://github.com/org/repo/pull/5
  2. --repo (optional): Target GitHub repository in owner/repo format. Defaults to the current repository or detected from GitHub URL.
  3. --reviewers (optional): Comma-separated list of GitHub usernames to assign as PR reviewers. If not provided, uses the reviewers field from TestPlan.md frontmatter. If both are empty, no reviewers are assigned.

Auto-detection

If no feature directory is provided and a TestPlan.md was just generated by /test-plan-create in this session, use its feature directory automatically — proceed directly to Step 0.

Interactive fallback

If no feature directory can be determined, ask the user via AskUserQuestion:

Where is the test plan to publish?

You can provide:

  • Local directory path (e.g., ~/Code/opendatahub-test-plans/plans/ai-hub/mcp_catalog)
  • GitHub branch URL (e.g., https://github.com/org/repo/tree/test-plan/RHAISTRAT-400)
  • GitHub PR URL (e.g., https://github.com/org/repo/pull/5)

Process

Step 0: Pre-flight Checks

0.0 Python dependencies

Install the test-plan package (makes all scripts importable):

bash "${CLAUDE_SKILL_DIR}/../../scripts/bootstrap.sh" --layout "${CLAUDE_SKILL_DIR}" || exit 1
(cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv sync --extra dev)

If installation fails, inform the user and do NOT proceed. Once installed, all Python scripts will work from any directory.

0.1 GitHub CLI

Run gh auth status via Bash. If it fails, inform the user that gh CLI must be installed and authenticated. Do NOT proceed until this succeeds.

0.2 Locate Feature Directory

Use the shared locate-feature-dir utility to find the test plan:

result=$(cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/repo.py locate-feature-dir "<source>")
if [ $? -ne 0 ]; then
    echo "$result"
    exit 1
fi

# Parse JSON output
feature_dir=$(printf '%s\n' "$result" | jq -r '.feature_dir')
source_type=$(printf '%s\n' "$result" | jq -r '.source_type')

# If GitHub source, extract repo info for auto-detecting --repo flag
if [ "$source_type" = "github" ]; then
    repo_owner=$(printf '%s\n' "$result" | jq -r '.repo_owner')
    repo_name=$(printf '%s\n' "$result" | jq -r '.repo_name')
    # Auto-set target_repo if --repo flag wasn't provided
    [ -z "$target_repo" ] && target_repo="$repo_owner/$repo_name"
fi

0.3 Validate artifacts and frontmatter

Run unified validation on the feature directory. This checks structure (TestPlan.md, test_cases/), validates frontmatter for all present artifacts (TestPlan.md, TestPlanGaps.md if exists), and validates test case files (if any). If validation fails, show the errors and do NOT proceed.

(cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/validate.py all <feature_dir>)

Check that README.md exists. Do NOT fail if TestPlanGaps.md or test_cases/ are absent — the user may publish before generating test cases.

0.5 Check for clean working state

Check if there are uncommitted changes in the feature directory. This is informational only — warn the user if there are unstaged changes, but do not block.

# Check if feature_dir is in a git repo
if git -C <feature_dir> rev-parse --git-dir > /dev/null 2>&1; then
    changes=$(git -C <feature_dir> status --porcelain .)
    if [ -n "$changes" ]; then
        echo "⚠ Uncommitted changes detected:"
        echo "$changes"
    else
        echo "✓ Working directory is clean"
    fi
else
    echo "ℹ Not a git repository - skipping clean state check"
fi

Note: Use variable name changes instead of status (which is readonly in zsh).

Step 1: Read Metadata

  1. Read frontmatter from <feature_dir>/TestPlan.md using Bash:
    (cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/frontmatter.py read <feature_dir>/TestPlan.md)
    
  2. Extract: source_key, version, feature, reviewers
  3. Determine the branch name: test-plan/<source_key> (version not included)
    • Example: test-plan/RHAISTRAT-400
    • Rationale: Using version-free branch names allows /test-plan-update to push updates to the same PR instead of creating new PRs for each version bump
  4. Determine reviewers: use --reviewers argument if provided, otherwise use frontmatter reviewers field

Step 1.5: Determine Target Repository

IMPORTANT: Test plans must NOT be published to the skill repository.

  1. Export CLAUDE_SKILL_DIR for validation utilities:

    export CLAUDE_SKILL_DIR
    
  2. If --repo was provided in arguments:

    • Validate it's not the skill repo:
      if ! (cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/repo.py validate-remote "$provided_repo"); then
          echo "Suggested: opendatahub-io/opendatahub-test-plans"
          exit 1
      fi
      
    • Use it as target_repo
  3. If --repo was NOT provided: Ask user via AskUserQuestion for a text input (NOT a menu):

    Where should this test plan be published?

    Specify the target GitHub repository in owner/repo format, or press Enter for default: opendatahub-io/opendatahub-test-plans

  4. Validate the user-provided or default repository:

    • Check format (must be owner/repo)
    • Validate not skill repo:
      if ! (cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/repo.py validate-remote "$target_repo"); then
          echo "Please specify a different repository."
          # Ask again via AskUserQuestion
      fi
      
    • Retry until user provides a valid non-skill repo
  5. Store the final validated value in target_repo

Rationale: Prevents accidental (or intentional) publishing of test plans to the skill repository, which would pollute the skill codebase with test plan artifacts and cause branch-switching issues for contributors.

Step 1.6: Verify Working from a Fork

IMPORTANT: Check if the local repository is a personal fork, not the upstream repository.

  1. Get and normalize the git remote of the feature directory:

    cd <feature_dir>
    remote_url=$(git config --get remote.origin.url)
    
    # Normalize: remove .git suffix and all trailing slashes
    normalized=$(echo "$remote_url" | sed -E 's|\.git$||; s|/+$||')
    
    # Extract owner/repo — portable across bash, zsh, sh, dash, ksh
    if ! echo "$normalized" | grep -qE 'github\.com[:/][^/]+/[^/]+$'; then
        echo "❌ Cannot parse GitHub repository from remote URL: $remote_url"
        exit 1
    fi
    owner=$(echo "$normalized" | sed -E 's|.*github\.com[:/]([^/]+)/[^/]+$|\1|')
    repo=$(echo "$normalized"  | sed -E 's|.*github\.com[:/][^/]+/([^/]+)$|\1|')
    remote_repo="${owner}/${repo}"
    
    # Lowercase for case-insensitive comparison
    remote_repo_lower=$(echo "$remote_repo" | tr '[:upper:]' '[:lower:]')
    
  2. Check if it's the upstream repository:

    if [ "$remote_repo_lower" = "opendatahub-io/opendatahub-test-plans" ]; then
        echo "❌ You are working directly in the upstream repository."
        echo ""
        echo "You must work from a personal fork to contribute:"
        echo "  1. Fork opendatahub-io/opendatahub-test-plans on GitHub"
        echo "  2. Clone YOUR fork: git clone https://github.com/YOUR-USERNAME/opendatahub-test-plans ~/Code/opendatahub-test-plans"
        echo "  3. Re-run /test-plan-create in your fork"
        echo ""
        exit 1
    fi
    
  3. If it's a fork, proceed. The PR will be created from the fork → upstream.

Rationale: Users cannot push branches directly to opendatahub-io/opendatahub-test-plans. They must work from a fork and create a PR from fork → upstream.

Step 2: Confirm with User

Before creating the branch and PR, present a summary to the user via AskUserQuestion:

Ready to publish test plan

  • Feature: <feature>
  • Strategy: <source_key>
  • Version: <version>
  • Branch: test-plan/<source_key>
  • Target repo: <target_repo>
  • Reviewers: <reviewer list> (or "none")
  • Files to publish:
    • <feature_dir>/TestPlan.md
    • <feature_dir>/README.md
    • <feature_dir>/TestPlanGaps.md (if exists)
    • <feature_dir>/test_cases/ (N files, if exists)

Proceed? (yes/no)

If the user declines, stop.

Step 3: Update Frontmatter

  1. Bump the status to In Review:
    (cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/frontmatter.py set <feature_dir>/TestPlan.md status="In Review")
    

Step 4: Create Branch and Commit

  1. Ensure we're in the correct working directory:

    # Get absolute path of feature directory's parent (the repo root)
    feature_dir_abs=$(cd "$(dirname "$feature_dir")" && pwd)/$(basename "$feature_dir")
    publish_repo_root=$(dirname "$feature_dir_abs")
    
    # Export CLAUDE_SKILL_DIR so functions in the script can use it
    export CLAUDE_SKILL_DIR
    
    # Check if we're in the skill repo
    current_repo=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
    
    # Get skill repo root
    skill_parent="${CLAUDE_SKILL_DIR}/../.."
    skill_repo_root=$(cd -P "$skill_parent" && git rev-parse --show-toplevel 2>/dev/null || echo "")
    
    if [ -n "$current_repo" ] && [ -n "$skill_repo_root" ] && [ "$current_repo" = "$skill_repo_root" ]; then
        echo "⚠ Currently in skill repository, switching to publish directory"
    fi
    
    # Always cd to publish_repo_root
    cd "$publish_repo_root" || { echo "❌ Failed to cd to $publish_repo_root"; exit 1; }
    echo "✓ Working in: $(pwd)"
    
    # Verify we're in a git repo (or initialize one if needed)
    if ! git rev-parse --git-dir > /dev/null 2>&1; then
        echo "Initializing git repository"
        git init
        git config user.name "$(git config --global user.name)"
        git config user.email "$(git config --global user.email)"
    fi
    
  2. Set up remote for target repository:

    # Set up temporary remote for target repo
    git remote add publish-target https://github.com/$target_repo.git 2>/dev/null || {
        # Remote might already exist, update it
        git remote set-url publish-target https://github.com/$target_repo.git
    }
    echo "✓ Publishing to: $target_repo"
    
  3. Safely checkout branch using shared utility (handles uncommitted changes, stale branches):

    # Get repo root (feature_dir may be a subdirectory)
    repo_root=$(git -C "$feature_dir" rev-parse --show-toplevel)
    
    # Check if branch exists remotely
    git fetch publish-target main
    if git ls-remote --heads publish-target test-plan/<source_key> | grep -q test-plan/<source_key>; then
        echo "Branch test-plan/<source_key> already exists - updating"
        # Branch exists - use safe-checkout to handle stale local branches
        (cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/repo.py safe-checkout "$repo_root" test-plan/<source_key> --remote publish-target)
        if [ $? -ne 0 ]; then
            echo "Failed to checkout branch safely"
            exit 1
        fi
    else
        echo "Creating new branch test-plan/<source_key>"
        # New branch - create from main (initial publish workflow)
        # First ensure we're on main and up-to-date
        (cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/repo.py safe-checkout "$repo_root" main --remote publish-target) || exit 1
        # Then create new branch from main
        git checkout -b test-plan/<source_key>
    fi
    

    Rationale:

    • Version-free branch names (test-plan/<source_key>) allow updates to push to the same PR
    • Uses shared safe-checkout utility for:
      • Uncommitted changes check (prevents data loss)
      • Stale local branch detection (addresses PR review comment #2)
      • Automatic pull to update stale branches
    • If branch doesn't exist: Create from up-to-date main
    • No user prompt needed - the workflow is deterministic
  4. Stage, check for changes, and commit in one call:

    feature_name=$(basename "$feature_dir")
    if ! publish_result=$(cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/repo.py publish-artifacts "$repo_root" "$feature_name" "test-plan(<source_key>): publish <feature> v<version>"); then
        echo "ERROR: publish-artifacts failed"; exit 1
    fi
    committed=$(printf '%s\n' "$publish_result" | jq -r '.committed')
    if [ "$committed" = "false" ]; then
        echo "⚠ No changes to commit - artifacts are already up to date"
        exit 0
    fi
    

    This selectively stages only the public artifacts (TestPlan.md, README.md, TestPlanGaps.md, TestPlanReview.md, test_cases/*.md), excludes internal working files, and commits if there are changes.

  5. Push the branch:

    git push publish-target test-plan/<source_key>
    

    Note: Always use regular push (not --force) since we're either creating a new branch or adding commits on top of an existing one.

  6. Clean up the temporary remote:

    git remote remove publish-target
    

Step 5: Create Pull Request

  1. Build the PR title: Test Plan: <feature> (v<version>)

  2. Resolve the strategy browse URL from the configured Jira URL (do not hardcode a Jira host; falls back to the JIRA_BASE_URL alias):

    jira_url="${JIRA_URL:-$JIRA_BASE_URL}"
    if [ -z "$jira_url" ]; then
        echo "ERROR: JIRA_URL (or JIRA_BASE_URL) is not set — cannot build the strategy link." >&2
        exit 1
    fi
    strat_url="${jira_url%/}/browse/${source_key}"
    
  3. Build the PR body by reading Section 1 (Executive Summary) from TestPlan.md. Format it as:

    ## Test Plan: <feature>
    
    **Strategy**: [<source_key>](<strat_url>)
    **Version**: <version>
    
    ### Summary
    <Section 1.1 Purpose content>
    
    ### Scope
    <Section 1.2 Scope — in-scope items only, as bullet list>
    
    ### Test Objectives
    <Section 1.3 Test Objectives content>
    
    ### Artifacts
    - TestPlan.md
    - TestPlanGaps.md (if exists)
    - test_cases/INDEX.md (if exists, with count)
    
  4. Create or detect existing PR:

    pr_result=$(cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/repo.py pr-create "$target_repo" "test-plan/<source_key>" "Test Plan: <feature> (v<version>)" "<pr_body>" $([ -n "$reviewers" ] && echo "--reviewers $reviewers"))
    pr_url=$(printf '%s\n' "$pr_result" | jq -r '.pr_url')
    pr_created=$(printf '%s\n' "$pr_result" | jq -r '.created')
    

    Note: When an existing PR is detected, the new commits are automatically added by the push. The PR title and body are NOT updated (preserving any manual edits reviewers may have made).

Step 6: Confirm

  1. Display the PR URL to the user

  2. Show a summary:

    If new PR created:

    Published successfully

    • PR: <PR_URL>
    • Branch: test-plan/<source_key>
    • Version: <version>
    • Status: frontmatter updated to In Review
    • Reviewers: <list or "none assigned">

    The test plan is now ready for review.

    If existing PR updated:

    PR updated successfully

    • PR: <PR_URL>
    • Branch: test-plan/<source_key>
    • Version: <old_version> → <new_version>
    • New commits pushed

    The PR has been updated with the latest changes.

  3. Switch back to the previous branch:

    git checkout -
    

What this skill does NOT do

  • Does NOT generate or modify test plan content — use /test-plan-create for that
  • Does NOT apply review feedback — use /test-plan-resolve-feedback for that
  • Does NOT support Confluence or Google Drive — GitHub PRs only
  • Does NOT auto-increment the version field — version is set by /test-plan-create and only bumped by /test-plan-resolve-feedback after changes

$ARGUMENTS