diff --git a/.claude/commands/release.md b/.claude/commands/release.md index f579bb31..72f2c569 100644 --- a/.claude/commands/release.md +++ b/.claude/commands/release.md @@ -150,17 +150,9 @@ Once complete: - Homebrew: brew tap calcmark/tap && brew upgrade calcmark/tap/calcmark ``` -## Step 9: Post velocity discussion +## Step 9: Velocity discussion (automated) -After the tag push succeeds, post a velocity report as a GitHub Discussion: - -```bash -.claude/skills/github-project/scripts/release-velocity.sh vX.Y.Z -``` - -This finds all issues referenced in commits since the previous tag, calculates lead time and cycle time for each, and posts an Announcements discussion with a metrics table and link to the release notes. - -Print the discussion URL when done. +The `velocity-release.yml` GitHub Actions workflow automatically posts a release quality Discussion when the release is published. **Do not post manually** — the workflow handles it. ## Hard Rules diff --git a/.claude/skills/github-project/SKILL.md b/.claude/skills/github-project/SKILL.md index 7bbc82cf..0e42f170 100644 --- a/.claude/skills/github-project/SKILL.md +++ b/.claude/skills/github-project/SKILL.md @@ -1,7 +1,7 @@ --- name: github-project description: Manage GitHub issues and project status for CalcMark work. Use when starting work, transitioning status, creating issues, or finishing features. Handles both local-only workflows (merge to main) and remote PR workflows. -allowed-tools: Bash(gh:*),Bash(git:*),Bash(jq:*),Bash(echo:*),Bash(printf:*),Bash(.claude/skills/github-project/scripts/*),Read,Glob,Grep +allowed-tools: Bash(gh:*),Bash(git:*),Bash(jq:*),Bash(echo:*),Bash(printf:*),Read,Glob,Grep --- # GitHub Project Management @@ -136,58 +136,35 @@ When work is finished (status set to "In review" and handed to human), run the m Both metrics share the same end point: the work appears in a release that users can see. The difference is the start point — lead time starts when the idea is filed, cycle time starts when coding begins. -### Scripts +### Velocity Metrics via gh-velocity -All metric scripts live in `.claude/skills/github-project/scripts/`: +All velocity metrics are handled by the [`gh-velocity`](https://dvhthomas.github.io/gh-velocity/) GitHub CLI extension. Configuration lives in `.gh-velocity.yml` at the repo root. -| Script | Purpose | Usage | -|--------|---------|-------| -| `lead-time.sh` | Issue created → now (snapshot; true lead time measured at release) | `.claude/skills/github-project/scripts/lead-time.sh ` | -| `cycle-time.sh` | First commit → last commit for an issue (active work duration) | `.claude/skills/github-project/scripts/cycle-time.sh [BRANCH]` | -| `pr-metrics.sh` | PR size (additions, deletions, files) | `.claude/skills/github-project/scripts/pr-metrics.sh ` | -| `issue-summary.sh` | Full completion summary (auto-detects release) | `.claude/skills/github-project/scripts/issue-summary.sh [BRANCH] [PR_NUMBER]` | -| `release-velocity.sh` | Post velocity Discussion for a release | `.claude/skills/github-project/scripts/release-velocity.sh ` | -| `helpers.sh` | Shared functions (sourced by other scripts) | `source .claude/skills/github-project/scripts/helpers.sh` | +**Commands for local use:** -`cycle-time.sh` finds commits associated with an issue through multiple signals: -1. Commits mentioning `(#N)` in the message (conventional commit trailers) -2. Commits linked to PRs that reference the issue -3. Commits on an explicit branch (optional fallback) -4. Commit SHAs mentioned in the issue body or comments +| Command | Purpose | +|---------|---------| +| `gh velocity issue ` | Full metrics for a single issue (lead time, cycle time) | +| `gh velocity pr ` | Full metrics for a single PR | +| `gh velocity flow lead-time ` | Lead time for a specific issue | +| `gh velocity flow cycle-time ` | Cycle time for a specific issue | +| `gh velocity report --since 7d` | Project-wide report for the last 7 days | +| `gh velocity quality release ` | Release quality analysis | -Run the summary script when handing work to the human: +**When handing work to the human**, run: ```bash -.claude/skills/github-project/scripts/issue-summary.sh [BRANCH] [PR_NUMBER] +gh velocity issue -r pretty ``` -### Release Velocity Discussion +**Automated workflows** (no manual action needed): -After every release, post a velocity report as a GitHub Discussion in the Announcements category: - -```bash -.claude/skills/github-project/scripts/release-velocity.sh v1.6.5 -``` - -This is called automatically at the end of the `/release` skill (Step 9). It: -1. Finds all issues referenced in commits between the previous tag and this tag -2. Calculates lead time (created → release) and cycle time (first → last commit) for each -3. Posts a Discussion with a metrics table and link to release notes - -The `/release` skill MUST call this script after Step 8 (post-release summary). - -### Future: `gh velocity` Extension - -These metrics scripts are candidates for a future `gh` CLI extension written in Go: - -``` -gh velocity show # current open issues with age -gh velocity stats # lead/cycle time stats across closed issues -gh velocity trend # trend chart over last N issues/releases -gh velocity release v1.2 # metrics for a specific release -``` - -This is out of scope for now but the data model is already in place via issue timestamps, commit timestamps, and PR metadata. +| Workflow | Trigger | What it does | +|----------|---------|--------------| +| `velocity-weekly.yml` | Monday 09:00 UTC | Posts a Discussion with 7-day project metrics | +| `velocity-pr.yml` | PR merged | Appends velocity metrics to PR body | +| `velocity-issue.yml` | Issue closed (completed) | Appends velocity metrics to issue body | +| `velocity-release.yml` | Release published | Posts release quality Discussion | ## Integration with Compound Engineering Pipeline diff --git a/.claude/skills/github-project/scripts/cycle-time.sh b/.claude/skills/github-project/scripts/cycle-time.sh deleted file mode 100755 index 6af6d5e1..00000000 --- a/.claude/skills/github-project/scripts/cycle-time.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -# cycle-time.sh — Measure coding duration for a GitHub issue. -# -# Finds commits associated with an issue through multiple signals: -# 1. Commits mentioning (#N) in the message (conventional commit trailers) -# 2. Commits linked to a PR that closes the issue -# 3. Commits on a branch passed as optional second argument -# 4. Commit SHAs mentioned in the issue body or comments -# -# Usage: ./cycle-time.sh [BRANCH] -set -euo pipefail - -ISSUE_NUMBER="${1:?Usage: cycle-time.sh [BRANCH]}" -BRANCH="${2:-}" -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/helpers.sh" - -# Collect commit hashes from all sources (deduped later) -COMMIT_HASHES="" - -# Source 1: Commits referencing (#N) in the message -PATTERN_COMMITS=$(git log --all --grep="(#${ISSUE_NUMBER})" --format="%H" 2>/dev/null || true) -if [ -n "$PATTERN_COMMITS" ]; then - COMMIT_HASHES="$PATTERN_COMMITS" -fi - -# Source 2: Commits on an explicit branch (relative to main) -if [ -n "$BRANCH" ] && [ "$BRANCH" != "main" ]; then - BRANCH_COMMITS=$(git log main.."$BRANCH" --format="%H" 2>/dev/null || true) - if [ -n "$BRANCH_COMMITS" ]; then - COMMIT_HASHES=$(printf "%s\n%s" "$COMMIT_HASHES" "$BRANCH_COMMITS") - fi -fi - -# Source 3: Commits from PRs that reference the issue -PR_NUMBERS=$(gh issue view "$ISSUE_NUMBER" --json timelineItems 2>/dev/null | \ - jq -r '[.timelineItems[] | select(.source.__typename == "PullRequest") | .source.number] | unique | .[]' 2>/dev/null || true) - -for PR in $PR_NUMBERS; do - PR_COMMITS=$(gh pr view "$PR" --json commits 2>/dev/null | \ - jq -r '.commits[].oid' 2>/dev/null || true) - if [ -n "$PR_COMMITS" ]; then - COMMIT_HASHES=$(printf "%s\n%s" "$COMMIT_HASHES" "$PR_COMMITS") - fi -done - -# Source 4: Commit SHAs mentioned in the issue body or comments -ISSUE_TEXT=$(gh issue view "$ISSUE_NUMBER" --json body,comments \ - --jq '[.body, (.comments[]?.body // empty)] | join("\n")' 2>/dev/null || true) -if [ -n "$ISSUE_TEXT" ]; then - # Match 7-40 char hex strings that are valid git objects - MENTIONED_SHAS=$(echo "$ISSUE_TEXT" | grep -oE '\b[0-9a-f]{7,40}\b' | while read -r sha; do - # Verify it's a real commit in this repo - if git cat-file -t "$sha" 2>/dev/null | grep -q "commit"; then - git rev-parse "$sha" 2>/dev/null - fi - done || true) - if [ -n "$MENTIONED_SHAS" ]; then - COMMIT_HASHES=$(printf "%s\n%s" "$COMMIT_HASHES" "$MENTIONED_SHAS") - fi -fi - -# Deduplicate and filter empty lines -COMMIT_HASHES=$(echo "$COMMIT_HASHES" | sort -u | sed '/^$/d') - -if [ -z "$COMMIT_HASHES" ]; then - echo "No commits found for issue #${ISSUE_NUMBER}" - echo "Looked for: commits with '(#${ISSUE_NUMBER})' in message, linked PRs${BRANCH:+, branch $BRANCH}" - exit 1 -fi - -# Get timestamps for all commits, sort chronologically -COMMIT_TIMES=$(echo "$COMMIT_HASHES" | while read -r hash; do - git log -1 --format="%aI %H" "$hash" 2>/dev/null || true -done | sort) - -FIRST_LINE=$(echo "$COMMIT_TIMES" | head -1) -LAST_LINE=$(echo "$COMMIT_TIMES" | tail -1) -FIRST_TIME=$(echo "$FIRST_LINE" | awk '{print $1}') -LAST_TIME=$(echo "$LAST_LINE" | awk '{print $1}') -FIRST_HASH=$(echo "$FIRST_LINE" | awk '{print $2}') -LAST_HASH=$(echo "$LAST_LINE" | awk '{print $2}') -COMMIT_COUNT=$(echo "$COMMIT_HASHES" | wc -l | tr -d ' ') - -FIRST_EPOCH=$(parse_iso_epoch "$FIRST_TIME") -LAST_EPOCH=$(parse_iso_epoch "$LAST_TIME") -DIFF_SECONDS=$((LAST_EPOCH - FIRST_EPOCH)) - -FIRST_SHORT=$(git log -1 --format="%h %s" "$FIRST_HASH") -LAST_SHORT=$(git log -1 --format="%h %s" "$LAST_HASH") - -echo "- Cycle time: $(format_duration $DIFF_SECONDS) (first commit → last commit)" -echo "- Commits: $COMMIT_COUNT" -echo "- First: $FIRST_SHORT" -echo "- Last: $LAST_SHORT" -# Machine-readable last hash for callers (e.g., issue-summary.sh release detection) -echo "- LastHash: $LAST_HASH" diff --git a/.claude/skills/github-project/scripts/helpers.sh b/.claude/skills/github-project/scripts/helpers.sh deleted file mode 100644 index c4570e5a..00000000 --- a/.claude/skills/github-project/scripts/helpers.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -# helpers.sh — Shared functions for metric scripts. -# Source this file: source "$(dirname "$0")/helpers.sh" - -# Parse ISO 8601 timestamp to epoch seconds (portable: macOS + Linux). -# Handles both Z suffix (2026-03-09T15:12:06Z) and offset (2026-03-09T09:12:06-06:00). -parse_iso_epoch() { - local ts="$1" - local ts_clean - ts_clean=$(echo "$ts" | sed 's/Z$/+0000/' | sed 's/\([+-][0-9][0-9]\):\([0-9][0-9]\)$/\1\2/') - date -jf "%Y-%m-%dT%H:%M:%S%z" "$ts_clean" +%s 2>/dev/null || date -d "$ts" +%s -} - -# Format seconds as human-readable duration: "3d 14h", "2h 8m", "28m", "5s". -format_duration() { - local secs=$1 - if [ "$secs" -ge 86400 ]; then - echo "$((secs / 86400))d $((secs % 86400 / 3600))h" - elif [ "$secs" -ge 3600 ]; then - echo "$((secs / 3600))h $((secs % 3600 / 60))m" - elif [ "$secs" -ge 60 ]; then - echo "$((secs / 60))m" - else - echo "${secs}s" - fi -} - -# Find the earliest semver release tag that contains a given commit. -# Returns the tag name, or empty string if no release contains it yet. -find_release_tag() { - local commit="$1" - # Get all semver tags containing this commit, sorted by version ascending - git tag --contains "$commit" 2>/dev/null \ - | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ - | sort -V \ - | head -1 -} - -# Get the epoch timestamp of a git tag (tagger date for annotated, commit date for lightweight). -tag_epoch() { - local tag="$1" - git log -1 --format="%ct" "$tag" 2>/dev/null -} diff --git a/.claude/skills/github-project/scripts/issue-summary.sh b/.claude/skills/github-project/scripts/issue-summary.sh deleted file mode 100755 index 26dee265..00000000 --- a/.claude/skills/github-project/scripts/issue-summary.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env bash -# issue-summary.sh — Print full completion metrics for a GitHub issue. -# -# Automatically detects if the issue's commits shipped in a release tag. -# If so, lead time measures created → release. Otherwise created → now. -# -# Usage: ./issue-summary.sh [BRANCH] [PR_NUMBER] -set -euo pipefail - -ISSUE_NUMBER="${1:?Usage: issue-summary.sh [BRANCH] [PR_NUMBER]}" -BRANCH="${2:-}" -PR_NUMBER="${3:-}" - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/helpers.sh" - -# Gather issue info -ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" --json createdAt,title,number,state) -TITLE=$(echo "$ISSUE_JSON" | jq -r '.title') -CREATED=$(echo "$ISSUE_JSON" | jq -r '.createdAt') -CREATED_EPOCH=$(parse_iso_epoch "$CREATED") - -# Cycle time via cycle-time.sh (issue-based) -CYCLE_OUTPUT=$("$SCRIPT_DIR/cycle-time.sh" "$ISSUE_NUMBER" "$BRANCH" 2>/dev/null || echo "- Cycle time: N/A (no commits found)") - -# Extract fields from cycle-time output -CYCLE_TIME=$(echo "$CYCLE_OUTPUT" | head -1 | sed 's/^- Cycle time: //') -COMMIT_LINE=$(echo "$CYCLE_OUTPUT" | grep "^- Commits:" | sed 's/^- Commits: //' || echo "0") -FIRST_COMMIT=$(echo "$CYCLE_OUTPUT" | grep "^- First:" || true) -LAST_COMMIT_LINE=$(echo "$CYCLE_OUTPUT" | grep "^- Last:" || true) -LAST_HASH=$(echo "$CYCLE_OUTPUT" | grep "^- LastHash:" | sed 's/^- LastHash: //' || true) - -# Detect release: find earliest semver tag containing the last commit -RELEASE_TAG="" -STATUS_LINE="In review → awaiting human verification" -if [ -n "$LAST_HASH" ]; then - RELEASE_TAG=$(find_release_tag "$LAST_HASH") -fi - -if [ -n "$RELEASE_TAG" ]; then - END_EPOCH=$(tag_epoch "$RELEASE_TAG") - LEAD_SECONDS=$((END_EPOCH - CREATED_EPOCH)) - LEAD_LABEL="(created → $RELEASE_TAG)" - STATUS_LINE="Shipped in $RELEASE_TAG" -else - NOW_EPOCH=$(date +%s) - LEAD_SECONDS=$((NOW_EPOCH - CREATED_EPOCH)) - LEAD_LABEL="(created → now; not yet released)" - STATUS_LINE="In review → awaiting human verification" -fi - -# Print summary -echo "────────────────────────────────────────" -echo "Issue #${ISSUE_NUMBER}: $TITLE" -echo "────────────────────────────────────────" -echo "Status: $STATUS_LINE" -echo "Lead time: $(format_duration $LEAD_SECONDS) $LEAD_LABEL" -echo "Cycle time: ${CYCLE_TIME}" -echo "Commits: $COMMIT_LINE" - -if [ -n "$FIRST_COMMIT" ]; then - echo "$FIRST_COMMIT" - echo "$LAST_COMMIT_LINE" -fi - -if [ -n "$PR_NUMBER" ]; then - PR_LINE=$("$SCRIPT_DIR/pr-metrics.sh" "$PR_NUMBER" 2>/dev/null || echo "- PR: #${PR_NUMBER}") - echo "PR: #${PR_NUMBER}" - echo "$PR_LINE" -fi - -echo "────────────────────────────────────────" diff --git a/.claude/skills/github-project/scripts/lead-time.sh b/.claude/skills/github-project/scripts/lead-time.sh deleted file mode 100755 index 43fe1dd8..00000000 --- a/.claude/skills/github-project/scripts/lead-time.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -# lead-time.sh — Calculate lead time for a GitHub issue. -# -# If the issue's commits are included in a release tag, measures created → release. -# Otherwise measures created → now (snapshot). -# -# Usage: ./lead-time.sh -set -euo pipefail - -ISSUE_NUMBER="${1:?Usage: lead-time.sh }" -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/helpers.sh" - -ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" --json createdAt,title,number,state) -CREATED=$(echo "$ISSUE_JSON" | jq -r '.createdAt') -TITLE=$(echo "$ISSUE_JSON" | jq -r '.title') - -CREATED_EPOCH=$(parse_iso_epoch "$CREATED") - -# Try to find a release containing this issue's commits -RELEASE_TAG="" -LAST_COMMIT=$(git log --all --grep="(#${ISSUE_NUMBER})" --format="%H" | head -1) -if [ -n "$LAST_COMMIT" ]; then - RELEASE_TAG=$(find_release_tag "$LAST_COMMIT") -fi - -if [ -n "$RELEASE_TAG" ]; then - END_EPOCH=$(tag_epoch "$RELEASE_TAG") - END_LABEL="$RELEASE_TAG release" -else - END_EPOCH=$(date +%s) - END_LABEL="now (not yet released)" -fi - -LEAD_SECONDS=$((END_EPOCH - CREATED_EPOCH)) - -echo "## Issue #${ISSUE_NUMBER}: $TITLE" -echo "- Created: $CREATED" -echo "- Lead time: $(format_duration $LEAD_SECONDS) (created → $END_LABEL)" diff --git a/.claude/skills/github-project/scripts/pr-metrics.sh b/.claude/skills/github-project/scripts/pr-metrics.sh deleted file mode 100755 index 660d25a6..00000000 --- a/.claude/skills/github-project/scripts/pr-metrics.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -# pr-metrics.sh — Fetch PR size metrics from GitHub. -# Usage: ./pr-metrics.sh -set -euo pipefail - -PR_NUMBER="${1:?Usage: pr-metrics.sh }" - -PR_JSON=$(gh pr view "$PR_NUMBER" --json createdAt,mergedAt,additions,deletions,changedFiles,commits) -ADDITIONS=$(echo "$PR_JSON" | jq -r '.additions') -DELETIONS=$(echo "$PR_JSON" | jq -r '.deletions') -CHANGED_FILES=$(echo "$PR_JSON" | jq -r '.changedFiles') -PR_COMMITS=$(echo "$PR_JSON" | jq -r '.commits | length') - -echo "- PR: +$ADDITIONS/-$DELETIONS across $CHANGED_FILES files ($PR_COMMITS commits)" diff --git a/.claude/skills/github-project/scripts/release-velocity.sh b/.claude/skills/github-project/scripts/release-velocity.sh deleted file mode 100755 index e0cfe2f3..00000000 --- a/.claude/skills/github-project/scripts/release-velocity.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash -# release-velocity.sh — Post a GitHub Discussion with velocity metrics for a release. -# -# Finds all issues included in a release (via commit messages between tags), -# calculates lead time and cycle time for each, and posts an Announcements -# discussion linking to the release notes. -# -# Usage: ./release-velocity.sh -# e.g.: ./release-velocity.sh v1.6.5 -set -euo pipefail - -TAG="${1:?Usage: release-velocity.sh }" -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/helpers.sh" - -# Validate tag exists -if ! git rev-parse "$TAG" >/dev/null 2>&1; then - echo "Error: tag $TAG not found" - exit 1 -fi - -# Find the previous semver tag -PREV_TAG=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -A1 "^${TAG}$" | tail -1) -if [ "$PREV_TAG" = "$TAG" ]; then - # TAG is the oldest — use all commits up to it - COMMIT_RANGE="$TAG" - PREV_TAG="(initial)" -else - COMMIT_RANGE="${PREV_TAG}..${TAG}" -fi - -# Extract unique issue numbers from commits in this release -ISSUE_NUMBERS=$(git log "$COMMIT_RANGE" --oneline --no-merges \ - | grep -oE '\(#[0-9]+\)' \ - | grep -oE '[0-9]+' \ - | sort -un) - -if [ -z "$ISSUE_NUMBERS" ]; then - echo "No issues found in commits between $PREV_TAG and $TAG" - exit 1 -fi - -ISSUE_COUNT=$(echo "$ISSUE_NUMBERS" | wc -l | tr -d ' ') -COMMIT_COUNT=$(git rev-list --count "$COMMIT_RANGE" --no-merges 2>/dev/null || echo "?") -RELEASE_DATE=$(git log -1 --format="%ai" "$TAG" | cut -d' ' -f1) - -# Build the metrics table -TABLE_ROWS="" -for ISSUE_NUM in $ISSUE_NUMBERS; do - # Get issue title - TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "(unknown)") - - # Get cycle time output - CYCLE_OUTPUT=$("$SCRIPT_DIR/cycle-time.sh" "$ISSUE_NUM" 2>/dev/null || true) - CYCLE_TIME=$(echo "$CYCLE_OUTPUT" | head -1 | sed 's/^- Cycle time: //' | sed 's/ (first commit.*$//' || echo "N/A") - ISSUE_COMMITS=$(echo "$CYCLE_OUTPUT" | grep "^- Commits:" | sed 's/^- Commits: //' || echo "?") - - # Get lead time (created → release tag) - CREATED=$(gh issue view "$ISSUE_NUM" --json createdAt --jq '.createdAt' 2>/dev/null || true) - LEAD_TIME="N/A" - if [ -n "$CREATED" ]; then - CREATED_EPOCH=$(parse_iso_epoch "$CREATED") - RELEASE_EPOCH=$(tag_epoch "$TAG") - LEAD_SECONDS=$((RELEASE_EPOCH - CREATED_EPOCH)) - if [ "$LEAD_SECONDS" -ge 0 ]; then - LEAD_TIME=$(format_duration $LEAD_SECONDS) - fi - fi - - TABLE_ROWS="${TABLE_ROWS}| #${ISSUE_NUM} | ${TITLE} | ${LEAD_TIME} | ${CYCLE_TIME} | ${ISSUE_COMMITS} | -" -done - -# Compose the discussion body -BODY="## Release Velocity: ${TAG} - -**Released:** ${RELEASE_DATE} -**Commits:** ${COMMIT_COUNT} (since ${PREV_TAG}) -**Issues closed:** ${ISSUE_COUNT} - -### Metrics - -| Issue | Title | Lead Time | Cycle Time | Commits | -|-------|-------|-----------|------------|---------| -${TABLE_ROWS} -**Lead time** = issue created → included in release -**Cycle time** = first commit → last commit for the issue - -### Definitions - -- **Lead time** measures the full idea-to-delivery pipeline. Shorter lead times mean faster response to reported issues. -- **Cycle time** measures active development duration. The gap between lead and cycle time reveals queue/wait time. - -### Release Notes - -https://github.com/CalcMark/go-calcmark/releases/tag/${TAG}" - -TITLE="Release Velocity: ${TAG}" - -# GraphQL IDs -REPO_ID="R_kgDOQSei8w" -CATEGORY_ID="DIC_kwDOQSei884C4Bnr" # Announcements - -# Post the discussion -RESULT=$(gh api graphql -f query=' - mutation($repoId: ID!, $categoryId: ID!, $title: String!, $body: String!) { - createDiscussion(input: { - repositoryId: $repoId - categoryId: $categoryId - title: $title - body: $body - }) { - discussion { - url - number - } - } - } -' -f repoId="$REPO_ID" -f categoryId="$CATEGORY_ID" -f title="$TITLE" -f body="$BODY") - -DISCUSSION_URL=$(echo "$RESULT" | jq -r '.data.createDiscussion.discussion.url') -DISCUSSION_NUM=$(echo "$RESULT" | jq -r '.data.createDiscussion.discussion.number') - -echo "Posted Discussion #${DISCUSSION_NUM}: ${TITLE}" -echo " ${DISCUSSION_URL}" diff --git a/.gh-velocity.yml b/.gh-velocity.yml new file mode 100644 index 00000000..8cc95841 --- /dev/null +++ b/.gh-velocity.yml @@ -0,0 +1,95 @@ +# gh-velocity configuration +# Generated by: gh velocity config preflight -R CalcMark/go-calcmark +# Date: 2026-03-21 + +# Scope: which issues/PRs to analyze (GitHub search syntax). +# Override per-run with: --scope 'label:"priority:high"' +scope: + query: 'repo:CalcMark/go-calcmark -label:"duplicate" -label:"invalid"' + # Excluded 2 noise label(s) detected in this repo: duplicate, invalid + +# Issue/PR classification — first matching category wins; unmatched = "other". +# Matchers: label:, type:, title://i +quality: + categories: + - name: bug + match: + - "type:Bug" # repo-configured issue type + - "label:bug" + - "title:/\\bfix(es|ed)?\\b/i" + - "title:/^fix[\\(: ]/i" + - "title:/^bug[\\(: ]/i" + - name: feature + match: + - "type:Feature" # repo-configured issue type + - "label:enhancement" + - "title:/^feat[\\(: ]/i" + - "title:/^add[\\(: ]/i" + - "title:/^feature[\\(: ]/i" + - name: chore + match: + - "type:Task" # repo-configured issue type + - "title:/^refactor[\\(: ]/i" + - "title:/^chore[\\(: ]/i" + - "title:/^ci[\\(: ]/i" + - "title:/^build[\\(: ]/i" + - name: docs + match: + - "label:documentation" + - "title:/^docs?[\\(: ]/i" + hotfix_window_hours: 72 + +# Commit message scanning +commit_ref: + patterns: ["closes"] + +# Cycle time strategy: issue +cycle_time: + strategy: issue + +# Exclude bot accounts from metrics. +exclude_users: + - "dependabot[bot]" + +# Minimum seconds between GitHub search API calls. +# Prevents secondary (abuse) rate limits which trigger on burst traffic. +# Set to 0 to disable (not recommended for CI). +api_throttle_seconds: 2 + +# Lifecycle stages (label-based, no project board) +# match: patterns use classify.Matcher syntax for cycle time detection +lifecycle: + in-progress: + match: + - "label:in-progress" + +# Post bulk reports (--post) as Discussion posts. +# CI/Actions: requires 'discussions: write' in workflow permissions. +discussions: + category: Announcements + title: "Weekly Velocity: ({{.Date}})" # optional Go template + # repo: owner/other-repo # optional: post to a different repo + +# Effort: how effort is assigned to work items (shared by velocity and WIP). +effort: + strategy: count + +# WIP limits: warn when work-in-progress exceeds these effort-weighted thresholds. +# Based on 1 human items currently in progress +wip: + team_limit: 5.0 # total items across all assignees + person_limit: 5.0 # max items per individual assignee + # bots: additional bot accounts (case-insensitive exact match) + # bots: + # - "claude-assistant" + # - "openai-bot" + +# Velocity: effort completed per iteration. +velocity: + unit: issues + iteration: + strategy: fixed + fixed: + length: "14d" + anchor: "2026-03-16" + count: 3 diff --git a/.github/workflows/velocity-issue.yml b/.github/workflows/velocity-issue.yml new file mode 100644 index 00000000..9efe4848 --- /dev/null +++ b/.github/workflows/velocity-issue.yml @@ -0,0 +1,44 @@ +name: Issue Velocity Metrics + +on: + issues: + types: [closed] + workflow_dispatch: + inputs: + issue_number: + description: 'Issue number to annotate' + required: true + type: number + +permissions: + contents: read + issues: write + +jobs: + metrics: + name: Annotate Issue with Velocity Metrics + runs-on: ubuntu-latest + if: >- + github.event_name == 'workflow_dispatch' || + github.event.issue.state_reason == 'completed' + steps: + - uses: actions/checkout@v4 + + - run: gh extension install dvhthomas/gh-velocity + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Post issue metrics + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_VELOCITY_POST_LIVE: 'true' + EVENT_NAME: ${{ github.event_name }} + INPUT_NUMBER: ${{ inputs.issue_number }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: | + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + NUM="$INPUT_NUMBER" + else + NUM="$ISSUE_NUMBER" + fi + gh velocity issue "$NUM" --post diff --git a/.github/workflows/velocity-pr.yml b/.github/workflows/velocity-pr.yml new file mode 100644 index 00000000..1458734f --- /dev/null +++ b/.github/workflows/velocity-pr.yml @@ -0,0 +1,45 @@ +name: PR Velocity Metrics + +on: + pull_request: + types: [closed] + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to annotate' + required: true + type: number + +permissions: + contents: read + pull-requests: write + +jobs: + metrics: + name: Annotate PR with Velocity Metrics + runs-on: ubuntu-latest + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.pull_request.merged == true && + !contains(fromJSON('["dependabot[bot]","renovate[bot]"]'), github.actor)) + steps: + - uses: actions/checkout@v4 + + - run: gh extension install dvhthomas/gh-velocity + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Post PR metrics + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_VELOCITY_POST_LIVE: 'true' + EVENT_NAME: ${{ github.event_name }} + INPUT_NUMBER: ${{ inputs.pr_number }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + NUM="$INPUT_NUMBER" + else + NUM="$PR_NUMBER" + fi + gh velocity pr "$NUM" --post diff --git a/.github/workflows/velocity-release.yml b/.github/workflows/velocity-release.yml new file mode 100644 index 00000000..4ba7a089 --- /dev/null +++ b/.github/workflows/velocity-release.yml @@ -0,0 +1,44 @@ +name: Release Velocity Metrics + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: 'Release tag (e.g. v1.6.5)' + required: true + type: string + +permissions: + contents: read + issues: read + discussions: write + +jobs: + metrics: + name: Post Release Velocity Report + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - run: gh extension install dvhthomas/gh-velocity + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Post release quality report + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_VELOCITY_POST_LIVE: 'true' + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ inputs.tag }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + TAG="$INPUT_TAG" + else + TAG="$RELEASE_TAG" + fi + gh velocity quality release "$TAG" --post diff --git a/.github/workflows/velocity-weekly.yml b/.github/workflows/velocity-weekly.yml new file mode 100644 index 00000000..b91ddcb2 --- /dev/null +++ b/.github/workflows/velocity-weekly.yml @@ -0,0 +1,28 @@ +name: Weekly Velocity Report + +on: + schedule: + - cron: '0 9 * * 1' # Monday 09:00 UTC + workflow_dispatch: + +permissions: + contents: read + issues: read + discussions: write + +jobs: + report: + name: Post Weekly Velocity Report + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - run: gh extension install dvhthomas/gh-velocity + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Post velocity report + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_VELOCITY_POST_LIVE: 'true' + run: gh velocity report --since 7d --post diff --git a/AGENTS.md b/AGENTS.md index 14d11025..86c4b4de 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ You are an expert language designer and implementer for the go-calcmark language - The spec can **never** depend on the implementation. - Golden examples in ./testdata are used both as valid and invalid grammar, semantic analysis, and runtime behavior. They are a great way to get oriented as to what the Calcmark language supports and does not support. - Manage GitHub issue lifecycle using the `github-project` skill — covers issue discovery, status transitions, local vs. PR workflows, and completion metrics. - - Use `.claude/skills/github-project/scripts/issue-summary.sh` to print metrics when handing work back to the human. + - Use `gh velocity issue -r pretty` to print metrics when handing work back to the human. ## Quality diff --git a/docs/plans/2026-03-21-001-feat-gh-velocity-metrics-integration-plan.md b/docs/plans/2026-03-21-001-feat-gh-velocity-metrics-integration-plan.md new file mode 100644 index 00000000..c92b07a2 --- /dev/null +++ b/docs/plans/2026-03-21-001-feat-gh-velocity-metrics-integration-plan.md @@ -0,0 +1,179 @@ +--- +title: "feat: Integrate gh-velocity for automated project metrics" +type: feat +status: completed +date: 2026-03-21 +--- + +# feat: Integrate gh-velocity for automated project metrics + +## Overview + +Replace custom velocity bash scripts with the `gh-velocity` GitHub CLI extension. Add three GitHub Actions workflows: weekly Discussion metrics, PR merge metrics insertion, and issue close metrics insertion. Update the `/release` command to use `gh-velocity` instead of `release-velocity.sh`. + +## Problem Statement / Motivation + +The current velocity system is six custom bash scripts (release-velocity.sh, issue-summary.sh, cycle-time.sh, lead-time.sh, pr-metrics.sh, helpers.sh) that only fire at release time. This means: + +- No ongoing visibility into project health between releases +- No metrics on individual PRs or issues when they close +- The scripts duplicate what `gh-velocity` provides out of the box with richer statistical analysis (P90, P95, outlier detection, quality ratios) +- Maintaining custom date-parsing and GraphQL logic is unnecessary overhead + +## Proposed Solution + +### 1. Install and configure gh-velocity + +Run `gh velocity config preflight --write` to auto-generate `.gh-velocity.yml` at the repository root. The preflight command examines existing labels, project boards, and recent activity to produce an optimal configuration. + +### 2. Three new GitHub Actions workflows + +**a) `.github/workflows/velocity-weekly.yml`** — Weekly Discussion post (Monday 09:00 UTC) +- Runs `gh velocity report --since 7d --post -r markdown` +- Posts to Announcements Discussion category +- Includes `workflow_dispatch` for manual testing +- Uses `--post` for idempotent updates (keyed on date range, won't duplicate) + +**b) `.github/workflows/velocity-pr.yml`** — PR merge metrics +- Triggers on `pull_request: types: [closed]` +- Guards with `if: github.event.pull_request.merged == true` +- Runs `gh velocity pr $PR_NUMBER -r markdown` +- Appends metrics to PR body wrapped in `` / `` sentinel markers +- Replaces existing block on re-run (idempotent) +- Skips bot PRs (check `github.actor` against known bots) + +**c) `.github/workflows/velocity-issue.yml`** — Issue close metrics +- Triggers on `issues: types: [closed]` +- Guards with `if: github.event.issue.state_reason == 'completed'` (skips "not planned") +- Runs `gh velocity issue $ISSUE_NUMBER -r markdown` +- Appends metrics to issue body with same sentinel marker pattern +- Idempotent on re-run + +### 3. Update `/release` command + +Replace Step 9 in `.claude/commands/release.md` to call `gh velocity quality release vX.Y.Z --post -r markdown` instead of `release-velocity.sh`. + +### 4. Remove legacy velocity scripts + +All six custom bash scripts were deleted. Agents now use `gh velocity issue -r pretty` for local metrics (available via the `gh-velocity` CLI extension). The `github-project` skill, AGENTS.md, and CLAUDE.md were updated accordingly. + +## Technical Considerations + +### Permissions + +| Workflow | Permissions needed | +|----------|--------------------| +| Weekly report | `contents: read`, `discussions: write` | +| PR merge | `contents: read`, `pull-requests: write` | +| Issue close | `contents: read`, `issues: write` | + +The default `GITHUB_TOKEN` covers all these scopes. A `GH_VELOCITY_TOKEN` PAT is only needed if using Projects v2 board data for velocity iterations — not required for the core metrics (lead time, cycle time, throughput, quality). + +### Idempotency + +PR and issue body edits use HTML comment sentinels: +``` + +...metrics markdown... + +``` + +The workflow checks for existing markers and replaces the block rather than appending. This handles workflow re-runs gracefully. + +### Race condition: PR merge closing an issue + +When a PR closes an issue via "Closes #N", both workflows fire concurrently. This is safe because they edit **different resources** (PR body vs issue body). No lock needed. + +### gh-velocity installation in CI + +Each workflow installs the extension fresh: +```yaml +- run: gh extension install dvhthomas/gh-velocity + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +Installation takes ~3 seconds — acceptable for event-driven workflows. Pin to a version tag when one is available. + +### Rate limits + +`gh velocity pr` or `gh velocity issue` costs 1-3 API calls — well within GitHub's 1000 requests/hour for Actions. The weekly report may cost 5-50 calls depending on volume. The `api_throttle_seconds` config option is available if needed. + +### Bot exclusion + +```yaml +exclude_users: + - "dependabot[bot]" + - "renovate[bot]" +``` + +This keeps automated dependency PRs from skewing velocity metrics. + +## System-Wide Impact + +- **Interaction graph**: PR merge → velocity-pr workflow → edits PR body. Issue close → velocity-issue workflow → edits issue body. Weekly cron → velocity-weekly workflow → creates/updates Discussion. Release tag push → release.yml → GoReleaser (unchanged). The `/release` command's Step 9 now calls `gh velocity` instead of `release-velocity.sh`. +- **Error propagation**: Workflow failures are isolated — a failed metrics insertion does not affect merges, releases, or other workflows. Non-zero exit from `gh velocity` fails the workflow step but the PR/issue is already closed. +- **State lifecycle risks**: No persistent state. Metrics are computed on-demand from GitHub's event timeline. Sentinel markers in PR/issue bodies are the only state, and they're idempotent. +- **API surface parity**: Both local agent use and CI automation now use `gh-velocity` commands. Single system, consistent behavior. + +## Acceptance Criteria + +- [ ] `.gh-velocity.yml` exists at repo root with quality categories matching existing labels +- [ ] `velocity-weekly.yml` workflow posts a Discussion every Monday at 09:00 UTC with 7-day metrics +- [ ] `velocity-weekly.yml` supports `workflow_dispatch` for manual testing +- [ ] `velocity-pr.yml` workflow appends metrics to PR body on merge (not on close-without-merge) +- [ ] `velocity-issue.yml` workflow appends metrics to issue body on close-as-completed (not "not planned") +- [ ] PR/issue body edits are idempotent (sentinel markers, no duplication on re-run) +- [ ] Bot PRs are excluded from metrics (dependabot, renovate) +- [ ] `/release` command Step 9 calls `gh velocity` instead of `release-velocity.sh` +- [ ] Existing local scripts remain functional (no breaking changes to `github-project` skill) +- [ ] All workflows have `workflow_dispatch` triggers for testing + +## Success Metrics + +- Weekly Discussion posts appear consistently every Monday +- Every merged PR has velocity metrics in its body within 2 minutes of merge +- Every completed issue has velocity metrics in its body within 2 minutes of close +- Release velocity Discussions continue to post (now via `gh velocity quality release`) + +## Dependencies & Risks + +| Risk | Mitigation | +|------|------------| +| `gh-velocity` extension unavailable or breaking change | Pin version; existing scripts remain as fallback | +| `GITHUB_TOKEN` insufficient for Discussions | Test with `workflow_dispatch` before relying on cron | +| Rate limiting on busy days (many concurrent merges) | `api_throttle_seconds: 2` in config | +| `gh velocity` output format changes | Sentinel markers isolate metrics block; format changes are cosmetic only | + +## Implementation Phases + +### Phase 1: Configuration and weekly report +1. Create `.gh-velocity.yml` with quality categories, bot exclusions +2. Create `.github/workflows/velocity-weekly.yml` +3. Test with `workflow_dispatch` + +### Phase 2: PR and issue metrics +4. Create `.github/workflows/velocity-pr.yml` with sentinel-based body editing +5. Create `.github/workflows/velocity-issue.yml` with state_reason guard +6. Test both with manual workflow runs + +### Phase 3: Release integration +7. Update `.claude/commands/release.md` Step 9 to use `gh velocity quality release` +8. Test with a dry-run release + +## Files to Create/Modify + +| Action | File | +|--------|------| +| Create | `.gh-velocity.yml` | +| Create | `.github/workflows/velocity-weekly.yml` | +| Create | `.github/workflows/velocity-pr.yml` | +| Create | `.github/workflows/velocity-issue.yml` | +| Modify | `.claude/commands/release.md` (Step 9) | + +## Sources & References + +- gh-velocity documentation: https://dvhthomas.github.io/gh-velocity/ +- Existing velocity scripts: `.claude/skills/github-project/scripts/release-velocity.sh` +- Existing release command: `.claude/commands/release.md` (lines 153-161) +- Existing workflows: `.github/workflows/release.yml`