| name | test-plan-publish |
| description | 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 collaboration and formal review feedback. |
| argument-hint | [FEATURE_SOURCE] [--repo owner/repo] [--reviewers user1,user2] |
| user-invocable | true |
| model | sonnet |
| allowedTools | ["Read","Bash","AskUserQuestion"] |
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:
- Fork
opendatahub-io/opendatahub-test-plans to your GitHub account
- Clone your fork:
git clone https://github.com/YOUR-USERNAME/opendatahub-test-plans ~/Code/opendatahub-test-plans
- Create test plan:
/test-plan-create RHAISTRAT-XXX
- Generate test cases:
/test-plan-create-cases
- 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:
- 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
--repo (optional): Target GitHub repository in owner/repo format. Defaults to the current repository or detected from GitHub URL.
--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):
(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
feature_dir=$(echo "$result" | jq -r '.feature_dir')
source_type=$(echo "$result" | jq -r '.source_type')
if [ "$source_type" = "github" ]; then
repo_owner=$(echo "$result" | jq -r '.repo_owner')
repo_name=$(echo "$result" | jq -r '.repo_name')
[ -z "$target_repo" ] && target_repo="$repo_owner/$repo_name"
fi
0.3 Validate artifacts exist
Check that the feature directory contains at least:
If TestPlanGaps.md or test_cases/ exist, they will be included. Do NOT fail if they are absent — the user may publish before generating test cases.
0.4 Validate frontmatter
Validate the TestPlan.md frontmatter. If validation fails, show the errors and do NOT proceed. The user must fix frontmatter issues before publishing.
(cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/frontmatter.py validate <feature_dir>/TestPlan.md)
If TestPlanGaps.md exists, validate it too:
(cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/frontmatter.py validate <feature_dir>/TestPlanGaps.md)
If test_cases/TC-*.md files exist, validate them:
(cd $(git -C ${CLAUDE_SKILL_DIR} rev-parse --show-toplevel) && uv run python scripts/validate_test_cases.py <feature_dir> test-case)
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.
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
- 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)
- Extract:
source_key, version, feature, reviewers
- 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
- 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.
-
Export CLAUDE_SKILL_DIR for validation utilities:
export CLAUDE_SKILL_DIR
-
If --repo was provided in arguments:
-
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
-
Validate the user-provided or default repository:
-
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.
-
Get and normalize the git remote of the feature directory:
cd <feature_dir>
remote_url=$(git config --get remote.origin.url)
normalized=$(echo "$remote_url" | sed -E 's|\.git$||; s|/+$||')
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}"
remote_repo_lower=$(echo "$remote_repo" | tr '[:upper:]' '[:lower:]')
-
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
-
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
- 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
-
Ensure we're in the correct working directory:
feature_dir_abs=$(cd "$(dirname "$feature_dir")" && pwd)/$(basename "$feature_dir")
publish_repo_root=$(dirname "$feature_dir_abs")
export CLAUDE_SKILL_DIR
current_repo=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
skill_parent="${CLAUDE_SKILL_DIR}/../.."
skill_repo_root=$(cd "$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
cd "$publish_repo_root" || { echo "❌ Failed to cd to $publish_repo_root"; exit 1; }
echo "✓ Working in: $(pwd)"
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
-
Set up remote for target repository:
git remote add publish-target https://github.com/$target_repo.git 2>/dev/null || {
git remote set-url publish-target https://github.com/$target_repo.git
}
echo "✓ Publishing to: $target_repo"
-
Safely checkout branch using shared utility (handles uncommitted changes, stale branches):
repo_root=$(git -C "$feature_dir" rev-parse --show-toplevel)
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"
(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>"
(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
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
-
Stage only public artifacts in the feature directory:
feature_name=$(basename "$feature_dir")
git add $feature_name/TestPlan.md $feature_name/README.md
[ -f $feature_name/TestPlanGaps.md ] && git add $feature_name/TestPlanGaps.md
[ -f $feature_name/TestPlanReview.md ] && git add $feature_name/TestPlanReview.md
[ -d $feature_name/test_cases ] && git add $feature_name/test_cases/*.md
Important: This selectively stages only the public artifacts (TestPlan.md, TC-*.md, INDEX.md, README.md, TestPlanGaps.md), excluding internal working files like .review-state.json, repo_instructions.md, test_implementation_conventions.md, and test_scores/ which are meant for internal orchestration only.
-
Check if there are changes to commit:
if ! git diff --cached --quiet; then
echo "✓ Changes detected, creating commit"
else
echo "⚠ No changes to commit - artifacts are already up to date"
exit 0
fi
-
Commit with a descriptive message. Use a heredoc to avoid shell injection from frontmatter values:
git commit -m "$(cat <<'EOF'
test-plan(<source_key>): publish <feature> v<version>
EOF
)"
-
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.
-
Clean up the temporary remote:
git remote remove publish-target
Step 5: Create Pull Request
-
Build the PR title: Test Plan: <feature> (v<version>)
-
Build the PR body by reading Section 1 (Executive Summary) from TestPlan.md. Format it as:
## Test Plan: <feature>
**Strategy**: [<source_key>](https://redhat.atlassian.net/browse/<source_key>)
**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)
-
Check if PR already exists for this branch, and create or update accordingly:
existing_pr=$(gh pr list --repo $target_repo --head test-plan/<source_key> --json number --jq '.[0].number' 2>/dev/null)
if [ -n "$existing_pr" ]; then
echo "✓ PR #$existing_pr updated with new commits"
pr_url=$(gh pr view $existing_pr --repo $target_repo --json url --jq '.url')
else
pr_url=$(gh pr create \
--repo $target_repo \
--title "Test Plan: <feature> (v<version>)" \
--body "$(cat <<'EOF'
<pr_body>
EOF
)" \
--base main \
--head test-plan/<source_key> \
$([ -n "$reviewers" ] && echo "--reviewer $reviewers") \
--json url --jq '.url')
fi
Note: When updating an existing PR, the new commits are automatically added. The PR title and body are NOT updated (preserving any manual edits reviewers may have made).
Step 6: Confirm
-
Display the PR URL to the user
-
Show a summary:
If new PR created:
Published successfully
- PR: <PR_URL>
- Branch:
test-plan/<source_key>
- 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.
-
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