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.mdskills/test-plan-publish/SKILL.md
GitHubTest 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-plansto 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-createrun)
Inputs
From arguments
Parse $ARGUMENTS to extract:
- First argument (optional): Test plan source - can be:
- Local directory path:
mcp_catalogor/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
- Local directory path:
--repo(optional): Target GitHub repository inowner/repoformat. 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 thereviewersfield 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
- Read frontmatter from
<feature_dir>/TestPlan.mdusing 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-updateto push updates to the same PR instead of creating new PRs for each version bump
- Example:
- Determine reviewers: use
--reviewersargument if provided, otherwise use frontmatterreviewersfield
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_DIRIf
--repowas 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
- Validate it's not the skill repo:
If
--repowas 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/repoformat, or press Enter for default:opendatahub-io/opendatahub-test-plansValidate 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
- Check format (must be
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) # 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:]')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 fiIf 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
statustoIn 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:
# 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)" fiSet 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"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> fiRationale:
- Version-free branch names (
test-plan/<source_key>) allow updates to push to the same PR - Uses shared
safe-checkoututility 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
- Version-free branch names (
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 fiThis 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.
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>)Resolve the strategy browse URL from the configured Jira URL (do not hardcode a Jira host; falls back to the
JIRA_BASE_URLalias):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}"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)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
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: <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-createfor that - Does NOT apply review feedback — use
/test-plan-resolve-feedbackfor that - Does NOT support Confluence or Google Drive — GitHub PRs only
- Does NOT auto-increment the
versionfield — version is set by/test-plan-createand only bumped by/test-plan-resolve-feedbackafter changes
$ARGUMENTS