diff --git a/AGENTS.md b/AGENTS.md index 2a035646..da07d1c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,6 +83,7 @@ _shared/ sizing-rubric.md # Shared sizing definitions (T-shirt sizes, heuristics, team effort guidance) scripts/ provenance.py # Capture/render CLI (used by prd and design provenance recipes) + publish.sh # Deterministic publish ops (preflight, push, PR/MR creation, metadata) recipes/ capture-provenance-event.md # Append session-local provenance on doc-mutating phases phase-override-resolution.md # Project-level phase override lookup and activation @@ -196,7 +197,8 @@ ai-workflows/ │ ├── review-protocol.md # Shared code review criteria and finding format │ ├── sizing-rubric.md # Shared sizing definitions and heuristics │ ├── scripts/ -│ │ └── provenance.py # Capture/render CLI for prd/design provenance +│ │ ├── provenance.py # Capture/render CLI for prd/design provenance +│ │ └── publish.sh # Deterministic publish ops (push, PR/MR, metadata) │ └── recipes/ │ ├── capture-provenance-event.md │ ├── phase-override-resolution.md # Project-level phase override lookup diff --git a/_shared/scripts/publish.sh b/_shared/scripts/publish.sh new file mode 100755 index 00000000..647cad28 --- /dev/null +++ b/_shared/scripts/publish.sh @@ -0,0 +1,613 @@ +#!/usr/bin/env bash +# Deterministic publish operations for ai-workflows. +# +# Provides reusable subcommands for the publish/PR/MR phase of multiple +# workflows (bugfix, implement, e2e, prd, design, docs-writer). Each +# subcommand handles one discrete, deterministic operation — the calling +# skill file retains ownership of AI-dependent work (PR body generation, +# cross-cutting review, user confirmation prompts). +# +# Subcommands: +# preflight Pre-flight checks (auth, branch, uncommitted changes) +# push Push a branch to a remote +# check-existing Check whether a PR/MR already exists for a branch +# create-pr Create a GitHub pull request via gh CLI +# create-mr Create a GitLab merge request via glab CLI +# save-metadata Write publish-metadata.json +# +# Usage: +# publish.sh preflight [--platform github|gitlab] +# publish.sh push --remote --branch +# publish.sh check-existing --repo --head [--platform github|gitlab] +# publish.sh create-pr --repo --base --head \ +# --title [--body-file <path>] [--body <text>] [--draft] [--labels <csv>] +# publish.sh create-mr --project <path> --source <branch> --target <branch> \ +# --title <title> [--description <text>] [--draft] +# publish.sh save-metadata --file <path> [key=value ...] +# +# Exit codes: +# 0 — success (preflight reports issues via structured output, not exit codes) +# 1 — missing argument or configuration error +# 3 — push failed +# 4 — PR/MR creation failed +# 5 — existing PR/MR found (check-existing only; prints details on stdout) + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +# Print an error message to stderr and exit with the given code. +fail() { + printf 'ERROR: %s\n' "$1" >&2 + exit "${2:-1}" +} + +# Print an informational message to stderr. +info() { + printf 'INFO: %s\n' "$1" >&2 +} + +# Print usage information extracted from the script header and exit. +usage() { + sed -n '/^# Usage:/,/^# Exit codes:/{ /^# Exit codes:/d; s/^# \?//; p }' "$0" >&2 + exit 1 +} + +# Fail with a descriptive message if a required argument is empty. +require_arg() { + if [[ -z "${2:-}" ]]; then + fail "Missing required argument: $1" 1 + fi +} + +# Validate that a CLI flag has an accompanying value argument. +flag_value() { + # Prevents cryptic "unbound variable" errors under set -u when a flag + # is passed without its value (e.g., --platform with no argument). + # Usage (inside a while/case loop): flag_value "--flag-name" "$#" + if [[ $2 -lt 2 ]]; then + fail "$1 requires a value" 1 + fi +} + +# Escape a string for safe embedding as a JSON string value. +# Handles backslash, double-quote, newline, tab, carriage return, and +# other control characters (0x00-0x1F, 0x7F) using \uXXXX notation. +# Does not add surrounding quotes — the caller wraps the result. +json_escape() { + local s="$1" + local out="" + local i char code + for (( i = 0; i < ${#s}; i++ )); do + char="${s:i:1}" + case "$char" in + \\) out+="\\\\" ;; + '"') out+="\\\"" ;; + $'\n') out+="\\n" ;; + $'\t') out+="\\t" ;; + $'\r') out+="\\r" ;; + $'\b') out+="\\b" ;; + $'\x0c') out+="\\f" ;; + *) + # Detect remaining control characters (0x00-0x1F, 0x7F) + printf -v code '%d' "'${char}" + if (( code >= 0 && code < 32 )) || (( code == 127 )); then + printf -v char '\\u%04x' "$code" + out+="${char}" + else + out+="${char}" + fi + ;; + esac + done + printf '%s' "$out" +} + +# --------------------------------------------------------------------------- +# Subcommand: preflight +# --------------------------------------------------------------------------- +# Run pre-flight checks: auth, branch, and working-tree cleanliness. +# Prints a structured key=value status block on stdout for the calling +# skill to parse. +# +# Flags: +# --platform github|gitlab Which CLI to check (default: github) + +cmd_preflight() { + local platform="github" + + while [[ $# -gt 0 ]]; do + case "$1" in + --platform) flag_value "$1" "$#"; platform="$2"; shift 2 ;; + *) fail "preflight: unknown flag: $1" 1 ;; + esac + done + + local auth_ok="false" + local auth_user="" + local branch="" + local has_uncommitted="false" + local has_staged="false" + local has_untracked="false" + + # -- Auth check -- + case "$platform" in + github) + if gh auth status >/dev/null 2>&1; then + auth_ok="true" + auth_user=$(gh api user --jq .login 2>/dev/null || true) + if [[ -z "$auth_user" ]]; then + # GitHub App / bot — try installation endpoint + auth_user=$(gh api /installation/repositories \ + --jq '.repositories[0].owner.login' 2>/dev/null || true) + fi + fi + ;; + gitlab) + if glab auth status >/dev/null 2>&1; then + auth_ok="true" + auth_user=$(glab api user --jq .username 2>/dev/null || true) + fi + ;; + *) fail "preflight: invalid platform: $platform (expected github or gitlab)" 1 ;; + esac + + # -- Branch -- + branch=$(git branch --show-current 2>/dev/null || true) + + # -- Uncommitted changes -- + if ! git diff --quiet 2>/dev/null; then + has_uncommitted="true" + fi + if ! git diff --cached --quiet 2>/dev/null; then + has_staged="true" + fi + if [[ -n "$(git ls-files --others --exclude-standard 2>/dev/null)" ]]; then + has_untracked="true" + fi + + # -- Output structured block -- + cat <<EOF +auth_ok=${auth_ok} +auth_user=${auth_user} +branch=${branch} +has_uncommitted=${has_uncommitted} +has_staged=${has_staged} +has_untracked=${has_untracked} +platform=${platform} +EOF +} + +# --------------------------------------------------------------------------- +# Subcommand: push +# --------------------------------------------------------------------------- +# Push a branch to the specified remote with upstream tracking (-u). +# +# Flags: +# --remote <name> Git remote name (e.g., fork, origin) +# --branch <branch> Branch name to push + +cmd_push() { + local remote="" + local branch="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --remote) flag_value "$1" "$#"; remote="$2"; shift 2 ;; + --branch) flag_value "$1" "$#"; branch="$2"; shift 2 ;; + *) fail "push: unknown flag: $1" 1 ;; + esac + done + + require_arg "--remote" "$remote" + require_arg "--branch" "$branch" + + # Verify the remote exists + if ! git remote get-url "$remote" >/dev/null 2>&1; then + fail "push: remote '$remote' does not exist. Available remotes: $(git remote | tr '\n' ' ')" 3 + fi + + info "Pushing $branch to $remote..." + if ! git push -u "$remote" "$branch" 2>&1; then + fail "push: git push failed (remote=$remote, branch=$branch)" 3 + fi + + info "Push successful: $remote/$branch" +} + +# --------------------------------------------------------------------------- +# Subcommand: check-existing +# --------------------------------------------------------------------------- +# Check whether an open PR (GitHub) or MR (GitLab) already exists for +# a branch. Supports owner:branch (GitHub) and project:branch (GitLab) +# formats for fork-aware matching. +# +# Flags: +# --repo <owner/repo> Target repository +# --head <ref> Branch, owner:branch (GitHub), or +# project:branch (GitLab) to match +# --platform github|gitlab Which platform (default: github) +# +# Exit code 0 if NO existing PR/MR found (safe to create one). +# Exit code 5 if an existing PR/MR IS found (details on stdout). + +cmd_check_existing() { + local repo="" + local head="" + local platform="github" + + while [[ $# -gt 0 ]]; do + case "$1" in + --repo) flag_value "$1" "$#"; repo="$2"; shift 2 ;; + --head) flag_value "$1" "$#"; head="$2"; shift 2 ;; + --platform) flag_value "$1" "$#"; platform="$2"; shift 2 ;; + *) fail "check-existing: unknown flag: $1" 1 ;; + esac + done + + require_arg "--repo" "$repo" + require_arg "--head" "$head" + + case "$platform" in + github) + local result exit_code=0 + if [[ "$head" == *:* ]]; then + # owner:branch format — gh pr list --head does not support this + # syntax. Search by branch name and filter by head repo owner. + local head_owner="${head%%:*}" + local head_branch="${head#*:}" + result=$(gh pr list --repo "$repo" --head "$head_branch" \ + --json number,url,headRepositoryOwner \ + --jq "[.[] | select(.headRepositoryOwner.login == \"$head_owner\")] | .[0] // empty" \ + 2>/dev/null) || exit_code=$? + else + result=$(gh pr list --repo "$repo" --head "$head" \ + --json number,url --jq '.[0] // empty' 2>/dev/null) || exit_code=$? + fi + if [[ $exit_code -ne 0 ]]; then + fail "check-existing: GitHub API query failed (exit $exit_code). Check gh auth status." 1 + fi + if [[ -n "$result" ]]; then + echo "$result" + exit 5 + fi + ;; + gitlab) + local source_branch="$head" + local source_project="" + if [[ "$head" == *:* ]]; then + # project:branch format — extract source project for cross-fork filtering + source_project="${head%%:*}" + source_branch="${head#*:}" + fi + local raw exit_code=0 + raw=$(glab mr list --repo "$repo" --source-branch "$source_branch" \ + --output json 2>/dev/null) || exit_code=$? + if [[ $exit_code -ne 0 ]]; then + fail "check-existing: GitLab API query failed (exit $exit_code). Check glab auth status." 1 + fi + local result + if [[ -n "$source_project" ]]; then + # Resolve the fork's numeric project ID to filter by source_project_id, + # preventing false matches from other forks with the same branch name. + local encoded_project + encoded_project=$(printf '%s' "$source_project" | sed 's|/|%2F|g') + local project_id + project_id=$(glab api "projects/$encoded_project" --jq '.id' 2>/dev/null) || true + if [[ -z "$project_id" ]]; then + fail "check-existing: could not resolve project ID for '$source_project'" 1 + fi + result=$(printf '%s' "$raw" | jq -r --argjson pid "$project_id" \ + '[.[] | select(.source_project_id == $pid)] | .[0] // empty' \ + 2>/dev/null) + else + result=$(printf '%s' "$raw" | jq -r '.[0] // empty' 2>/dev/null) + fi + if [[ -n "$result" ]]; then + echo "$result" + exit 5 + fi + ;; + *) fail "check-existing: invalid platform: $platform" 1 ;; + esac + + # No existing PR/MR found + info "No existing PR/MR found for head=$head on $repo" +} + +# --------------------------------------------------------------------------- +# Subcommand: create-pr +# --------------------------------------------------------------------------- +# Create a GitHub pull request via the gh CLI. +# +# Flags: +# --repo <owner/repo> Target repository (required for fork-based PRs) +# --base <branch> Base branch (e.g., main) +# --head <ref> Head ref — owner:branch for forks, branch for direct +# --title <title> PR title +# --body-file <path> Path to file containing PR body (mutually exclusive with --body) +# --body <text> Inline PR body text (mutually exclusive with --body-file) +# --draft Create as draft PR (default: true) +# --no-draft Create as non-draft PR +# --labels <csv> Comma-separated label names +# +# On success, prints the PR URL on stdout. + +cmd_create_pr() { + local repo="" + local base="" + local head="" + local title="" + local body_file="" + local body="" + local draft="true" + local labels="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --repo) flag_value "$1" "$#"; repo="$2"; shift 2 ;; + --base) flag_value "$1" "$#"; base="$2"; shift 2 ;; + --head) flag_value "$1" "$#"; head="$2"; shift 2 ;; + --title) flag_value "$1" "$#"; title="$2"; shift 2 ;; + --body-file) flag_value "$1" "$#"; body_file="$2"; shift 2 ;; + --body) flag_value "$1" "$#"; body="$2"; shift 2 ;; + --draft) draft="true"; shift ;; + --no-draft) draft="false"; shift ;; + --labels) flag_value "$1" "$#"; labels="$2"; shift 2 ;; + *) fail "create-pr: unknown flag: $1" 1 ;; + esac + done + + require_arg "--base" "$base" + require_arg "--head" "$head" + require_arg "--title" "$title" + + # Build gh pr create command + local -a cmd=(gh pr create) + + if [[ "$draft" == "true" ]]; then + cmd+=(--draft) + fi + + if [[ -n "$repo" ]]; then + cmd+=(--repo "$repo") + fi + + cmd+=(--base "$base" --head "$head" --title "$title") + + if [[ -n "$body_file" ]]; then + if [[ ! -f "$body_file" ]]; then + fail "create-pr: body file not found: $body_file" 1 + fi + cmd+=(--body-file "$body_file") + elif [[ -n "$body" ]]; then + cmd+=(--body "$body") + else + cmd+=(--body "") + fi + + if [[ -n "$labels" ]]; then + cmd+=(--label "$labels") + fi + + info "Creating PR: ${title}" + local pr_url + local stderr_file + stderr_file=$(mktemp) + if pr_url=$("${cmd[@]}" 2>"$stderr_file"); then + rm -f "$stderr_file" + # gh pr create prints the URL on success + echo "$pr_url" + info "PR created: $pr_url" + else + # Print the error but use a distinct exit code so the skill can + # detect the failure and fall back (e.g., to a compare URL). + cat "$stderr_file" >&2 + rm -f "$stderr_file" + exit 4 + fi +} + +# --------------------------------------------------------------------------- +# Subcommand: create-mr +# --------------------------------------------------------------------------- +# Create a GitLab merge request via the glab CLI. +# +# Flags: +# --project <path> Upstream project path (for fork-based MRs) +# --source <branch> Source branch +# --target <branch> Target branch (e.g., main) +# --title <title> MR title +# --description <text> MR description text +# --desc-file <path> Path to file containing MR description +# --draft Create as draft MR (default: true) +# --no-draft Create as non-draft MR +# --head <project> Fork project path (for fork-based MRs) +# +# On success, prints the MR URL on stdout. + +cmd_create_mr() { + local project="" + local source_branch="" + local target_branch="" + local title="" + local description="" + local desc_file="" + local draft="true" + local head_project="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --project) flag_value "$1" "$#"; project="$2"; shift 2 ;; + --source) flag_value "$1" "$#"; source_branch="$2"; shift 2 ;; + --target) flag_value "$1" "$#"; target_branch="$2"; shift 2 ;; + --title) flag_value "$1" "$#"; title="$2"; shift 2 ;; + --description) flag_value "$1" "$#"; description="$2"; shift 2 ;; + --desc-file) flag_value "$1" "$#"; desc_file="$2"; shift 2 ;; + --draft) draft="true"; shift ;; + --no-draft) draft="false"; shift ;; + --head) flag_value "$1" "$#"; head_project="$2"; shift 2 ;; + *) fail "create-mr: unknown flag: $1" 1 ;; + esac + done + + require_arg "--source" "$source_branch" + require_arg "--target" "$target_branch" + require_arg "--title" "$title" + + # Build glab mr create command + local -a cmd=(glab mr create --yes) + + if [[ "$draft" == "true" ]]; then + cmd+=(--draft) + fi + + if [[ -n "$project" ]]; then + cmd+=(--repo "$project") + fi + + if [[ -n "$head_project" ]]; then + cmd+=(--head "$head_project") + fi + + cmd+=(--source-branch "$source_branch" --target-branch "$target_branch" --title "$title") + + if [[ -n "$desc_file" ]]; then + if [[ ! -f "$desc_file" ]]; then + fail "create-mr: description file not found: $desc_file" 1 + fi + # glab uses --description, not --body-file — read the file content + description=$(<"$desc_file") + fi + + if [[ -n "$description" ]]; then + cmd+=(--description "$description") + fi + + info "Creating MR: ${title}" + local mr_url + local stderr_file + stderr_file=$(mktemp) + if mr_url=$("${cmd[@]}" 2>"$stderr_file"); then + rm -f "$stderr_file" + echo "$mr_url" + info "MR created: $mr_url" + else + cat "$stderr_file" >&2 + rm -f "$stderr_file" + exit 4 + fi +} + +# --------------------------------------------------------------------------- +# Subcommand: save-metadata +# --------------------------------------------------------------------------- +# Write a JSON metadata file from key=value pairs with full escaping. +# +# Flags: +# --file <path> Output file path (required) +# +# Remaining positional arguments are key=value pairs. All values are +# stored as JSON strings to avoid leading-zero truncation and to keep +# the output type-stable. +# +# Example: +# publish.sh save-metadata --file .artifacts/impl/EDM-1/publish-metadata.json \ +# repo=acme/project branch=feat/x base=main pr_number=42 \ +# pr_url=https://github.com/acme/project/pull/42 jira_key=EDM-1 + +cmd_save_metadata() { + local file="" + local -a pairs=() + + while [[ $# -gt 0 ]]; do + case "$1" in + --file) flag_value "$1" "$#"; file="$2"; shift 2 ;; + *=*) pairs+=("$1"); shift ;; + *) fail "save-metadata: unexpected argument: $1 (expected key=value)" 1 ;; + esac + done + + require_arg "--file" "$file" + + if [[ ${#pairs[@]} -eq 0 ]]; then + fail "save-metadata: no key=value pairs provided" 1 + fi + + # Build JSON using printf — avoids jq dependency. + # Keys are sorted alphabetically for stable output. + # NUL-delimited sort prevents values with embedded newlines from being + # split into separate lines before json_escape can process them. + # Uses a temp file so sort failures are detected (process substitution + # masks the sort exit status). + local json="{" + local first="true" + local -a sorted_pairs + local sort_tmp + sort_tmp=$(mktemp) || fail "save-metadata: failed to create temp file for sorting" 1 + if ! printf '%s\0' "${pairs[@]}" > "$sort_tmp"; then + rm -f "$sort_tmp" + fail "save-metadata: failed to write pairs to temp file" 1 + fi + if ! sort -z -o "$sort_tmp" "$sort_tmp"; then + rm -f "$sort_tmp" + fail "save-metadata: sort failed" 1 + fi + mapfile -d '' -t sorted_pairs < "$sort_tmp" + rm -f "$sort_tmp" + + for pair in "${sorted_pairs[@]}"; do + local key="${pair%%=*}" + local value="${pair#*=}" + + if [[ "$first" == "true" ]]; then + first="false" + else + json+="," + fi + + # Always serialize as a JSON string to avoid leading-zero truncation + # (e.g., "007" → 7) and to keep the output type-stable. Full JSON + # escaping handles newlines, tabs, quotes, backslashes, and control + # characters that would otherwise produce invalid JSON. + key=$(json_escape "$key") + value=$(json_escape "$value") + json+=$(printf '\n "%s": "%s"' "$key" "$value") + done + + json+=$'\n}\n' + + # Create parent directory if needed + local dir + dir=$(dirname "$file") + if [[ ! -d "$dir" ]]; then + mkdir -p "$dir" + fi + + printf '%s' "$json" > "$file" + info "Metadata saved to $file" +} + +# --------------------------------------------------------------------------- +# Main dispatcher +# --------------------------------------------------------------------------- + +if [[ $# -eq 0 ]]; then + usage +fi + +subcommand="$1" +shift + +case "$subcommand" in + preflight) cmd_preflight "$@" ;; + push) cmd_push "$@" ;; + check-existing) cmd_check_existing "$@" ;; + create-pr) cmd_create_pr "$@" ;; + create-mr) cmd_create_mr "$@" ;; + save-metadata) cmd_save_metadata "$@" ;; + -h|--help|help) usage ;; + *) fail "Unknown subcommand: $subcommand. Run with --help for usage." 1 ;; +esac diff --git a/bugfix/SKILL.md b/bugfix/SKILL.md index 20ea747d..5a86db93 100644 --- a/bugfix/SKILL.md +++ b/bugfix/SKILL.md @@ -1,6 +1,6 @@ --- name: bugfix -version: 0.8.0 +version: 0.8.1 description: >- Diagnostic and repair workflow that analyzes error logs, traces root causes, implements fixes, and verifies with regression tests. diff --git a/bugfix/skills/pr.md b/bugfix/skills/pr.md index 88981529..f59f66c3 100644 --- a/bugfix/skills/pr.md +++ b/bugfix/skills/pr.md @@ -35,6 +35,18 @@ the documented recovery paths instead of guessing. - **Never attempt `gh repo fork` without asking the user first.** - **Never fall back to patch files without exhausting all other options.** +## Shared Script + +This skill delegates deterministic git and CLI operations to a shared +script. Reference it using a relative path from this file: + +``` +../../_shared/scripts/publish.sh +``` + +The script provides subcommands: `preflight`, `push`, `check-existing`, +`create-pr`, and `save-metadata`. See the script header for full usage. + ## Process ### Placeholders Used in This Skill @@ -68,35 +80,40 @@ commands run from there. If the user provides a path or the repo is obvious from session context (prior commands, artifacts), use that directly. +Now that you are inside the project repo, resolve the shared script to an +absolute path so it remains valid regardless of working directory: + +```bash +PUBLISH_SCRIPT="$(git rev-parse --show-toplevel)/_shared/scripts/publish.sh" +``` + +Use `$PUBLISH_SCRIPT` instead of the relative path in all subsequent +commands. + ### Step 1: Pre-flight Checks Run ALL of these before doing anything else. Do not skip any. -**1a. Check GitHub CLI authentication and determine GH_USER:** +**1a. Run the shared pre-flight checks:** ```bash -gh auth status +"$PUBLISH_SCRIPT" preflight --platform github ``` -- If authenticated, determine `GH_USER` — the **real user's** GitHub username - (not the bot). Try these in order: - -```bash -# Works for normal user tokens: -gh api user --jq .login 2>/dev/null - -# If that fails (403), you're running as a GitHub App/bot. -# Get the real user from the app installation: -gh api /installation/repositories --jq '.repositories[0].owner.login' -``` +Parse the structured output: +- `auth_ok` — whether `gh auth` succeeded +- `auth_user` — the GitHub username (`GH_USER`) +- `branch` — current branch name +- `has_uncommitted` / `has_staged` / `has_untracked` — whether there are uncommitted, staged, or untracked changes -The `/installation/repositories` endpoint works because GitHub Apps are -installed on user accounts — the repo owner is the actual user. +If `auth_ok=true`, set `GH_USER` from `auth_user`. If `auth_user` is +empty (GitHub App/bot), the script already tried the +`/installation/repositories` fallback. -- If not authenticated: note this — several later steps depend on `gh`. But - do NOT dump all manual instructions yet. Continue the remaining pre-flight - checks (1b–1e) to gather as much information as possible from git alone. - After pre-flight, you will present options to the user. +If `auth_ok=false`: note this — several later steps depend on `gh`. But +do NOT dump all manual instructions yet. Continue the remaining pre-flight +checks (1b–1d) to gather as much information as possible from git alone. +After pre-flight, you will present options to the user. **1b. Check git configuration:** @@ -148,15 +165,11 @@ git remote get-url origin | sed -E 's#.*/([^/]+/[^/]+?)(\.git)?$#\1#' Record the result as `UPSTREAM_OWNER/REPO` — you'll need it later. -**1e. Check current branch and changes:** - -```bash -git status -git diff --stat -``` - -Confirm there are actual changes to commit. If there are no changes, stop -and tell the user. +Confirm there are actual changes to commit (from the pre-flight output's +`has_uncommitted`, `has_staged`, or `has_untracked` fields). If all three +are `false`, there are no changes — stop and tell the user. If +`has_untracked` is `true`, warn the user about untracked files and ask +whether they should be included in the commit. **Pre-flight summary:** Before moving on, you should now know: `UPSTREAM_OWNER/REPO`, which remotes exist, and whether there are changes to @@ -440,10 +453,10 @@ to write an accurate commit message. Don't make up details. ### Step 8: Push to Fork ```bash -git push -u fork bugfix/BRANCH_NAME +"$PUBLISH_SCRIPT" push --remote fork --branch bugfix/BRANCH_NAME ``` -**If this fails:** +**If the script exits with code 3 (push failed):** - **Authentication error**: Check `gh auth status` again. The user may need to re-authenticate or the sandbox may be blocking network access. @@ -456,78 +469,60 @@ access. Please run: `git push -u fork BRANCH_NAME`" ### Step 9: Create the Draft PR -**If a pull request already exists** for this branch on -`UPSTREAM_OWNER/REPO`, skip this step and proceed to **Confirm and -Report**. Check with: +**Check for an existing PR** before attempting creation. Use +`FORK_OWNER:bugfix/BRANCH_NAME` so the check matches only PRs from +this fork (plain `bugfix/BRANCH_NAME` would match any fork's branch +with the same name): ```bash -gh pr list --repo UPSTREAM_OWNER/REPO --head bugfix/BRANCH_NAME --json number,url --jq '.[0] // empty' +"$PUBLISH_SCRIPT" check-existing \ + --repo UPSTREAM_OWNER/REPO \ + --head FORK_OWNER:bugfix/BRANCH_NAME ``` -If the command fails (auth error, network error, API error), **stop and -report the failure** — do not fall through to PR creation. Only proceed -when the command succeeds: a result means the PR already exists (skip to -Step 10 and report its URL); an empty result means no existing PR (continue -with creation below). +If exit code is 5, a PR already exists — skip to Step 10 and report its +URL. If the command fails (auth error, network error, API error), **stop +and report the failure** — do not fall through to PR creation. **PR title format:** Use **`[ISSUE_KEY]: short description in lowercase`**. If the artifact `.artifacts/bugfix/{issue}/pr-description.md` exists and has a `## Title` line in this format, use that title. Otherwise set `ISSUE_KEY` from the branch name or context (e.g. Jira EDM-1234, GitHub #47) and build the title as `[ISSUE_KEY]: short description`. -**Try `gh pr create` first** (it works for normal user tokens): +**Create the PR using the shared script** (works for normal user tokens): + +If the `--body-file` artifact exists: ```bash -gh pr create \ - --draft \ +"$PUBLISH_SCRIPT" create-pr \ --repo UPSTREAM_OWNER/REPO \ --head FORK_OWNER:bugfix/BRANCH_NAME \ --base main \ --title "[ISSUE_KEY]: short description in lowercase" \ - --body-file .artifacts/bugfix/{issue}/pr-description.md + --body-file .artifacts/bugfix/{issue}/pr-description.md \ + --draft ``` -**Key flags explained:** - -- `--repo`: The upstream repository (where the PR goes). REQUIRED for cross-fork PRs. -- `--head`: Must be `FORK_OWNER:BRANCH_NAME` format for fork-based PRs. Without the - owner prefix, GitHub looks for the branch on the upstream repo and fails. -- `--base`: The target branch on upstream (usually `main`). -- `--draft`: Always submit as draft first. -- `--title`: PR title must be `[ISSUE_KEY]: short description`. Prefer the title from the artifact's `## Title` section if present. -- `--body-file`: Use the PR description artifact if `/document` was run. - -**If `--body-file` artifact doesn't exist**, use `--body` with inline content: +If the artifact doesn't exist, generate the PR body inline (AI-dependent — +see the template in this skill's Notes section) and pass it with `--body`: ```bash -gh pr create \ - --draft \ +"$PUBLISH_SCRIPT" create-pr \ --repo UPSTREAM_OWNER/REPO \ --head FORK_OWNER:bugfix/BRANCH_NAME \ --base main \ --title "[ISSUE_KEY]: short description in lowercase" \ - --body "## Problem -WHAT_WAS_BROKEN - -## Root Cause -WHY_IT_WAS_BROKEN - -## Fix -WHAT_THIS_PR_CHANGES - -## Testing -HOW_THE_FIX_WAS_VERIFIED - -## Confidence -HIGH_MEDIUM_LOW — BRIEF_JUSTIFICATION - -## Rollback -HOW_TO_REVERT_IF_SOMETHING_GOES_WRONG + --body "PR_BODY_TEXT" \ + --draft +``` -## Risk Assessment -LOW_MEDIUM_HIGH — WHAT_COULD_BE_AFFECTED +**Key flags explained:** -Fixes #ISSUE_NUMBER" -``` +- `--repo`: The upstream repository (where the PR goes). REQUIRED for cross-fork PRs. +- `--head`: Must be `FORK_OWNER:BRANCH_NAME` format for fork-based PRs. Without the + owner prefix, GitHub looks for the branch on the upstream repo and fails. +- `--base`: The target branch on upstream (usually `main`). +- `--draft`: Always submit as draft first. +- `--title`: PR title must be `[ISSUE_KEY]: short description`. Prefer the title from the artifact's `## Title` section if present. -**If `gh pr create` fails (403, "Resource not accessible by integration", etc.):** +**If the script exits with code 4 (PR creation failed, e.g., 403, "Resource not accessible by integration"):** This is the expected outcome when running as a GitHub App bot. Do NOT retry, do NOT debug further, do NOT fall back to a patch file. Instead: diff --git a/design/SKILL.md b/design/SKILL.md index fdc4968f..cf183255 100644 --- a/design/SKILL.md +++ b/design/SKILL.md @@ -1,6 +1,6 @@ --- name: design -version: 0.9.1 +version: 0.9.2 description: >- Design-and-decompose workflow that takes a PRD, researches the problem space, drafts a technical design document with a requirement-anchored testplan, diff --git a/design/skills/publish.md b/design/skills/publish.md index fa50aa1b..116b2ccb 100644 --- a/design/skills/publish.md +++ b/design/skills/publish.md @@ -21,8 +21,32 @@ the user before taking action. - **No force-push.** No destructive git operations. - **No direct commits to main.** Always use a feature branch. +## Shared Script + +This skill delegates deterministic git and CLI operations to a shared +script. Reference it using a relative path from this file: + +``` +../../_shared/scripts/publish.sh +``` + +The script provides subcommands: `preflight`, `push`, `check-existing`, +`create-pr`, and `save-metadata`. See the script header for full usage. + ## Process +### Prerequisites: Resolve Script Path + +Before any `cd` or subshell that changes the working directory, resolve +the shared script to an absolute path so it remains valid: + +```bash +PUBLISH_SCRIPT="$(git rev-parse --show-toplevel)/_shared/scripts/publish.sh" +``` + +Use `$PUBLISH_SCRIPT` instead of the relative path in all subsequent +commands. + ### Step 1: Read the Design Document Read `.artifacts/design/{issue-key}/03-design.md`. @@ -56,13 +80,13 @@ validated `docs_repo_path` and `docs_repo_remote`. ### Step 3: Pre-Flight Checks -Verify the environment: +Run the shared pre-flight checks from the docs repo directory: ```bash -gh auth status +(cd "{docs_repo_path}" && "$PUBLISH_SCRIPT" preflight --platform github) ``` -In the docs repo directory: +Parse the output to confirm `auth_ok=true`. Also verify the docs repo state: ```bash git -C "{docs_repo_path}" remote -v @@ -206,12 +230,14 @@ git -C "{docs_repo_path}" commit -m "Add design document and testplan for {issue ### Step 5: Push and Create PR +Push the branch using the shared script (run from the docs repo): + ```bash -git -C "{docs_repo_path}" push -u origin {branch-name} +(cd "{docs_repo_path}" && "$PUBLISH_SCRIPT" push --remote origin --branch {branch-name}) ``` Read the design document and identify specific areas that warrant reviewer -attention: +attention (AI-dependent): - Open questions from Section 9 (list each by title) - Sections with remaining TBD markers - Key architectural decisions that have significant trade-offs @@ -249,37 +275,54 @@ draft PR. Set `{pr-title}` based on whether `{issue-key}` is a Jira key: if yes, use `{issue-key}: Design - {title}`; otherwise use `Design: {title}`. +First, check whether a PR already exists for this branch: + ```bash -gh pr create --draft --repo {owner}/{repo} --base {base-branch} --head {branch-name} --title "{pr-title}" --body-file .artifacts/design/{issue-key}/08-pr-description.md +"$PUBLISH_SCRIPT" check-existing --repo {owner}/{repo} --head {branch-name} ``` -### Step 6: Save Publish Metadata +If exit code is 5, a PR already exists — skip to Step 6 and report its +URL. Parse the PR number from the returned JSON. If the command fails +(non-zero exit other than 5), stop and report the error. If exit code +is 0, create a new PR: -Write `.artifacts/design/{issue-key}/publish-metadata.json`: +```bash +"$PUBLISH_SCRIPT" create-pr \ + --repo {owner}/{repo} \ + --base {base-branch} \ + --head {branch-name} \ + --title "{pr-title}" \ + --body-file .artifacts/design/{issue-key}/08-pr-description.md \ + --draft +``` + +The script prints the PR URL on stdout. Parse the PR number from the URL path. + +### Step 6: Save Publish Metadata If `04-testplan.md` was published: -```json -{ - "release": "{release}", - "feature": "{feature}", - "design_file_path": "{release}/{feature}/design.md", - "testplan_file_path": "{release}/{feature}/testplan.md", - "pr_number": {pr-number}, - "branch": "{branch-name}" -} +```bash +"$PUBLISH_SCRIPT" save-metadata \ + --file .artifacts/design/{issue-key}/publish-metadata.json \ + release={release} \ + feature={feature} \ + design_file_path={release}/{feature}/design.md \ + testplan_file_path={release}/{feature}/testplan.md \ + pr_number={pr-number} \ + branch={branch-name} ``` -If no testplan was published, omit `testplan_file_path` entirely: +If no testplan was published, omit `testplan_file_path`: -```json -{ - "release": "{release}", - "feature": "{feature}", - "design_file_path": "{release}/{feature}/design.md", - "pr_number": {pr-number}, - "branch": "{branch-name}" -} +```bash +"$PUBLISH_SCRIPT" save-metadata \ + --file .artifacts/design/{issue-key}/publish-metadata.json \ + release={release} \ + feature={feature} \ + design_file_path={release}/{feature}/design.md \ + pr_number={pr-number} \ + branch={branch-name} ``` ### Step 7: Report to User diff --git a/docs-writer/SKILL.md b/docs-writer/SKILL.md index b4ca9873..de25ef2a 100644 --- a/docs-writer/SKILL.md +++ b/docs-writer/SKILL.md @@ -1,6 +1,6 @@ --- name: docs-writer -version: 0.3.1 +version: 0.3.2 description: Documentation workflow that converts requirements into structured AsciiDoc sections, runs Vale for style compliance, and produces merge-ready content. Use when creating or updating AsciiDoc documentation from Jira tickets, GitHub issues, or feature descriptions. --- # Docs Writer Workflow Orchestrator diff --git a/docs-writer/skills/create-mr.md b/docs-writer/skills/create-mr.md index dce6262e..5c4a8cd8 100644 --- a/docs-writer/skills/create-mr.md +++ b/docs-writer/skills/create-mr.md @@ -29,6 +29,32 @@ recovery paths instead of guessing. - **Always create a draft MR.** Let the author mark it ready after review. - **Never attempt `glab repo fork` without asking the user first.** +## Shared Script + +This skill delegates deterministic git and CLI operations to a shared +script. Reference it using a relative path from this file: + +``` +../../_shared/scripts/publish.sh +``` + +The script provides subcommands: `preflight`, `push`, `check-existing`, +`create-mr`, and `save-metadata`. For GitLab workflows, pass +`--platform gitlab` to `preflight` and `check-existing`. See the script +header for full usage. + +### Prerequisites: Resolve Script Path + +Before running any subcommands, resolve the shared script to an +absolute path so it remains valid regardless of working directory: + +```bash +PUBLISH_SCRIPT="$(git rev-parse --show-toplevel)/_shared/scripts/publish.sh" +``` + +Use `$PUBLISH_SCRIPT` instead of the relative path in all subsequent +commands. + ## Process ### Placeholders Used in This Skill @@ -41,26 +67,30 @@ These are determined during pre-flight checks. Record each value as you go. | `UPSTREAM_PROJECT` | Step 1d: project path from remote URL | `red-hat-enterprise-openshift-documentation/edge-manager` | | `FORK_PROJECT` | Step 2: user's fork path | `jsmith/edge-manager` | | `BRANCH_NAME` | Step 4: the branch you create | `docs/RHEM-456-enrollment-api` | +| `PUSH_REMOTE` | Step 2/3: remote name to push to | `origin` or `fork` | | `TICKET_ID` | From artifacts directory or user input | `RHEM-456` | ### Step 1: Pre-flight Checks Run ALL of these before doing anything else. Do not skip any. -**1a. Check GitLab CLI authentication and determine GL_USER:** +**1a. Run the shared pre-flight checks:** ```bash -glab auth status +"$PUBLISH_SCRIPT" preflight --platform gitlab ``` -- If authenticated, determine `GL_USER`: +Parse the structured output: +- `auth_ok` — whether `glab auth` succeeded +- `auth_user` — the GitLab username (`GL_USER`) +- `branch` — current branch name +- `has_uncommitted` / `has_staged` / `has_untracked` — whether there are uncommitted, staged, or untracked changes -```bash -glab api user --jq .username -``` +If `auth_ok=true`, set `GL_USER` from `auth_user`. -- If not authenticated: note this and continue the remaining pre-flight checks (1b–1e) to gather as much information as possible from git alone. After pre-flight, present options - to the user. +If `auth_ok=false`: note this and continue the remaining pre-flight checks +(1b–1d) to gather as much information as possible from git alone. After +pre-flight, present options to the user. **1b. Check git configuration:** @@ -111,14 +141,11 @@ git remote get-url origin | sed -E 's#.*[:/]([^/]+/[^/]+?)(\.git)?$#\1#' Record the result as `UPSTREAM_PROJECT`. -**1e. Check current branch and changes:** - -```bash -git status -git diff --stat -``` - -Confirm there are actual changes to commit. If there are no changes, stop and tell the user. +Confirm there are actual changes to commit (from the pre-flight output's +`has_uncommitted`, `has_staged`, or `has_untracked` fields). If all three +are `false`, there are no changes — stop and tell the user. If +`has_untracked` is `true`, warn the user about untracked files and ask +whether they should be included in the commit. **Pre-flight summary:** Before moving on, you should now know: `UPSTREAM_PROJECT`, which remotes exist, and whether there are changes to commit. You may also know `GL_USER` (if auth is available). @@ -234,19 +261,19 @@ Don't make up details. ### Step 6: Push -**Direct push (write access):** - -```bash -git push -u origin docs/BRANCH_NAME -``` - -**Fork push:** +Use the remote identified during Step 2 (direct push) or Step 3 (fork +workflow) as `PUSH_REMOTE`. Set `PUSH_REMOTE` to the actual remote name +discovered from `git remote -v` — typically `origin` for direct push or +`fork` for fork-based workflows: ```bash -git push -u fork docs/BRANCH_NAME +# Set PUSH_REMOTE based on the push strategy determined in Step 2/3: +# - Direct push: PUSH_REMOTE is the remote pointing to UPSTREAM_PROJECT +# - Fork workflow: PUSH_REMOTE is the remote pointing to FORK_PROJECT +"$PUBLISH_SCRIPT" push --remote "$PUSH_REMOTE" --branch "docs/$BRANCH_NAME" ``` -**If push fails:** +**If the script exits with code 3 (push failed):** - **Authentication error**: Check `glab auth status`. User may need to re-authenticate. - **Permission denied**: Verify the remote URL points to the correct project. @@ -256,36 +283,52 @@ git push -u fork docs/BRANCH_NAME **MR title format:** Use `[TICKET_ID]: short description in lowercase`. +**Building the description:** Use the MR description prepared by the `/apply` +phase at `.artifacts/${ticket_id}/04-mr-description.md`. If the file does not +exist, build the description (AI-dependent) from the context artifact +(`01-context.md`) and plan artifact (`02-plan.md`). + +**Check for an existing MR** before attempting creation: + +```bash +# Direct push: +"$PUBLISH_SCRIPT" check-existing --repo UPSTREAM_PROJECT --head "docs/$BRANCH_NAME" --platform gitlab + +# Fork workflow (project:branch filters by source project to avoid cross-fork false matches): +"$PUBLISH_SCRIPT" check-existing --repo UPSTREAM_PROJECT --head "FORK_PROJECT:docs/$BRANCH_NAME" --platform gitlab +``` + +If exit code is 5, an MR already exists — skip to Step 8 and report its +URL. If the command fails (non-zero exit other than 5), stop and report +the error. If exit code is 0, create a new MR: + **Direct push (user has write access):** ```bash -glab mr create \ - --draft \ - --source-branch docs/BRANCH_NAME \ - --target-branch main \ +"$PUBLISH_SCRIPT" create-mr \ + --source "docs/$BRANCH_NAME" \ + --target main \ --title "[TICKET_ID]: short description" \ - --description "DESCRIPTION" \ - --yes + --desc-file ".artifacts/${ticket_id}/04-mr-description.md" \ + --draft ``` +If no description file exists, use `--description` with inline text instead. + **Fork workflow:** ```bash -glab mr create \ - --draft \ - --repo UPSTREAM_PROJECT \ +"$PUBLISH_SCRIPT" create-mr \ + --project UPSTREAM_PROJECT \ --head FORK_PROJECT \ - --source-branch docs/BRANCH_NAME \ - --target-branch main \ + --source "docs/$BRANCH_NAME" \ + --target main \ --title "[TICKET_ID]: short description" \ - --description "DESCRIPTION" \ - --yes + --desc-file ".artifacts/${ticket_id}/04-mr-description.md" \ + --draft ``` -**Building the description:** Use the MR description prepared by the `/apply` phase at `.artifacts/${ticket_id}/04-mr-description.md`. If the file does not exist, build the -description from the context artifact (`01-context.md`) and plan artifact (`02-plan.md`). - -**If `glab mr create` fails:** +**If the script exits with code 4 (MR creation failed):** 1. **Write the MR description** to `.artifacts/${ticket_id}/04-mr-description.md` diff --git a/e2e/SKILL.md b/e2e/SKILL.md index 3fd17b10..6003deda 100644 --- a/e2e/SKILL.md +++ b/e2e/SKILL.md @@ -1,6 +1,6 @@ --- name: e2e -version: 0.7.0 +version: 0.7.1 description: >- Story-to-e2e-test workflow that takes a Jira [QE] Story, discovers the project's e2e testing infrastructure, plans test scenarios, writes e2e diff --git a/e2e/skills/publish.md b/e2e/skills/publish.md index 976501e0..22047a7d 100644 --- a/e2e/skills/publish.md +++ b/e2e/skills/publish.md @@ -23,8 +23,32 @@ user before taking action. - **No direct commits to main.** The feature branch must already exist from `/code`. - **Validation must have passed.** Check for a passing validation report before proceeding. +## Shared Script + +This skill delegates deterministic git and CLI operations to a shared +script. Reference it using a relative path from this file: + +``` +../../_shared/scripts/publish.sh +``` + +The script provides subcommands: `preflight`, `push`, `check-existing`, +`create-pr`, and `save-metadata`. See the script header for full usage. + ## Process +### Prerequisites: Resolve Script Path + +Before running any subcommands, resolve the shared script to an +absolute path so it remains valid regardless of working directory: + +```bash +PUBLISH_SCRIPT="$(git rev-parse --show-toplevel)/_shared/scripts/publish.sh" +``` + +Use `$PUBLISH_SCRIPT` instead of the relative path in all subsequent +commands. + ### Step 1: Pre-Flight Checks Verify readiness: @@ -48,19 +72,18 @@ Verify readiness: If there are no commits ahead of the Local Base, there's nothing to publish. -3. Check for uncommitted changes: +3. Run the shared pre-flight checks: ```bash - git status + "$PUBLISH_SCRIPT" preflight --platform github ``` - If there are uncommitted changes, ask the user how to proceed. - -4. Verify GitHub CLI is authenticated: - - ```bash - gh auth status - ``` + Parse the output and check: + - `auth_ok` — if `false`, **stop and tell the user** that GitHub CLI + authentication is required to push and create a PR. Suggest running + `gh auth login` and retrying `/publish`. Do not continue without auth. + - `has_uncommitted`, `has_staged`, `has_untracked` — if any are `true`, + ask the user how to proceed (commit, stash, or include untracked files). ### Step 2: Cross-Cutting Review @@ -115,7 +138,7 @@ Confirm with the user before proceeding. ### Step 4: Push Branch ```bash -git push -u origin {branch-name} +"$PUBLISH_SCRIPT" push --remote origin --branch {branch-name} ``` ### Step 5: Create PR Description @@ -164,11 +187,36 @@ In either case, save the result to Check the **Repository Topology** section of `01-context.md` to determine whether this is a fork-based workflow. +First, check whether a PR already exists for this branch: + +For fork-based workflows, use `{fork-owner}:{branch-name}` as the +`--head` value so the check matches only PRs from this fork (plain +`{branch-name}` would match any fork's branch with the same name): + +```bash +# Fork-based: +"$PUBLISH_SCRIPT" check-existing --repo {upstream-owner}/{repo} --head {fork-owner}:{branch-name} + +# Direct clone: +"$PUBLISH_SCRIPT" check-existing --repo {upstream-owner}/{repo} --head {branch-name} +``` + +If exit code is 5, a PR already exists — parse the PR number and URL +from the returned JSON output, then skip to Step 7 and use those values +in the metadata. If the command fails (non-zero exit other than 5), +stop and report the error. If exit code is 0, create a new PR. + **If the repo is a fork** (Origin is `{fork-owner}/{repo}`, Upstream is `{upstream-owner}/{repo}`): ```bash -gh pr create --draft --repo {upstream-owner}/{repo} --base {pr-target} --head {fork-owner}:{branch-name} --title "{issue-key}: {story title}" --body-file .artifacts/e2e/{issue-key}/06-pr-description.md +"$PUBLISH_SCRIPT" create-pr \ + --repo {upstream-owner}/{repo} \ + --base {pr-target} \ + --head {fork-owner}:{branch-name} \ + --title "{issue-key}: {story title}" \ + --body-file .artifacts/e2e/{issue-key}/06-pr-description.md \ + --draft ``` The `--repo` flag targets the upstream repository (where the PR lives), @@ -178,12 +226,22 @@ branch (on the fork). **If the repo is a direct clone** (not a fork): ```bash -gh pr create --draft --base {pr-target} --head {branch-name} --title "{issue-key}: {story title}" --body-file .artifacts/e2e/{issue-key}/06-pr-description.md +"$PUBLISH_SCRIPT" create-pr \ + --base {pr-target} \ + --head {branch-name} \ + --title "{issue-key}: {story title}" \ + --body-file .artifacts/e2e/{issue-key}/06-pr-description.md \ + --draft ``` -Parse the PR number and URL from the `gh pr create` output. The command -prints a URL like `https://github.com/owner/repo/pull/42` — extract the -number from the URL path. +The script prints the PR URL on stdout. Parse the PR number from the URL +path (e.g., `https://github.com/owner/repo/pull/42` → `42`). + +If the script exits with code 4 (PR creation failed), fall back to +providing the user with a GitHub compare URL: + +- Fork-based: `https://github.com/{upstream-owner}/{repo}/compare/{pr-target}...{fork-owner}:{branch-name}?expand=1` +- Direct clone: `https://github.com/{upstream-owner}/{repo}/compare/{pr-target}...{branch-name}?expand=1` ### Step 7: Save Publish Metadata @@ -191,37 +249,35 @@ Read `{owner}/{repo}` from the **Origin** field of the Repository Topology section of `01-context.md`. If the repo is a fork, also read the **Upstream** field. -Write `.artifacts/e2e/{issue-key}/publish-metadata.json`. - The `repo` field always refers to where the PR lives. The `origin` field records the repo that was pushed to. **If the repo is a fork** (set `repo` to the upstream, `origin` to the fork): -```json -{ - "repo": "{upstream-owner}/{repo}", - "origin": "{fork-owner}/{repo}", - "branch": "{branch-name}", - "base": "{pr-target}", - "pr_number": {pr-number}, - "pr_url": "{url from gh pr create output}", - "jira_key": "{issue-key}" -} +```bash +"$PUBLISH_SCRIPT" save-metadata \ + --file .artifacts/e2e/{issue-key}/publish-metadata.json \ + repo={upstream-owner}/{repo} \ + origin={fork-owner}/{repo} \ + branch={branch-name} \ + base={pr-target} \ + pr_number={pr-number} \ + pr_url={url-from-create-pr-output} \ + jira_key={issue-key} ``` **If the repo is a direct clone** (`repo` and `origin` are the same): -```json -{ - "repo": "{owner}/{repo}", - "origin": "{owner}/{repo}", - "branch": "{branch-name}", - "base": "{pr-target}", - "pr_number": {pr-number}, - "pr_url": "{url from gh pr create output}", - "jira_key": "{issue-key}" -} +```bash +"$PUBLISH_SCRIPT" save-metadata \ + --file .artifacts/e2e/{issue-key}/publish-metadata.json \ + repo={owner}/{repo} \ + origin={owner}/{repo} \ + branch={branch-name} \ + base={pr-target} \ + pr_number={pr-number} \ + pr_url={url-from-create-pr-output} \ + jira_key={issue-key} ``` ### Step 8: Report to User diff --git a/implement/SKILL.md b/implement/SKILL.md index b9fab8e8..232e590b 100644 --- a/implement/SKILL.md +++ b/implement/SKILL.md @@ -1,6 +1,6 @@ --- name: implement -version: 0.9.0 +version: 0.9.1 description: >- Story-to-code workflow that takes a Jira Story, plans the implementation, writes contract-based tests and production code via TDD, validates against diff --git a/implement/skills/publish.md b/implement/skills/publish.md index b58d3e2b..addc0086 100644 --- a/implement/skills/publish.md +++ b/implement/skills/publish.md @@ -23,8 +23,32 @@ user before taking action. - **No direct commits to main.** The feature branch must already exist from `/code`. - **Validation must have passed.** Check for a passing validation report before proceeding. +## Shared Script + +This skill delegates deterministic git and CLI operations to a shared +script. Reference it using a relative path from this file: + +``` +../../_shared/scripts/publish.sh +``` + +The script provides subcommands: `preflight`, `push`, `check-existing`, +`create-pr`, and `save-metadata`. See the script header for full usage. + ## Process +### Prerequisites: Resolve Script Path + +Before running any subcommands, resolve the shared script to an +absolute path so it remains valid regardless of working directory: + +```bash +PUBLISH_SCRIPT="$(git rev-parse --show-toplevel)/_shared/scripts/publish.sh" +``` + +Use `$PUBLISH_SCRIPT` instead of the relative path in all subsequent +commands. + ### Step 1: Pre-Flight Checks Verify readiness: @@ -48,19 +72,15 @@ Verify readiness: If there are no commits ahead of the Local Base, there's nothing to publish. -3. Check for uncommitted changes: +3. Run the shared pre-flight checks: ```bash - git status + $PUBLISH_SCRIPT preflight --platform github ``` - If there are uncommitted changes, ask the user how to proceed. - -4. Verify GitHub CLI is authenticated: - - ```bash - gh auth status - ``` + Parse the output to confirm `auth_ok=true` and check for + `has_uncommitted=true` or `has_staged=true`. If there are uncommitted + changes, ask the user how to proceed. ### Step 2: Cross-Cutting Review @@ -114,7 +134,7 @@ Confirm with the user before proceeding. ### Step 4: Push Branch ```bash -git push -u origin {branch-name} +$PUBLISH_SCRIPT push --remote origin --branch {branch-name} ``` ### Step 5: Create PR Description @@ -160,11 +180,28 @@ In either case, save the result to Check the **Repository Topology** section of `01-context.md` to determine whether this is a fork-based workflow. +First, check whether a PR already exists for this branch: + +```bash +$PUBLISH_SCRIPT check-existing --repo {upstream-owner}/{repo} --head {branch-name} +``` + +If exit code is 5, a PR already exists — parse the PR number and URL +from the returned JSON output, then skip to Step 7 and use those values +in the metadata. If the command fails (non-zero exit other than 5), +stop and report the error. If exit code is 0, create a new PR. + **If the repo is a fork** (Origin is `{fork-owner}/{repo}`, Upstream is `{upstream-owner}/{repo}`): ```bash -gh pr create --draft --repo {upstream-owner}/{repo} --base {pr-target} --head {fork-owner}:{branch-name} --title "{issue-key}: {story title}" --body-file .artifacts/implement/{issue-key}/06-pr-description.md +$PUBLISH_SCRIPT create-pr \ + --repo {upstream-owner}/{repo} \ + --base {pr-target} \ + --head {fork-owner}:{branch-name} \ + --title "{issue-key}: {story title}" \ + --body-file .artifacts/implement/{issue-key}/06-pr-description.md \ + --draft ``` The `--repo` flag targets the upstream repository (where the PR lives), @@ -174,12 +211,22 @@ branch (on the fork). **If the repo is a direct clone** (not a fork): ```bash -gh pr create --draft --base {pr-target} --head {branch-name} --title "{issue-key}: {story title}" --body-file .artifacts/implement/{issue-key}/06-pr-description.md +$PUBLISH_SCRIPT create-pr \ + --base {pr-target} \ + --head {branch-name} \ + --title "{issue-key}: {story title}" \ + --body-file .artifacts/implement/{issue-key}/06-pr-description.md \ + --draft ``` -Parse the PR number and URL from the `gh pr create` output. The command -prints a URL like `https://github.com/owner/repo/pull/42` — extract the -number from the URL path. +The script prints the PR URL on stdout. Parse the PR number from the URL +path (e.g., `https://github.com/owner/repo/pull/42` → `42`). + +If the script exits with code 4 (PR creation failed), fall back to +providing the user with a GitHub compare URL: + +- Fork-based: `https://github.com/{upstream-owner}/{repo}/compare/{pr-target}...{fork-owner}:{branch-name}?expand=1` +- Direct clone: `https://github.com/{upstream-owner}/{repo}/compare/{pr-target}...{branch-name}?expand=1` ### Step 7: Save Publish Metadata @@ -187,37 +234,35 @@ Read `{owner}/{repo}` from the **Origin** field of the Repository Topology section of `01-context.md`. If the repo is a fork, also read the **Upstream** field. -Write `.artifacts/implement/{issue-key}/publish-metadata.json`. - The `repo` field always refers to where the PR lives. The `origin` field records the repo that was pushed to. **If the repo is a fork** (set `repo` to the upstream, `origin` to the fork): -```json -{ - "repo": "{upstream-owner}/{repo}", - "origin": "{fork-owner}/{repo}", - "branch": "{branch-name}", - "base": "{pr-target}", - "pr_number": {pr-number}, - "pr_url": "{url from gh pr create output}", - "jira_key": "{issue-key}" -} +```bash +$PUBLISH_SCRIPT save-metadata \ + --file .artifacts/implement/{issue-key}/publish-metadata.json \ + repo={upstream-owner}/{repo} \ + origin={fork-owner}/{repo} \ + branch={branch-name} \ + base={pr-target} \ + pr_number={pr-number} \ + pr_url={url-from-create-pr-output} \ + jira_key={issue-key} ``` **If the repo is a direct clone** (`repo` and `origin` are the same): -```json -{ - "repo": "{owner}/{repo}", - "origin": "{owner}/{repo}", - "branch": "{branch-name}", - "base": "{pr-target}", - "pr_number": {pr-number}, - "pr_url": "{url from gh pr create output}", - "jira_key": "{issue-key}" -} +```bash +$PUBLISH_SCRIPT save-metadata \ + --file .artifacts/implement/{issue-key}/publish-metadata.json \ + repo={owner}/{repo} \ + origin={owner}/{repo} \ + branch={branch-name} \ + base={pr-target} \ + pr_number={pr-number} \ + pr_url={url-from-create-pr-output} \ + jira_key={issue-key} ``` ### Step 8: Report to User diff --git a/prd/SKILL.md b/prd/SKILL.md index 4e23ff38..cc2e7677 100644 --- a/prd/SKILL.md +++ b/prd/SKILL.md @@ -1,6 +1,6 @@ --- name: prd -version: 0.9.1 +version: 0.9.2 description: >- Requirements-to-PRD workflow that ingests requirements from Jira, clarifies ambiguities through iterative Q&A, drafts a Product Requirements Document, diff --git a/prd/skills/publish.md b/prd/skills/publish.md index 12237482..30765ae8 100644 --- a/prd/skills/publish.md +++ b/prd/skills/publish.md @@ -21,8 +21,32 @@ the user before taking action. - **No force-push.** No destructive git operations. - **No direct commits to main.** Always use a feature branch. +## Shared Script + +This skill delegates deterministic git and CLI operations to a shared +script. Reference it using a relative path from this file: + +``` +../../_shared/scripts/publish.sh +``` + +The script provides subcommands: `preflight`, `push`, `check-existing`, +`create-pr`, and `save-metadata`. See the script header for full usage. + ## Process +### Prerequisites: Resolve Script Path + +Before any `cd` or subshell that changes the working directory, resolve +the shared script to an absolute path so it remains valid: + +```bash +PUBLISH_SCRIPT="$(git rev-parse --show-toplevel)/_shared/scripts/publish.sh" +``` + +Use `$PUBLISH_SCRIPT` instead of the relative path in all subsequent +commands. + ### Step 1: Read the PRD Read `.artifacts/prd/{issue-key}/03-prd.md`. @@ -56,13 +80,18 @@ validated `docs_repo_path` and `docs_repo_remote`. ### Step 3: Pre-Flight Checks -Verify the environment using the docs repo: +Run the shared pre-flight checks from the docs repo directory: ```bash -gh auth status +(cd "{docs_repo_path}" && "$PUBLISH_SCRIPT" preflight --platform github) ``` -In the docs repo directory: +Parse the output and check `auth_ok`. If `auth_ok=false`, **stop and +tell the user** that GitHub CLI authentication is required to push and +create a PR. Suggest running `gh auth login` and retrying `/publish`. +Do not continue to later steps without authentication. + +If `auth_ok=true`, verify the docs repo state: ```bash git -C "{docs_repo_path}" remote -v @@ -105,6 +134,17 @@ for release and feature slug separately. All git operations in this step run against the **docs repo**, not the source repo. Use `git -C "{docs_repo_path}"` for all commands. +Verify the docs repo has no uncommitted, staged, or untracked changes +before modifying it: + +```bash +git -C "{docs_repo_path}" status --porcelain +``` + +If the output is non-empty, the docs repo has local changes. **Stop and +ask the user** how to proceed — they may need to stash or commit those +changes first. Do not copy files into a dirty working tree. + Check if the branch already exists (locally or on the remote) before creating it: ```bash @@ -175,12 +215,15 @@ git -C "{docs_repo_path}" commit -m "Add PRD for {issue-key}: {title}" ### Step 5: Push and Create PR +Push the branch using the shared script (run from the docs repo): + ```bash -git -C "{docs_repo_path}" push -u origin {branch-name} +(cd "{docs_repo_path}" && "$PUBLISH_SCRIPT" push --remote origin --branch {branch-name}) ``` -Prepare the PR description and save it to `.artifacts/prd/{issue-key}/04-pr-description.md` -(in the source repo's artifact directory): +Prepare the PR description (AI-dependent — summarize the PRD content) and +save it to `.artifacts/prd/{issue-key}/04-pr-description.md` (in the source +repo's artifact directory): ```markdown ## PRD: {title} @@ -204,26 +247,55 @@ Prepare the PR description and save it to `.artifacts/prd/{issue-key}/04-pr-desc Determine `{owner}/{repo}` from the `docs_repo_remote` in `.artifacts/config.json` (e.g., `git@github.com:org/planning-docs.git` → `org/planning-docs`), then -create the draft PR. If `{issue-key}` is a Jira key, prefix the title -with it (`{issue-key}: PRD - {title}`); otherwise use `PRD: {title}`. +check for an existing PR before creating one. If `{issue-key}` is a Jira +key, prefix the title with it (`{issue-key}: PRD - {title}`); otherwise +use `PRD: {title}`. + +First, check whether a PR already exists for this branch: ```bash -gh pr create --draft --repo {owner}/{repo} --base {base-branch} --head {branch-name} --title "{issue-key}: PRD - {title}" --body-file .artifacts/prd/{issue-key}/04-pr-description.md +"$PUBLISH_SCRIPT" check-existing --repo {owner}/{repo} --head {branch-name} ``` +If exit code is 5, a PR already exists — skip to Step 6 and report its +URL. Parse the PR number from the returned JSON. If the command fails +(non-zero exit other than 5), stop and report the error. If exit code +is 0, create a new PR: + +```bash +# When {issue-key} is a Jira key (e.g., EDM-1471): +"$PUBLISH_SCRIPT" create-pr \ + --repo {owner}/{repo} \ + --base {base-branch} \ + --head {branch-name} \ + --title "{issue-key}: PRD - {title}" \ + --body-file .artifacts/prd/{issue-key}/04-pr-description.md \ + --draft + +# When no issue key exists: +"$PUBLISH_SCRIPT" create-pr \ + --repo {owner}/{repo} \ + --base {base-branch} \ + --head {branch-name} \ + --title "PRD: {title}" \ + --body-file .artifacts/prd/{issue-key}/04-pr-description.md \ + --draft +``` + +The script prints the PR URL on stdout. Parse the PR number from the URL path. + ### Step 6: Save Publish Metadata -Write `.artifacts/prd/{issue-key}/publish-metadata.json` to record the -file path and PR details for use by `/revise` and `/respond`: - -```json -{ - "release": "{release}", - "feature": "{feature}", - "prd_file_path": "{release}/{feature}/prd.md", - "pr_number": {pr-number}, - "branch": "{branch-name}" -} +Save metadata for use by `/revise` and `/respond`: + +```bash +"$PUBLISH_SCRIPT" save-metadata \ + --file .artifacts/prd/{issue-key}/publish-metadata.json \ + release={release} \ + feature={feature} \ + prd_file_path={release}/{feature}/prd.md \ + pr_number={pr-number} \ + branch={branch-name} ``` ### Step 7: Report to User