diff --git a/.ci/scripts/README.md b/.ci/scripts/README.md index 72fc4bd4af..c8c86698a0 100644 --- a/.ci/scripts/README.md +++ b/.ci/scripts/README.md @@ -6,7 +6,9 @@ This directory contains scripts for measuring and analyzing CI performance metri ### measure-ci-baseline.sh -Collects workflow run data from GitHub Actions and calculates baseline metrics. +Collects workflow run data from GitHub Actions and calculates baseline metrics via +`cargo xtask ci-baseline` (the underlying implementation), with this wrapper +script retained as a convenience entrypoint. **Features:** - Fetches workflow runs from specified branch @@ -19,41 +21,49 @@ Collects workflow run data from GitHub Actions and calculates baseline metrics. **Prerequisites:** - [GitHub CLI (gh)](https://cli.github.com/) - installed and authenticated -- [jq](https://stedolan.github.io/jq/) - for JSON processing +- Rust toolchain - for the workspace xtask binary +- `cargo` - comes with Rust, used to run `cargo xtask` **Installation of prerequisites:** ```bash # macOS -brew install gh jq +brew install gh # Ubuntu/Debian -sudo apt install gh jq +sudo apt install gh -# Then authenticate +# Authenticate once for CLI queries gh auth login ``` -**Usage:** +**Usage (canonical):** ```bash # Basic usage (analyzes master branch, last 30 days) -./measure-ci-baseline.sh +cargo xtask ci-baseline # Analyze a different branch -./measure-ci-baseline.sh --branch main +cargo xtask ci-baseline --branch main # Analyze last 7 days only -./measure-ci-baseline.sh --days 7 +cargo xtask ci-baseline --days 7 # Fetch more runs for higher accuracy -./measure-ci-baseline.sh --limit 500 +cargo xtask ci-baseline --limit 500 # Custom output directory -./measure-ci-baseline.sh --output ./reports +cargo xtask ci-baseline --output ./reports # All options -./measure-ci-baseline.sh --branch master --days 30 --limit 200 --output .ci +cargo xtask ci-baseline --branch master --days 30 --limit 200 --output .ci +``` + +**Usage (wrapper shim):** + +```bash +# Run the shim script (delegates to `cargo xtask ci-baseline`) +./measure-ci-baseline.sh ``` **Options:** @@ -111,10 +121,10 @@ gh auth login ```bash # Run before making CI changes -./measure-ci-baseline.sh --output .ci/before +cargo xtask ci-baseline --output .ci/before # After changes -./measure-ci-baseline.sh --output .ci/after +cargo xtask ci-baseline --output .ci/after # Compare the JSON files to measure improvement ``` @@ -130,8 +140,8 @@ gh auth login ```bash # Compare feature branch to main -./measure-ci-baseline.sh --branch main --output .ci/main-baseline -./measure-ci-baseline.sh --branch feature-x --output .ci/feature-baseline +cargo xtask ci-baseline --branch main --output .ci/main-baseline +cargo xtask ci-baseline --branch feature-x --output .ci/feature-baseline ``` ## Interpreting Results @@ -168,20 +178,6 @@ gh auth login # Follow the prompts to authenticate ``` -### "jq: command not found" - -Install jq for your platform: -```bash -# macOS -brew install jq - -# Ubuntu/Debian -sudo apt install jq - -# RHEL/CentOS -sudo yum install jq -``` - ### No workflow runs found - Verify the branch name exists diff --git a/.ci/scripts/measure-ci-baseline.sh b/.ci/scripts/measure-ci-baseline.sh index a79bb3e9fb..5fc382c55e 100755 --- a/.ci/scripts/measure-ci-baseline.sh +++ b/.ci/scripts/measure-ci-baseline.sh @@ -1,409 +1,14 @@ #!/usr/bin/env bash +# Canonical implementation: cargo xtask ci-baseline. # -# measure-ci-baseline.sh - Measure CI baseline metrics from GitHub Actions -# -# This script collects workflow run data from GitHub Actions and calculates -# baseline metrics including median duration, P95 duration, success rate, -# and approximate billable minutes. -# -# Requirements: -# - gh CLI installed and authenticated (gh auth login) -# - jq for JSON processing -# - bc for calculations (optional, falls back to awk) -# -# Usage: -# ./measure-ci-baseline.sh [options] -# -# Options: -# -b, --branch BRANCH Branch to analyze (default: master) -# -d, --days DAYS Number of days to look back (default: 30) -# -l, --limit LIMIT Max runs to fetch (default: 200) -# -o, --output DIR Output directory (default: .ci) -# -h, --help Show this help message -# -# Output: -# .ci/ci_baseline.json Machine-readable metrics -# .ci/ci_baseline.md Human-readable report +# Supported flags are passed through to: +# cargo xtask ci-baseline --branch BRANCH --days DAYS --limit LIMIT --output DIR set -euo pipefail -# Default configuration -BRANCH="master" -DAYS=30 -LIMIT=200 -OUTPUT_DIR=".ci" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -# Colors for output (disabled if not a terminal) -if [[ -t 1 ]]; then - RED='\033[0;31m' - GREEN='\033[0;32m' - YELLOW='\033[1;33m' - BLUE='\033[0;34m' - NC='\033[0m' # No Color -else - RED='' - GREEN='' - YELLOW='' - BLUE='' - NC='' -fi - -log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } -log_success() { echo -e "${GREEN}[OK]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } - -usage() { - head -30 "$0" | grep -E "^#" | sed 's/^# //' | sed 's/^#//' - exit 0 -} - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - -b|--branch) BRANCH="$2"; shift 2 ;; - -d|--days) DAYS="$2"; shift 2 ;; - -l|--limit) LIMIT="$2"; shift 2 ;; - -o|--output) OUTPUT_DIR="$2"; shift 2 ;; - -h|--help) usage ;; - *) log_error "Unknown option: $1"; usage ;; - esac -done - -# Check prerequisites -check_prerequisites() { - local missing=() - - if ! command -v gh &>/dev/null; then - missing+=("gh (GitHub CLI)") - fi - - if ! command -v jq &>/dev/null; then - missing+=("jq") - fi - - if [[ ${#missing[@]} -gt 0 ]]; then - log_error "Missing required tools: ${missing[*]}" - echo "" - echo "Installation instructions:" - echo " gh: https://cli.github.com/manual/installation" - echo " jq: https://stedolan.github.io/jq/download/" - exit 1 - fi - - # Check gh authentication - if ! gh auth status &>/dev/null; then - log_error "GitHub CLI is not authenticated" - echo "" - echo "Please run: gh auth login" - exit 1 - fi - - log_success "Prerequisites satisfied" -} - -# Calculate percentile from sorted array (passed as newline-separated values) -calculate_percentile() { - local percentile=$1 - local values - mapfile -t values < <(sort -n) - local count=${#values[@]} - - if [[ $count -eq 0 ]]; then - echo "0" - return - fi - - local index - index=$(awk "BEGIN {printf \"%d\", ($count - 1) * $percentile / 100}") - echo "${values[$index]}" -} - -# Calculate median -calculate_median() { - calculate_percentile 50 -} - -# Calculate P95 -calculate_p95() { - calculate_percentile 95 -} - -# Convert ISO 8601 timestamp to epoch seconds -iso_to_epoch() { - local ts="$1" - # Handle both GNU and BSD date - if date --version &>/dev/null 2>&1; then - date -d "$ts" +%s 2>/dev/null || echo "0" - else - date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +%s 2>/dev/null || echo "0" - fi -} - -# Calculate duration between two ISO timestamps in seconds -calculate_duration() { - local start="$1" - local end="$2" - local start_epoch end_epoch - - start_epoch=$(iso_to_epoch "$start") - end_epoch=$(iso_to_epoch "$end") - - if [[ "$start_epoch" -eq 0 ]] || [[ "$end_epoch" -eq 0 ]]; then - echo "0" - return - fi - - echo $((end_epoch - start_epoch)) -} - -# Format seconds as human-readable duration -format_duration() { - local seconds=$1 - if [[ $seconds -lt 60 ]]; then - echo "${seconds}s" - elif [[ $seconds -lt 3600 ]]; then - echo "$((seconds / 60))m $((seconds % 60))s" - else - echo "$((seconds / 3600))h $((seconds % 3600 / 60))m" - fi -} - -# Main measurement function -measure_ci_baseline() { - local cutoff_date - cutoff_date=$(date -d "-${DAYS} days" +%Y-%m-%d 2>/dev/null || date -v-${DAYS}d +%Y-%m-%d) - - log_info "Fetching workflow runs from branch '$BRANCH' (last $DAYS days, limit $LIMIT)" - - # Fetch workflow runs - local runs_json - runs_json=$(gh run list --limit "$LIMIT" --branch "$BRANCH" \ - --json name,conclusion,createdAt,updatedAt,databaseId,workflowName,status) - - if [[ -z "$runs_json" ]] || [[ "$runs_json" == "[]" ]]; then - log_warn "No workflow runs found for branch '$BRANCH'" - return 1 - fi - - local run_count - run_count=$(echo "$runs_json" | jq 'length') - log_info "Found $run_count workflow runs" - - # Get unique workflow names - local workflows - mapfile -t workflows < <(echo "$runs_json" | jq -r '.[].workflowName' | sort -u) - - log_info "Analyzing ${#workflows[@]} unique workflows..." - - # Initialize results - local results='{"generated_at": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'", "branch": "'$BRANCH'", "days_analyzed": '$DAYS', "workflows": {}}' - - # Process each workflow - for workflow in "${workflows[@]}"; do - log_info " Processing: $workflow" - - # Filter runs for this workflow - local workflow_runs - workflow_runs=$(echo "$runs_json" | jq --arg wf "$workflow" '[.[] | select(.workflowName == $wf)]') - - local total_runs completed_runs success_count skipped_count failure_count - total_runs=$(echo "$workflow_runs" | jq 'length') - - # Count by conclusion - success_count=$(echo "$workflow_runs" | jq '[.[] | select(.conclusion == "success")] | length') - failure_count=$(echo "$workflow_runs" | jq '[.[] | select(.conclusion == "failure")] | length') - skipped_count=$(echo "$workflow_runs" | jq '[.[] | select(.conclusion == "skipped")] | length') - - # Only count non-skipped runs for duration analysis - completed_runs=$((success_count + failure_count)) - - # Calculate success rate (excluding skipped) - local success_rate="0" - if [[ $completed_runs -gt 0 ]]; then - success_rate=$(awk "BEGIN {printf \"%.1f\", $success_count * 100 / $completed_runs}") - fi - - # Calculate durations for completed runs - local durations=() - while IFS= read -r run; do - local created updated duration - created=$(echo "$run" | jq -r '.createdAt') - updated=$(echo "$run" | jq -r '.updatedAt') - conclusion=$(echo "$run" | jq -r '.conclusion') - - # Skip skipped runs - if [[ "$conclusion" == "skipped" ]]; then - continue - fi - - duration=$(calculate_duration "$created" "$updated") - if [[ $duration -gt 0 ]]; then - durations+=("$duration") - fi - done < <(echo "$workflow_runs" | jq -c '.[]') - - # Calculate statistics - local median_duration=0 p95_duration=0 avg_duration=0 - local total_duration=0 - - if [[ ${#durations[@]} -gt 0 ]]; then - # Calculate median - median_duration=$(printf '%s\n' "${durations[@]}" | calculate_median) - - # Calculate P95 - p95_duration=$(printf '%s\n' "${durations[@]}" | calculate_p95) - - # Calculate average - for d in "${durations[@]}"; do - total_duration=$((total_duration + d)) - done - avg_duration=$((total_duration / ${#durations[@]})) - fi - - # Estimate billable minutes (rounded up to nearest minute per run) - local billable_minutes=0 - for d in "${durations[@]}"; do - billable_minutes=$((billable_minutes + (d + 59) / 60)) - done - - # Add workflow results - local workflow_key - workflow_key=$(echo "$workflow" | tr ' ' '_' | tr -cd '[:alnum:]_-') - - results=$(echo "$results" | jq --arg key "$workflow_key" \ - --arg name "$workflow" \ - --argjson total "$total_runs" \ - --argjson completed "$completed_runs" \ - --argjson success "$success_count" \ - --argjson failure "$failure_count" \ - --argjson skipped "$skipped_count" \ - --arg rate "$success_rate" \ - --argjson median "$median_duration" \ - --argjson p95 "$p95_duration" \ - --argjson avg "$avg_duration" \ - --argjson billable "$billable_minutes" \ - '.workflows[$key] = { - "name": $name, - "total_runs": $total, - "completed_runs": $completed, - "success_count": $success, - "failure_count": $failure, - "skipped_count": $skipped, - "success_rate_percent": ($rate | tonumber), - "median_duration_seconds": $median, - "p95_duration_seconds": $p95, - "avg_duration_seconds": $avg, - "billable_minutes": $billable - }') - done - - # Calculate totals - local total_billable total_runs total_success - total_billable=$(echo "$results" | jq '[.workflows[].billable_minutes] | add // 0') - total_runs=$(echo "$results" | jq '[.workflows[].total_runs] | add // 0') - total_success=$(echo "$results" | jq '[.workflows[].success_count] | add // 0') - total_completed=$(echo "$results" | jq '[.workflows[].completed_runs] | add // 0') - - local overall_success_rate="0" - if [[ $total_completed -gt 0 ]]; then - overall_success_rate=$(awk "BEGIN {printf \"%.1f\", $total_success * 100 / $total_completed}") - fi - - results=$(echo "$results" | jq \ - --argjson total_runs "$total_runs" \ - --argjson total_billable "$total_billable" \ - --arg overall_rate "$overall_success_rate" \ - '. + { - "summary": { - "total_runs": $total_runs, - "total_billable_minutes": $total_billable, - "overall_success_rate_percent": ($overall_rate | tonumber) - } - }') - - # Create output directory - mkdir -p "$OUTPUT_DIR" - - # Write JSON output - echo "$results" | jq '.' > "$OUTPUT_DIR/ci_baseline.json" - log_success "Written: $OUTPUT_DIR/ci_baseline.json" - - # Generate markdown report - generate_markdown_report "$results" "$OUTPUT_DIR/ci_baseline.md" - log_success "Written: $OUTPUT_DIR/ci_baseline.md" - - # Print summary - echo "" - echo "======================================" - echo "CI Baseline Summary" - echo "======================================" - echo "Branch: $BRANCH" - echo "Analysis period: Last $DAYS days" - echo "Total runs: $total_runs" - echo "Total billable: ${total_billable}m" - echo "Overall success: ${overall_success_rate}%" - echo "======================================" -} - -generate_markdown_report() { - local results="$1" - local output_file="$2" - - cat > "$output_file" << HEADER -# CI Baseline Metrics Report - -**Generated:** $(date -u +"%Y-%m-%d %H:%M:%S UTC") -**Branch:** $(echo "$results" | jq -r '.branch') -**Analysis Period:** Last $(echo "$results" | jq -r '.days_analyzed') days - -## Summary - -| Metric | Value | -|--------|-------| -| Total Runs | $(echo "$results" | jq -r '.summary.total_runs') | -| Overall Success Rate | $(echo "$results" | jq -r '.summary.overall_success_rate_percent')% | -| Total Billable Minutes | $(echo "$results" | jq -r '.summary.total_billable_minutes')m | - -## Workflow Details - -| Workflow | Runs | Success Rate | Median | P95 | Billable | -|----------|------|--------------|--------|-----|----------| -HEADER - - # Add each workflow - echo "$results" | jq -r '.workflows | to_entries[] | - "| \(.value.name) | \(.value.completed_runs) | \(.value.success_rate_percent)% | \(.value.median_duration_seconds)s | \(.value.p95_duration_seconds)s | \(.value.billable_minutes)m |"' >> "$output_file" - - cat >> "$output_file" << 'FOOTER' - -## Notes - -- **Median Duration:** 50th percentile of run duration (in seconds) -- **P95 Duration:** 95th percentile of run duration (in seconds) -- **Billable Minutes:** Estimated billable time (each run rounded up to nearest minute) -- **Success Rate:** Calculated excluding skipped runs - -## Recommendations - -Based on this baseline: - -1. **Monitor P95 durations** - Workflows with high P95 vs median ratio may have reliability issues -2. **Success rate targets** - Aim for >95% success rate on critical workflows -3. **Cost optimization** - Focus on high-billable workflows for optimization efforts - ---- -*Generated by measure-ci-baseline.sh* -FOOTER -} - -# Entry point -main() { - cd "$REPO_ROOT" - check_prerequisites - measure_ci_baseline -} +cd "$REPO_ROOT" -main "$@" +exec cargo xtask ci-baseline "$@" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ba3ee2dbc..8af2f4fd4b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,6 +95,28 @@ bash scripts/install-githooks.sh 1. Push your branch and open a PR 2. Describe your changes and link related issues (e.g., "Fixes #123") 3. All PRs run format checks, clippy, and tests automatically in CI +4. Merge with a conventional, descriptive squash commit + +Once checks are green and reviews are complete, use an explicit conventional commit +subject when squashing so history is release-friendly and changelog-friendly. + +```bash +pr= +gh pr merge "$pr" --squash \ + --subject "feat(lsp): ... " \ + --body "PR summary: +- ... +``` + +Conventional subject format: + +- `feat(scope): imperative summary` +- `fix(scope): imperative summary` +- `chore(scope): imperative summary` +- include `!` for breaking changes, e.g. `feat!: ...` + +Do not rely on PR title defaults (often noisy, e.g. `(...#NNNN)`), because they +break commit consistency for changelog generation. #### CI Labels (Opt-in) @@ -118,6 +140,8 @@ For full CI details, see [CI & Automation](docs/project/CI.md). - Run `cargo fmt --all` before every commit - Fix all `cargo clippy --workspace` warnings - Use [conventional commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, etc. +- For squash merges, prefer scope-qualified forms such as + `feat(lsp): ...`, `fix(dap): ...`, `chore(release): ...`, etc. ### Banned in Production Code diff --git a/Cargo.toml b/Cargo.toml index 32700d79dd..d99977f6dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -442,7 +442,6 @@ perl-parser-pest = { path = "crates/perl-parser-pest", version = "0.12.0" } # External dependencies tree-sitter-perl = { path = "crates/tree-sitter-perl-rs", features = ["pure-rust"] } -tree-sitter-perl-c = { path = "crates/tree-sitter-perl-c" } tree-sitter = "0.26.6" ropey = "1.6.1" serde = { version = "1.0.228", features = ["derive"] } diff --git a/docs/CHANGELOG_WORKFLOW.md b/docs/CHANGELOG_WORKFLOW.md index f1a2621aa1..c1ff49c1ea 100644 --- a/docs/CHANGELOG_WORKFLOW.md +++ b/docs/CHANGELOG_WORKFLOW.md @@ -182,7 +182,7 @@ The changelog is automatically generated during releases via the release orchest ```bash # Recommended: run both workflow steps through gh automation. # Canonical command for RC orchestration: -./scripts/release-turnkey-pr.sh <0.x.y> +cargo xtask release-turnkey <0.x.y> ``` ### Alternative Manual Release Process @@ -190,7 +190,7 @@ The changelog is automatically generated during releases via the release orchest ```bash # 1. Generate changelog content # Use the same canonical flow entrypoint: -./scripts/release-turnkey-pr.sh <0.x.y> --no-auto-merge --no-wait-release +cargo xtask release-turnkey <0.x.y> --no-auto-merge --no-wait-release # 2. Manually review and merge the generated version bump PR. @@ -286,13 +286,19 @@ Every commit should follow the conventional commit format. This ensures: - Proper categorization - Automatic semantic versioning -### 3. Group Related Changes +### 3. Keep Squash Merges Explicitly Conventional -Use PR merges with conventional commit messages: +Use explicit subject/body on squash merges so merged history stays descriptive: ```bash -# Instead of multiple small commits, use PR title -feat(parser): add comprehensive heredoc support (#123) +pr=2943 +gh pr merge "$pr" --squash \ + --subject "feat(lsp): improve type definition and implementation with OO inheritance support" \ + --body "PR summary: +- improve OO lookup fallback for type definitions and implementations +- add coverage for inherited methods and signatures +- preserve existing behavior for non-OO dispatch paths" + --delete-branch ``` ### 4. Document Breaking Changes diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index 9b4b473648..aa1fc85c0e 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -82,7 +82,7 @@ After publish completes: To run the entire path from PR creation through publish dispatch: ```bash -scripts/release-turnkey-pr.sh <0.x.y> +cargo xtask release-turnkey <0.x.y> ``` Use `--skip-crates` to run validation and release without crates.io publishing when needed. diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index de17b1df86..b665c9ed55 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -83,7 +83,7 @@ git checkout master git reset --hard origin/master # optional but recommended: use the turnkey orchestrator -scripts/release-turnkey-pr.sh <0.x.y> +cargo xtask release-turnkey <0.x.y> ``` Or run the two workflow dispatches manually with `gh`: diff --git a/docs/handoff/swarm-pack/commands/green-merge.md b/docs/handoff/swarm-pack/commands/green-merge.md index 4c506d6794..8f0ea5e8cf 100644 --- a/docs/handoff/swarm-pack/commands/green-merge.md +++ b/docs/handoff/swarm-pack/commands/green-merge.md @@ -23,10 +23,41 @@ gh pr list --state open --json number,title,headRefName,mergeable,statusCheckRol ### 3. Merge green PRs sequentially For each green PR, in dependency order: ```bash -gh pr merge --squash --delete-branch +pr= +gh pr merge "$pr" --squash \ + --subject "feat(scope): " \ + --body "Merge PR #$pr + +- +- " ``` Wait for each merge to complete before the next. Sequential merging prevents race conditions. +Rules for consistency: +- Use a conventional prefix: `feat|fix|chore|refactor|docs` plus a single scope in parentheses. +- Keep the subject imperative and <=72 characters if possible. +- Make the summary specific to behavior impact, not generic PR intent. +- Put bullet list details in the body so reviewers can audit intent after squash. + +Example commit subject patterns: +- `feat(perl-lsp): add linked-editing capability wiring` +- `fix(parser): recover nested here-doc parsing` +- `chore(release): add release-turnkey xtask wrapper` +- `refactor(semantic): simplify call-hierarchy parent map` + +If the PR title is not conventional, convert it before merge (do not keep `Merge pull request` defaults). + +Optional: derive a clearer subject from files changed: +- `scope=lsp` if files are in `crates/perl-lsp/**` +- `scope=dap` if files are in `crates/perl-dap/**` +- `scope=parser` for `crates/perl-parser*`/`crates/perl-lexer*`/`crates/perl-parser-core*` +- `scope=semantic` for `crates/perl-semantic-analyzer/**`/`crates/perl-workspace-index/**` +- `scope=release` for `scripts/**`, `xtask/**`, `docs/**`, `.github/**` +Examples used in this repo: +- `feat(lsp): ...` +- `fix(parser): ...` +- `chore(release): ...` + ### 4. Handle post-merge drift After all merges complete: ```bash diff --git a/docs/project/CI_COST_TRACKING.md b/docs/project/CI_COST_TRACKING.md index 93ac9d520e..5a205232da 100644 --- a/docs/project/CI_COST_TRACKING.md +++ b/docs/project/CI_COST_TRACKING.md @@ -334,42 +334,32 @@ jobs: ## Monitoring -### Using `scripts/ci-cost-monitor.sh` +### Monitoring CI cost -**Note**: This script doesn't exist yet. Below is the specification for creating it. +Run the XTASK implementation directly: ```bash -#!/bin/bash -# scripts/ci-cost-monitor.sh -# Estimates CI costs from GitHub Actions API +cargo xtask ci-cost-monitor +``` -# Usage: -# bash scripts/ci-cost-monitor.sh [--month YYYY-MM] [--repo owner/name] +If you prefer using the shim script, it delegates to the same command: -# Fetch workflow runs for the month -# Calculate total minutes per runner type -# Multiply by pricing -# Output cost breakdown and trends +```bash +# Via shim +bash scripts/ci-cost-monitor.sh ``` -**Planned implementation** (Issue #211 Phase 3): +Examples: ```bash -# Show current month costs -bash scripts/ci-cost-monitor.sh +# Show costs for the last 30 days +cargo xtask ci-cost-monitor + +# Show JSON output for CI automation +cargo xtask ci-cost-monitor --json -# Output: -# CI Cost Report (2025-01) -# ======================== -# Linux: 127 min × $0.008 = $1.02 -# Windows: 45 min × $0.016 = $0.72 -# macOS: 0 min × $0.080 = $0.00 -# ──────────────────────────────────── -# Total: $1.74 -# -# PRs: 18 -# Avg/PR: $0.097 -# Trend: ↓ 12% vs last month +# Show costs for the last 7 days +cargo xtask ci-cost-monitor --days 7 ``` ### Reading GitHub Billing Reports diff --git a/docs/project/CI_TEST_LANES.md b/docs/project/CI_TEST_LANES.md index 0991225651..b418248dff 100644 --- a/docs/project/CI_TEST_LANES.md +++ b/docs/project/CI_TEST_LANES.md @@ -37,7 +37,7 @@ nix develop -c just ci-gate 1. `cargo fmt --check --all` - Format check 2. `cargo clippy --workspace --lib --locked -- -D warnings -A missing_docs` - Lint 3. `cargo test --workspace --lib --locked` - Library tests -4. `.ci/scripts/check-from-raw.sh` - Policy checks +4. `cargo xtask check-from-raw` - Policy checks 5. LSP semantic definition tests ### What `just ci-full` Adds diff --git a/docs/project/PUBLISHING_ROADMAP.md b/docs/project/PUBLISHING_ROADMAP.md index b0121b7d5f..24c89badb5 100644 --- a/docs/project/PUBLISHING_ROADMAP.md +++ b/docs/project/PUBLISHING_ROADMAP.md @@ -33,7 +33,7 @@ grep '^version' features.toml | head -1 # Expected: version = "NEW_VERSION" # Automated check (must print nothing) -bash scripts/check-version-sync.sh +cargo xtask check-version-sync ``` Fail if any string mismatches. Fix with: @@ -636,7 +636,7 @@ gh workflow run release-orchestration.yml \ | `CHANGELOG.md` | Promote `[Unreleased]` to `[NEW_VERSION] - DATE` | Manual edit | | `docs/project/status/index.md` | Release posture narrative | Manual edit after ship | -The `scripts/check-version-sync.sh` script validates all of the above except `CHANGELOG.md`. +`cargo xtask check-version-sync` validates all of the above except `CHANGELOG.md`. --- diff --git a/docs/project/RELEASE_CHECKLIST.md b/docs/project/RELEASE_CHECKLIST.md index dd68a8daba..e56ba1bbe8 100644 --- a/docs/project/RELEASE_CHECKLIST.md +++ b/docs/project/RELEASE_CHECKLIST.md @@ -47,7 +47,7 @@ All merge-blocking gates must pass: - [ ] All version strings agree across `Cargo.toml` (workspace), `features.toml`, `package.json` (VSCode extension), and any `build.rs` references -- [ ] Verified by `scripts/check-version-sync.sh` +- [ ] Verified by `just version-check` ### 4. SemVer Check **[automated]** diff --git a/docs/project/XTASK_MIGRATION.md b/docs/project/XTASK_MIGRATION.md index c5c7e23a51..b4a7f44c39 100644 --- a/docs/project/XTASK_MIGRATION.md +++ b/docs/project/XTASK_MIGRATION.md @@ -15,22 +15,59 @@ xtask subcommands (`cargo xtask `). | Subcommand | Module | Lines | Purpose | |------------|--------|------:|---------| +| `ci-audit-workflows` | `ci_audit_workflows.rs` | 150 | Audit PR workflows for ungated non-trivial jobs | +| `ci-baseline` | `ci_metrics.rs` | -- | Measure CI baseline from recent workflow runs | +| `ci-cost-monitor` | `ci_metrics.rs` | -- | Analyze GitHub Actions spend over a period | | `ci` | `ci.rs` | 117 | Lean CI suite (format + clippy + tests) | | `check-only` | `ci.rs` | -- | Format and clippy checks only | +| `ci-measure` | `ci_measure.rs` | -- | Measure CI lane runtimes and emit timing artifacts | +| `targeted-checks` | `targeted_checks.rs` | 319 | Target changed crates and run fast checks | | `build` | `build.rs` | 69 | Build with configurable features/mode | | `test` | `test.rs` | 148 | Run tests with suite/coverage options | | `bench` | `bench.rs` | 355 | Run benchmarks | +| `bench-alert` | `benchmarks.rs` | -- | Run benchmark alert checks | +| `bench-alert-test` | `benchmarks.rs` | -- | Run benchmark alert regression test suite | +| `bench-extract` | `benchmarks.rs` | -- | Extract and normalize criterion outputs | +| `inject-sha-assets` | `inject_sha_assets.rs` | 206 | Generate Homebrew formula and VS Code asset map | +| `update-homebrew` | `update_homebrew.rs` | 282 | Generate Homebrew formula from release SHA256SUMS | +| `bench-compare` | `benchmarks.rs` | -- | Compare benchmark artifacts against baseline | +| `bench-format` | `benchmarks.rs` | -- | Format benchmark JSON output | +| `bench-run` | `benchmarks.rs` | -- | Wrapper for benchmark runner script | | `compare` | `compare.rs` | 1174 | C vs Rust benchmark comparison | +| `build-timing-receipt` | `build_timing.rs` | -- | Collect build timing receipts and measurement baselines | +| `compare-build-timing` | `build_timing.rs` | -- | Compare build timing receipts with markdown report | | `doc` | `doc.rs` | 35 | Generate documentation | | `check` | `check.rs` | 58 | Code quality checks (clippy, fmt) | | `fmt` | `fmt.rs` | 43 | Format code | | `clean` | `clean.rs` | 47 | Clean build artifacts | | `dev` | `dev.rs` | 178 | Development server with watch | | `parse-rust` | `parse_rust.rs` | 54 | Run pure Rust parser on a file | +| `parser-matrix` | `parser_matrix.rs` | 338 | Generate `docs/reference/PARSER_FEATURE_MATRIX.md` from parser audit report | +| `validate-workspace-exclusions` | `validate_workspace_exclusions.rs` | -- | Validate workspace exclusion strategy and dependency invariants | | `release` | `release.rs` | 223 | Prepare a release | +| `security-hardening` | `hardening.rs` | -- | Production security hardening checks | +| `release-turnkey` | `release_turnkey.rs` | -- | PR-driven release orchestration | +| `performance-hardening` | `hardening.rs` | -- | Production performance hardening checks | | `bump-version` | `bump_version.rs` | 184 | Bump version numbers across project | | `publish-crates` | `publish.rs` | 203 | Publish crates to crates.io | +| `forbid-fatal-constructs` | `forbid_fatal_constructs.rs` | -- | Run forbidden fatal construct checks via perl-ci-hygiene | +| `forensics-dossier` | `forensics.rs` | -- | Generate complete PR dossier artifacts | +| `forensics-harvest` | `forensics.rs` | -- | Harvest PR forensics metadata | +| `forensics-render` | `forensics.rs` | -- | Render PR dossiers from existing YAML | +| `forensics-telemetry-full` | `forensics.rs` | -- | Run full PR telemetry pipeline | +| `forensics-telemetry-quick` | `forensics.rs` | -- | Run quick PR telemetry pipeline | +| `forensics-temporal` | `forensics.rs` | -- | Analyze PR temporal topology | +| `gh-backfill-prefixed-labels` | `github.rs` | -- | Backfill legacy issue labels into prefixed taxonomy | +| `gh-labels` | `github.rs` | -- | Ensure GitHub label taxonomy is present | +| `gh-triage` | `github.rs` | -- | Show issues missing required label taxonomy | +| `ci-hygiene` | `ci_hygiene.rs` | -- | Pass-through to `perl-ci-hygiene` subcommands | +| `worktree-cleanup` | `worktrees.rs` | 71 | Remove stale `.claude/worktrees` entries | | `publish-vscode` | `publish.rs` | -- | Publish VSCode extension | +| `populate-book` | `populate_book.rs` | -- | Populate mdBook source directory from docs | +| `e2e-validate` | `e2e_validate.rs` | -- | Run end-to-end validation suite | +| `verify-publication-facts` | `publication_facts.rs` | -- | Verify PUBLICATION_FACTS_LEDGER metrics | +| `publish-receipts` | `publish_receipts.rs` | -- | Publish phase-0 receipt bundle for review | +| `prep-crates-io-launch` | `prep_crates_io_launch.rs` | -- | Launch preflight checks (`core` / `all`) | | `test-heredoc` | (delegates to `test.rs`) | -- | Heredoc-specific tests | | `test-edge-cases` | `edge_cases.rs` | 110 | Edge case test suite | | `corpus-audit` | `corpus_audit.rs` | 325 | Corpus coverage analysis | @@ -43,6 +80,7 @@ xtask subcommands (`cargo xtask `). | `srp-microcrates` | `srp_microcrates.rs` | 194 | SRP microcrate inventory | | `validate-memory-profiler` | `compare.rs` | -- | Memory profiling validation | | `gates` | `gates.rs` | 1370 | CI gates with receipt generation | +| `production-gates-validation` | `hardening.rs` | -- | Validate production gates and SLO posture | | `corpus` | `corpus.rs` | 625 | Corpus tests (legacy feature) | | `highlight` | `highlight.rs` | 272 | Highlight tests (parser-tasks feature) | | `bindings` | `bindings.rs` | 49 | Generate bindings (parser-tasks feature) | @@ -53,61 +91,63 @@ xtask subcommands (`cargo xtask `). | Script | Lines | Xtask Equivalent | Status | Priority | Notes | |--------|------:|-------------------|--------|----------|-------| -| `dead-code-check.sh` | 457 | -- | **Not Started** | High | Called from `just dead-code*`; complex logic with baseline comparison | +| `dead-code-check.sh` | 457 | `cargo xtask dead-code` | **Replaced** | Low | Compatibility shim for `cargo xtask dead-code` | | `execute-gate.sh` | 112 | `cargo xtask gates` | **Replaced** | -- | Xtask `gates` subsumes single-gate execution with receipts | | `run-gates.sh` | 165 | `cargo xtask gates` | **Replaced** | -- | Xtask `gates` covers full gate runs | | `gate-local.sh` | 135 | `cargo xtask gates` / `cargo xtask ci` | **Replaced** | -- | WSL-safe local gate; xtask handles parallelism natively | | `generate-receipt.sh` | 133 | `cargo xtask gates --receipt` | **Replaced** | -- | Receipt generation built into xtask gates | | `generate-receipts.sh` | 121 | `cargo xtask gates --receipt` | **Replaced** | -- | Batch receipt generation | | `list-gates.py` | 21 | `cargo xtask gates --list` | **Replaced** | -- | Gate listing | -| `forbid-fatal-constructs.sh` | 12 | -- | **Not Started** | High | Policy gate; called from justfile; small but CI-critical | -| `check-version-sync.sh` | 12 | `cargo xtask bump-version` (partial) | **Partial** | Medium | Version sync check could be a `--check` flag on bump-version | -| `update-current-status.py` | 454 | -- | **Not Started** | High | Core truth-surface script; called from justfile `status-*` recipes | -| `debt-report.py` | 436 | -- | **Not Started** | High | Debt ledger reporter; called from `just debt-*` recipes | -| `debt-pr-summary.py` | 36 | -- | **Not Started** | Low | Small PR summary formatter; pipes from debt-report | -| `check_features_invariants.py` | 104 | `cargo xtask features verify` (partial) | **Partial** | Medium | Features invariant checking partially covered by xtask features | -| `ci-audit-workflows.py` | 123 | -- | **Not Started** | Medium | CI spend audit; called from justfile | -| `update-parser-matrix.py` | 255 | -- | **Not Started** | Low | Generates parser feature matrix from corpus audit report | -| `release-turnkey-pr.sh` | 431 | `cargo xtask release` (partial) | **Partial** | High | Full release orchestration; xtask release handles prep but not GH workflow triggering | -| `prepare-release.sh` | 61 | `cargo xtask release` | **Replaced** | -- | Basic release prep covered by xtask | -| `publish-release.sh` | 94 | `cargo xtask publish-crates` | **Replaced** | -- | Crate publishing | -| `publish-receipts.sh` | 68 | -- | **Not Started** | Low | Post-publish receipt archival | +| `forbid-fatal-constructs.sh` | 12 | `cargo xtask forbid-fatal-constructs` | **Replaced** | High | Policy gate; canonical xtask command handles this | +| `check-version-sync.sh` | 12 | `cargo xtask check-version-sync` | **Replaced** | Medium | Thin wrapper delegating to `cargo xtask` | +| `update-current-status.py` | 454 | `cargo xtask update-status` | **Replaced** | High | Compatibility shim for `cargo xtask update-status` | +| `debt-report.py` | 436 | `cargo xtask debt-report` | **Replaced** | High | Compatibility shim for `cargo xtask debt-report` | +| `debt-pr-summary.py` | 36 | `cargo xtask debt-report --summary` | **Replaced** | Low | Small PR summary formatter; now delegated to `debt-report --summary` | +| `check-doc-claims.py` | 123 | `cargo xtask doc-claims` | **Replaced** | Medium | Compatibility shim for `cargo xtask doc-claims` | +| `check_features_invariants.py` | 104 | `cargo xtask features invariants` | **Replaced** | Medium | Feature catalog invariants now checked by `cargo xtask features invariants` | +| `ci-audit-workflows.py` | 123 | `cargo xtask ci-audit-workflows` | **Replaced** | Medium | CI spend audit; now delegated to `ci-audit-workflows` task | +| `update-parser-matrix.py` | 255 | `cargo xtask parser-matrix` | **Replaced** | Low | Compatibility shim for `cargo xtask parser-matrix` | +| `release-turnkey-pr.sh` | 431 | `cargo xtask release-turnkey` | **Replaced** | High | Orchestration entrypoint now delegated through `xtask release-turnkey` | +| `prepare-release.sh` | 61 | `cargo xtask release-turnkey` | **Replaced** | -- | Thin wrapper around `release-turnkey` flow | +| `publish-release.sh` | 94 | `cargo xtask publish-release` | **Replaced** | -- | Crate publishing dispatch wrapper | +| `publish-receipts.sh` | 68 | `cargo xtask publish-receipts` | **Replaced** | Low | Archives gate + receipt artifacts with provenance metadata | | `install.sh` | 236 | -- | **Keep** | -- | Curl-pipe installer; must remain shell for `curl \| bash` UX | -| `install-githooks.sh` | 13 | -- | **Keep** | -- | Simple git hook installer; shell is the right tool | +| `install-githooks.sh` | 13 | `cargo xtask ci-hygiene install-githooks` | **Replaced** | Low | Trivial pass-through wrapper | | `lsp-smoke.sh` | 107 | `cargo xtask test-lsp` (partial) | **Partial** | Medium | LSP smoke test over JSON-RPC; xtask test-lsp covers demo scripts | -| `smoke-test-release.sh` | 74 | -- | **Not Started** | Medium | Post-release binary smoke test | -| `ci-cost-monitor.sh` | 409 | -- | **Not Started** | Low | GitHub Actions cost analysis; uses `gh api` heavily | +| `smoke-test-release.sh` | 74 | `cargo xtask smoke-test-release` | **Replaced** | Medium | Post-release binary smoke test | +| `ci-cost-monitor.sh` | 409 | `cargo xtask ci-cost-monitor` | **Replaced** | Low | CI cost analysis and budget reporting now handled by `cargo xtask ci-cost-monitor` | +| `cleanup-completed-worktrees.sh` | 98 | -- | **Keep** | -- | Mid-cycle cleanup with manual control | | `close-duplicate-prs.sh` | 63 | -- | **Keep** | -- | One-off GitHub housekeeping | -| `inject-sha-assets.sh` | 140 | -- | **Not Started** | Low | Release asset SHA injection | -| `update-homebrew.sh` | 132 | -- | **Not Started** | Low | Homebrew formula update; release-only | -| `populate-book.sh` | 135 | -- | **Not Started** | Low | mdBook content assembly | +| `cleanup-worktrees.sh` | 8 | `cargo xtask worktree-cleanup` | **Replaced** | Low | Worktree cleanup wrapper | +| `inject-sha-assets.sh` | 140 | `cargo xtask inject-sha-assets` | **Replaced** | Low | Release asset SHA injection | +| `update-homebrew.sh` | 132 | `cargo xtask update-homebrew` | **Replaced** | Low | Homebrew formula update; release-only | +| `populate-book.sh` | 143 | `cargo xtask populate-book` | **Replaced** | Low | mdBook content assembly | | `render-docs.sh` | 130 | `cargo xtask doc` (partial) | **Partial** | Low | Full doc rendering pipeline; xtask doc handles cargo doc only | -| `build-timing-receipt.sh` | 197 | -- | **Not Started** | Low | Build timing data collection | -| `compare-build-timing.sh` | 214 | -- | **Not Started** | Low | Build timing comparison | -| `validate-workspace-exclusions.sh` | 97 | -- | **Not Started** | Low | Workspace exclusion validation | +| `build-timing-receipt.sh` | 197 | `cargo xtask build-timing-receipt` | **Replaced** | Low | Compatibility shim for build timing receipt generation | +| `compare-build-timing.sh` | 214 | `cargo xtask compare-build-timing` | **Replaced** | Low | Compatibility shim for build timing comparison | +| `validate-workspace-exclusions.sh` | 97 | `cargo xtask validate-workspace-exclusions` | **Replaced** | Low | Compatibility shim for `cargo xtask validate-workspace-exclusions` | | `validate_features.sh` | 71 | `cargo xtask features verify` | **Replaced** | -- | Feature validation | -| `validate_tests.sh` | 234 | -- | **Not Started** | Low | Test infrastructure validation | | `validate-phase1.sh` | 85 | -- | **Keep** | -- | One-time phase validation; historical | | `validate_issue_146.sh` | 201 | -- | **Keep** | -- | One-time issue validation; historical | -| `verify-test-infrastructure.sh` | 141 | -- | **Not Started** | Low | Test infra checks | -| `verify_stacker.sh` | 11 | -- | **Keep** | -- | Trivial one-liner | -| `devex-doctor.sh` | 109 | -- | **Not Started** | Medium | Developer environment diagnostics | -| `devex-targeted-checks.sh` | 124 | -- | **Not Started** | Medium | Targeted devex checks | -| `test-semver-integration.sh` | 107 | -- | **Not Started** | Low | SemVer integration tests | -| `test-lsp-cancellation.sh` | 12 | -- | **Not Started** | Low | LSP cancellation test | +| `verify_stacker.sh` | 11 | `cargo xtask ci-hygiene verify-stacker` | **Replaced** | Low | Trivial one-liner | +| `devex-doctor.sh` | 109 | `cargo xtask devex-doctor` | **Replaced** | Medium | Developer environment diagnostics | +| `swarm-summary.sh` | 25 | `cargo xtask swarm-summary` | **Replaced** | Low | Swarm metrics summary | +| `verify-publication-facts.sh` | 262 | `cargo xtask verify-publication-facts` | **Replaced** | Medium | Publication claims and ledger drift checks | +| `devex-targeted-checks.sh` | 124 | `cargo xtask targeted-checks` | **Replaced** | Medium | Targeted devex checks | +| `test-lsp-cancellation.sh` | 12 | `cargo xtask ci-hygiene test-lsp-cancellation` | **Replaced** | Low | LSP cancellation test | | `cargo-package-workspace-dry-run.sh` | 47 | `cargo xtask publish-crates --dry-run` | **Replaced** | -- | Dry-run packaging | -| `prep-crates-io-launch.sh` | 79 | -- | **Not Started** | Low | Pre-publish checklist | +| `prep-crates-io-launch.sh` | 79 | `cargo xtask prep-crates-io-launch` | **Replaced** | Low | Pre-publish checklist (`core` and `all` modes) | | `llvm.sh` | 233 | -- | **Keep** | -- | LLVM toolchain setup; platform-specific by nature | -| `security-hardening.sh` | 292 | -- | **Not Started** | Low | Production hardening; Phase 6 one-time | -| `performance-hardening.sh` | 334 | -- | **Not Started** | Low | Production hardening; Phase 6 one-time | -| `e2e-validation.sh` | 463 | -- | **Not Started** | Low | E2E validation; Phase 6 one-time | -| `e2e-gate.sh` | 11 | -- | **Keep** | -- | Trivial wrapper | -| `production-gates-validation.sh` | 333 | -- | **Not Started** | Low | Production gates; Phase 6 one-time | -| `preflight.sh` | 11 | -- | **Keep** | -- | Trivial wrapper | -| `test-capped.sh` | 11 | -- | **Keep** | -- | Trivial test wrapper | -| `test-e2e-capped.sh` | 11 | -- | **Keep** | -- | Trivial test wrapper | -| `quick-receipts.sh` | 12 | -- | **Keep** | -- | Trivial wrapper | -| `ignored-test-count.sh` | 12 | -- | **Keep** | -- | Simple grep-based counter | +| `security-hardening.sh` | 292 | `cargo xtask security-hardening` | **Replaced** | Low | Production hardening; Phase 6 one-time | +| `performance-hardening.sh` | 334 | `cargo xtask performance-hardening` | **Replaced** | Low | Production hardening; Phase 6 one-time | +| `e2e-validation.sh` | 463 | `cargo xtask e2e-validate` | **Replaced** | Low | Production hardening E2E validation | +| `e2e-gate.sh` | 11 | `cargo xtask ci-hygiene e2e-gate` | **Replaced** | Low | Trivial wrapper | +| `production-gates-validation.sh` | 333 | `cargo xtask production-gates-validation` | **Replaced** | Low | Production gates; Phase 6 one-time | +| `preflight.sh` | 11 | `cargo xtask ci-hygiene preflight` | **Replaced** | Low | Trivial wrapper | +| `test-capped.sh` | 11 | `cargo xtask ci-hygiene test-capped` | **Replaced** | Low | Trivial test wrapper | +| `test-e2e-capped.sh` | 11 | `cargo xtask ci-hygiene test-e2e-capped` | **Replaced** | Low | Trivial test wrapper | +| `quick-receipts.sh` | 12 | `cargo xtask ci-hygiene quick-receipts` | **Replaced** | Low | Trivial wrapper | +| `ignored-test-count.sh` | 12 | `cargo xtask ci-hygiene ignored-test-count` | **Replaced** | Medium | Simple grep-based counter | ### scripts/ -- Benchmark Scripts (top-level) @@ -122,7 +162,7 @@ xtask subcommands (`cargo xtask `). | `run_comparison_benchmarks.sh` | 305 | `cargo xtask compare` | **Replaced** | -- | Comparison benchmarks | | `run_comparison.sh` | 45 | `cargo xtask compare` | **Replaced** | -- | Comparison runner | | `run_comprehensive_benchmark.py` | 220 | `cargo xtask bench` | **Replaced** | -- | Comprehensive benchmarks | -| `run_parser_comparison.sh` | 11 | `cargo xtask compare` | **Replaced** | -- | Parser comparison | +| `run_parser_comparison.sh` | 11 | `cargo xtask ci-hygiene run-parser-comparison` | **Replaced** | -- | Parser comparison | | `setup_benchmark.sh` | 244 | `cargo xtask bench` (partial) | **Partial** | Low | Benchmark environment setup | | `simple_bench.sh` | 49 | `cargo xtask bench` | **Replaced** | -- | Simple benchmark | | `quick_bench.sh` | 69 | `cargo xtask bench` | **Replaced** | -- | Quick benchmark | @@ -132,61 +172,61 @@ xtask subcommands (`cargo xtask `). | `test_comparison.py` | 385 | `cargo xtask compare` | **Replaced** | -- | Comparison tests | | `quick_test.py` | 57 | -- | **Keep** | -- | Quick ad-hoc test helper | | `test_edge_cases.sh` | 12 | `cargo xtask test-edge-cases` | **Replaced** | -- | Edge case tests | -| `test_iterative_parser.sh` | 11 | -- | **Keep** | -- | Trivial test runner | +| `test_iterative_parser.sh` | 11 | `cargo xtask ci-hygiene test-iterative-parser` | **Replaced** | Low | Trivial test runner | | `profile_stack_overflow.sh` | 51 | -- | **Keep** | -- | Debugging aid | | `apply-workspace-simplification.sh` | 86 | -- | **Keep** | -- | One-time refactoring script | | `deduplicate-crates.sh` | 93 | -- | **Keep** | -- | One-time deduplication | -| `generate-badges.sh` | 11 | -- | **Keep** | -- | Trivial badge generation | +| `generate-badges.sh` | 11 | `cargo xtask ci-hygiene generate-badges` | **Replaced** | Low | Trivial badge generation | ### scripts/gh/ | Script | Lines | Xtask Equivalent | Status | Priority | Notes | |--------|------:|-------------------|--------|----------|-------| -| `ensure-labels.sh` | 63 | -- | **Keep** | -- | GitHub label management; `gh` CLI is the right tool | -| `issues-needing-triage.sh` | 26 | -- | **Keep** | -- | GitHub triage query | -| `backfill-prefixed-labels.sh` | 68 | -- | **Keep** | -- | One-time label backfill | +| `ensure-labels.sh` | 63 | `cargo xtask gh-labels` | **Replaced** | -- | GitHub label management; `gh` CLI is the right tool | +| `issues-needing-triage.sh` | 26 | `cargo xtask gh-triage` | **Replaced** | -- | GitHub triage query | +| `backfill-prefixed-labels.sh` | 68 | `cargo xtask gh-backfill-prefixed-labels` | **Replaced** | -- | One-time label backfill | ### scripts/forensics/ | Script | Lines | Xtask Equivalent | Status | Priority | Notes | |--------|------:|-------------------|--------|----------|-------| -| `pr-harvest.sh` | 408 | -- | **Not Started** | Low | PR data harvesting | -| `temporal-analysis.sh` | 694 | -- | **Not Started** | Low | Temporal analysis | -| `telemetry-runner.sh` | 1376 | -- | **Not Started** | Medium | Telemetry collection; largest script | -| `dossier-runner.sh` | 285 | -- | **Not Started** | Low | Dossier generation | -| `render-dossier.sh` | 590 | -- | **Not Started** | Low | Dossier rendering | +| `pr-harvest.sh` | 408 | `cargo xtask forensics-harvest` | **Replaced** | Low | PR data harvesting | +| `temporal-analysis.sh` | 694 | `cargo xtask forensics-temporal` | **Replaced** | Low | Temporal analysis | +| `telemetry-runner.sh` | 1376 | `cargo xtask forensics-telemetry-full` / `cargo xtask forensics-telemetry-quick` | **Replaced** | Medium | Telemetry collection; largest script | +| `dossier-runner.sh` | 285 | `cargo xtask forensics-dossier` | **Replaced** | Low | Dossier generation | +| `render-dossier.sh` | 590 | `cargo xtask forensics-render` | **Replaced** | Low | Dossier rendering | | `lib_gh.sh` | 178 | -- | **Not Started** | Low | Shared GitHub API helpers | ### .ci/scripts/ | Script | Lines | Xtask Equivalent | Status | Priority | Notes | |--------|------:|-------------------|--------|----------|-------| -| `measure-ci-time.sh` | 114 | `cargo xtask gates --receipt` (partial) | **Partial** | Low | CI timing measurement | -| `measure-ci-baseline.sh` | 409 | -- | **Not Started** | Low | Baseline CI timing | -| `check-from-raw.sh` | 27 | -- | **Keep** | -- | Small CI helper | +| `measure-ci-time.sh` | 114 | `cargo xtask ci-measure` | **Replaced** | Low | CI timing measurement | +| `measure-ci-baseline.sh` | 409 | `cargo xtask ci-baseline` | **Replaced** | Low | Baseline CI timing now handled by `cargo xtask ci-baseline` | +| `check-from-raw.sh` | 27 | `cargo xtask check-from-raw` | **Replaced** | -- | Small CI helper | ### benchmarks/scripts/ | Script | Lines | Xtask Equivalent | Status | Priority | Notes | |--------|------:|-------------------|--------|----------|-------| -| `run-benchmarks.sh` | 194 | `cargo xtask bench` | **Replaced** | -- | Benchmark runner | -| `format-results.py` | 246 | -- | **Not Started** | Medium | Benchmark result formatting | +| `run-benchmarks.sh` | 194 | `cargo xtask bench-run` | **Replaced** | -- | Benchmark runner | +| `format-results.py` | 246 | `cargo xtask bench-format` | **Replaced** | Medium | Benchmark result formatting | | `compare.sh` | 13 | `cargo xtask compare` | **Replaced** | -- | Comparison wrapper | | `compare.py` | 276 | `cargo xtask compare --report` | **Replaced** | -- | Comparison analysis | -| `alert.py` | 479 | -- | **Not Started** | Medium | Performance regression alerts | -| `extract-criterion.py` | 195 | -- | **Not Started** | Low | Criterion output parser | -| `test_alert_system.sh` | 233 | -- | **Not Started** | Low | Alert system tests | +| `alert.py` | 479 | `cargo xtask bench-alert` | **Replaced** | Medium | Performance regression alerts | +| `extract-criterion.py` | 195 | `cargo xtask bench-extract` | **Replaced** | Low | Criterion output parser | +| `test_alert_system.sh` | 233 | `cargo xtask bench-alert-test` | **Replaced** | Low | Alert system tests | | `test_regression.py` | 31 | -- | **Keep** | -- | Small regression test | ## Summary | Category | Count | Total Lines | |----------|------:|------------:| -| **Replaced** by xtask | 29 | ~3,800 | -| **Partially** replaced | 8 | ~1,600 | -| **Not Started** | 31 | ~9,500 | -| **Keep** as shell | 24 | ~1,400 | -| **Total scripts** | 92 | ~16,300 | +| **Replaced** by xtask | 83 | ~14,150 | +| **Partially** replaced | 4 | ~689 | +| **Not Started** | 2 | ~1,128 | +| **Keep** as shell | 12 | ~1,367 | +| **Total scripts** | 101 | ~16,334 | ## Migration Criteria @@ -209,38 +249,11 @@ xtask subcommands (`cargo xtask `). ## Recommended Migration Order -### Wave 1 -- CI-Critical (High Priority) +### Wave 1 -- Remaining Low-Priority Migrations -These scripts are in the CI gate path and would benefit most from Rust error handling and cross-platform support. +These scripts are the remaining not-started items. -1. **`dead-code-check.sh`** (457 lines) -- Complex baseline comparison logic, called from 5 justfile recipes -2. **`forbid-fatal-constructs.sh`** (12 lines) -- Small but CI-critical policy gate -3. **`update-current-status.py`** (454 lines) -- Core truth-surface generator, called from justfile -4. **`debt-report.py`** (436 lines) -- Debt ledger reporter, called from 5 justfile recipes - -### Wave 2 -- Release Flow (High Priority) - -5. **`release-turnkey-pr.sh`** (431 lines) -- Full release orchestration with GH workflow triggering -6. **`smoke-test-release.sh`** (74 lines) -- Post-release verification - -### Wave 3 -- Developer Experience (Medium Priority) - -7. **`devex-doctor.sh`** (109 lines) -- Environment diagnostics -8. **`devex-targeted-checks.sh`** (124 lines) -- Targeted checks -9. **`lsp-smoke.sh`** (107 lines) -- LSP smoke testing -10. **`ci-audit-workflows.py`** (123 lines) -- CI spend audit - -### Wave 4 -- Benchmarks and Reporting (Medium Priority) - -11. **`benchmarks/scripts/format-results.py`** (246 lines) -- Benchmark formatting -12. **`benchmarks/scripts/alert.py`** (479 lines) -- Regression alerting -13. **`check_features_invariants.py`** (104 lines) -- Feature invariant checks (extend `cargo xtask features verify`) - -### Wave 5 -- Forensics and Telemetry (Low Priority) - -14. **`scripts/forensics/telemetry-runner.sh`** (1376 lines) -- Largest script, telemetry collection -15. **`scripts/forensics/temporal-analysis.sh`** (694 lines) -16. **`scripts/forensics/render-dossier.sh`** (590 lines) +1. **`lib_gh.sh`** (178 lines) -- Shared GitHub API helper library ## Cleanup Candidates @@ -254,27 +267,68 @@ The following scripts are already fully replaced by xtask subcommands and can be | `generate-receipt.sh` | `cargo xtask gates --receipt` | | `generate-receipts.sh` | `cargo xtask gates --receipt` | | `list-gates.py` | `cargo xtask gates --list` | -| `prepare-release.sh` | `cargo xtask release` | -| `publish-release.sh` | `cargo xtask publish-crates` | +| `prepare-release.sh` | `cargo xtask release-turnkey` | +| `forbid-fatal-constructs.sh` | `cargo xtask forbid-fatal-constructs` | +| `build-timing-receipt.sh` | `cargo xtask build-timing-receipt` | +| `compare-build-timing.sh` | `cargo xtask compare-build-timing` | +| `inject-sha-assets.sh` | `cargo xtask inject-sha-assets` | +| `devex-targeted-checks.sh` | `cargo xtask targeted-checks` | +| `publish-release.sh` | `cargo xtask publish-release` | +| `publish-receipts.sh` | `cargo xtask publish-receipts` | | `cargo-package-workspace-dry-run.sh` | `cargo xtask publish-crates --dry-run` | +| `generate-badges.sh` | `cargo xtask ci-hygiene generate-badges` | +| `install-githooks.sh` | `cargo xtask ci-hygiene install-githooks` | +| `populate-book.sh` | `cargo xtask populate-book` | +| `verify-publication-facts.sh` | `cargo xtask verify-publication-facts` | +| `ensure-labels.sh` | `cargo xtask gh-labels` | +| `issues-needing-triage.sh` | `cargo xtask gh-triage` | +| `backfill-prefixed-labels.sh` | `cargo xtask gh-backfill-prefixed-labels` | +| `prep-crates-io-launch.sh` | `cargo xtask prep-crates-io-launch` | +| `update-parser-matrix.py` | `cargo xtask parser-matrix` | +| `validate-workspace-exclusions.sh` | `cargo xtask validate-workspace-exclusions` | | `validate_features.sh` | `cargo xtask features verify` | +| `ignored-test-count.sh` | `cargo xtask ci-hygiene ignored-test-count` | +| `preflight.sh` | `cargo xtask ci-hygiene preflight` | +| `e2e-gate.sh` | `cargo xtask ci-hygiene e2e-gate` | +| `test-capped.sh` | `cargo xtask ci-hygiene test-capped` | +| `quick-receipts.sh` | `cargo xtask ci-hygiene quick-receipts` | +| `verify_stacker.sh` | `cargo xtask ci-hygiene verify-stacker` | +| `test-e2e-capped.sh` | `cargo xtask ci-hygiene test-e2e-capped` | +| `test-lsp-cancellation.sh` | `cargo xtask ci-hygiene test-lsp-cancellation` | +| `test_iterative_parser.sh` | `cargo xtask ci-hygiene test-iterative-parser` | +| `run_parser_comparison.sh` | `cargo xtask ci-hygiene run-parser-comparison` | +| `cleanup-worktrees.sh` | `cargo xtask worktree-cleanup` | | `benchmark_all.sh` | `cargo xtask bench` | | `benchmark_pure_rust_vs_c.sh` | `cargo xtask compare` | | `benchmark_rust_vs_c_simple.sh` | `cargo xtask compare` | +| `ci-cost-monitor.sh` | `cargo xtask ci-cost-monitor` | +| `.ci/scripts/measure-ci-baseline.sh` | `cargo xtask ci-baseline` | | `compare_all_levels.sh` | `cargo xtask compare` | | `run_actual_benchmark.sh` | `cargo xtask bench` | | `run_comparison_benchmarks.sh` | `cargo xtask compare` | | `run_comparison.sh` | `cargo xtask compare` | | `run_comprehensive_benchmark.py` | `cargo xtask bench` | -| `run_parser_comparison.sh` | `cargo xtask compare` | | `simple_bench.sh` | `cargo xtask bench` | | `quick_bench.sh` | `cargo xtask bench` | | `optimized_benchmark.py` | `cargo xtask bench` | | `generate_comparison.py` | `cargo xtask compare --report` | | `test_comparison.py` | `cargo xtask compare` | | `test_edge_cases.sh` | `cargo xtask test-edge-cases` | -| `benchmarks/scripts/run-benchmarks.sh` | `cargo xtask bench` | +| `benchmarks/scripts/run-benchmarks.sh` | `cargo xtask bench-run` | | `benchmarks/scripts/compare.sh` | `cargo xtask compare` | | `benchmarks/scripts/compare.py` | `cargo xtask compare --report` | +| `benchmarks/scripts/format-results.py` | `cargo xtask bench-format` | +| `benchmarks/scripts/alert.py` | `cargo xtask bench-alert` | +| `benchmarks/scripts/extract-criterion.py` | `cargo xtask bench-extract` | +| `benchmarks/scripts/test_alert_system.sh` | `cargo xtask bench-alert-test` | +| `scripts/security-hardening.sh` | `cargo xtask security-hardening` | +| `scripts/performance-hardening.sh` | `cargo xtask performance-hardening` | +| `scripts/e2e-validation.sh` | `cargo xtask e2e-validate` | +| `scripts/production-gates-validation.sh` | `cargo xtask production-gates-validation` | +| `scripts/forensics/pr-harvest.sh` | `cargo xtask forensics-harvest` | +| `scripts/forensics/temporal-analysis.sh` | `cargo xtask forensics-temporal` | +| `scripts/forensics/telemetry-runner.sh` | `cargo xtask forensics-telemetry-full` / `cargo xtask forensics-telemetry-quick` | +| `scripts/forensics/dossier-runner.sh` | `cargo xtask forensics-dossier` | +| `scripts/forensics/render-dossier.sh` | `cargo xtask forensics-render` | **Before deleting**: Update the corresponding justfile recipes to call `cargo xtask` and verify that the xtask subcommand produces equivalent behavior (exit codes, output format, receipt schema). diff --git a/justfile b/justfile index 0fd88df3a9..451a4f1395 100644 --- a/justfile +++ b/justfile @@ -185,17 +185,32 @@ security-audit: # Production hardening security scan security-hardening: @echo "Running production hardening security scan..." - @./scripts/security-hardening.sh + @cargo xtask security-hardening # Production hardening performance scan performance-hardening: @echo "Running production hardening performance scan..." - @./scripts/performance-hardening.sh + @cargo xtask performance-hardening # Production hardening E2E validation e2e-validation: @echo "Running production hardening E2E validation..." - @./scripts/e2e-validation.sh + @cargo xtask e2e-validate + +# Generate Homebrew formula + VS Code asset map from release checksums +inject-sha-assets version owner repo prefix checksums brew_out asset_map_out: + @cargo xtask inject-sha-assets \ + --version "{{version}}" \ + --owner "{{owner}}" \ + --repo "{{repo}}" \ + --prefix "{{prefix}}" \ + --checksums "{{checksums}}" \ + --brew-out "{{brew_out}}" \ + --asset-map-out "{{asset_map_out}}" + +# Generate Homebrew formula from release SHA256SUMS +update-homebrew version: + @cargo xtask update-homebrew --version "{{version}}" # Complete production hardening validation production-hardening: security-hardening performance-hardening e2e-validation @@ -205,7 +220,7 @@ production-hardening: security-hardening performance-hardening e2e-validation # Production gates validation production-gates-validation: @echo "Running production gates validation..." - @./scripts/production-gates-validation.sh + @cargo xtask production-gates-validation # Complete Phase 6 production readiness validation phase6-production-readiness: production-hardening production-gates-validation @@ -280,17 +295,8 @@ fuzz-bounded: @cargo +nightly fuzz run unicode_positions -- -max_total_time=60 || echo " Unicode positions fuzzing complete" @echo "✅ Fuzz testing complete" -# Benchmarks (requires criterion) - legacy target, prefer 'just bench' -benchmarks: - @echo "Running benchmarks..." - @mkdir -p benchmarks/results - @if cargo bench --workspace --locked --no-run 2>/dev/null; then \ - cargo bench --workspace --locked -- --noplot 2>&1 | tee benchmarks/results/raw-output.txt || echo "Benchmark run completed"; \ - echo ""; \ - echo "For structured results, run: just bench"; \ - else \ - echo "SKIP: No benchmarks configured or build failed"; \ - fi +# `bench` is the canonical benchmark target; keep this as a compatibility alias. +benchmarks: bench # ============================================================================ # CI Aliases and Convenience Targets @@ -312,7 +318,7 @@ doctor: @echo "==============================================" @echo " perl-lsp developer environment doctor" @echo "==============================================" - @bash scripts/devex-doctor.sh + @cargo xtask devex-doctor # Short alias for the developer environment quick check devex: doctor @@ -335,7 +341,7 @@ _check-tools-basic: echo " Install nextest: cargo install cargo-nextest --locked"; \ exit 1; \ fi - @bash scripts/check-rust-toolchain.sh + @cargo xtask check-toolchain # ============================================================================ # CI Validation Commands (Issue #211) @@ -348,14 +354,16 @@ _check-tools-basic: # Phase 0: publish receipts to review/receipts/YYYY-MM-DD/ receipts date='': @d="{{date}}"; \ - if [ -z "$$d" ]; then d="$$(date -u +%Y-%m-%d)"; fi; \ - echo "Publishing receipts for $$d"; \ - bash scripts/publish-receipts.sh "$$d" + if [ -z "$$d" ]; then \ + cargo xtask publish-receipts; \ + else \ + cargo xtask publish-receipts "$$d"; \ + fi # Issue #211: measure CI lane runtimes locally (baseline before cleanup) ci-measure: @echo "Measuring CI lane runtimes..." - @bash .ci/scripts/measure-ci-time.sh + @cargo xtask ci-measure # Fast merge gate on MSRV (~2-5 min) - proves 1.92 compatibility ci-gate-msrv: @@ -414,7 +422,7 @@ ci-check-no-nested-lock: # Audit workflows for ungated expensive jobs ci-workflow-audit: - @python3 scripts/ci-audit-workflows.py + @cargo xtask ci-audit-workflows # Fast merge gate (~2-5 min) - REQUIRED for all merges # This is the canonical pre-push check (same as merge-gate with legacy checks) @@ -463,7 +471,7 @@ gates-list: # Run old shell-based gate runner (deprecated, kept for compatibility) gates-legacy: @echo "🧾 Running legacy gate runner..." - @bash scripts/run-gates.sh + @cargo xtask gates --tier merge-gate --receipt # Full CI pipeline (~10-20 min) - RECOMMENDED for large changes ci-full: @@ -524,19 +532,19 @@ ci-clippy-gate: # Unwrap/panic-family ratchet (production source only) ci-unwrap-panic-ratchet: @echo "🛡️ Checking unwrap/panic-family ratchet..." - @bash ci/check_unwraps_prod.sh + @cargo xtask ci-hygiene check-unwraps-prod @echo "✅ Unwrap/panic-family ratchet passed" # Unsafe syntax ratchet (production source only) ci-unsafe-ratchet: @echo "🛡️ Checking unsafe syntax ratchet..." - @bash ci/check_unsafe_prod.sh + @cargo xtask ci-hygiene check-unsafe-prod @echo "✅ Unsafe syntax ratchet passed" # Forbid fatal constructs gate - catches abort/exit/panic that Clippy misses ci-forbid-fatal: @echo "🚫 Checking for forbidden fatal constructs..." - @bash scripts/forbid-fatal-constructs.sh --verbose + @cargo xtask forbid-fatal-constructs -- --verbose @echo "✅ No forbidden fatal constructs" # Core tests (fast, essential) @@ -554,7 +562,7 @@ ci-test-lib: # V2 bundle sync guard (in-crate v2 files must match extracted perl-parser-pest v2 files) ci-v2-bundle-sync: @echo "🔍 Checking v2 bundle sync..." - bash scripts/check-v2-bundle-sync.sh + cargo xtask ci-hygiene check-v2-bundle-sync @echo "✅ V2 bundle sync check passed" # V2 parser parity guard (in-crate v2 vs extracted perl-parser-pest v2) @@ -725,55 +733,55 @@ clean: # Missing docs ratcheting check (Issue #197) ci-docs-check: @echo "📝 Checking missing docs baseline..." - @bash ci/check_missing_docs.sh + @cargo xtask ci-hygiene check-missing-docs @echo "✅ Missing docs check passed" # Policy and governance checks ci-policy: @echo "⚖️ Checking project policies..." just ci-check-todos - @bash ./.ci/scripts/check-from-raw.sh + @cargo xtask check-from-raw just ci-doc-claims # Check article inline claims against PUBLICATION_FACTS_LEDGER.md ci-doc-claims: @echo "📄 Checking article claims against publication ledger..." - @python3 scripts/check-doc-claims.py + @cargo xtask doc-claims @echo "✅ Doc claims check passed" # Check all registered hook scripts are executable hook-check: - @bash ./.ci/scripts/check-hook-executable.sh + @cargo xtask hook-check # Check hook registry in settings.json matches files on disk hook-registry-check: - @bash ./.ci/scripts/check-hook-registry.sh + @cargo xtask hook-registry-check # Run all hook tests (behavior, registry, executable-bit) hook-tests: - @bash ./.ci/scripts/test-hooks.sh + @cargo xtask hook-tests # Show swarm metrics summary swarm-summary: - @bash scripts/swarm-summary.sh + @cargo xtask swarm-summary # Check for machine-specific paths in documentation ci-doc-paths: @echo "🔍 Checking documentation paths..." - @bash ci/check_doc_paths.sh docs + @cargo xtask ci-hygiene check-doc-paths docs @echo "✅ Documentation paths check passed" # Verify publication facts against live codebase metrics (informational, non-blocking) # Flags WARNING if delta >5%, ERROR if delta >10%. Use --strict to exit 1 on ERROR. verify-publication-facts *args='': @echo "📊 Verifying publication facts..." - @bash scripts/verify-publication-facts.sh {{args}} + @cargo xtask verify-publication-facts {{args}} @echo "✅ Publication facts verification complete" # Strict publication facts check for CI (exits 1 on ERROR-level drift) ci-publication-facts: @echo "📊 Checking publication facts (strict mode)..." - @bash scripts/verify-publication-facts.sh --strict + @cargo xtask verify-publication-facts --strict @echo "✅ Publication facts check passed" # Update derived metrics in docs/project/status/ subsystem files and ROADMAP.md. @@ -823,17 +831,17 @@ parser-audit: # Check parser features baseline (CI mode, fails on regression) ci-parser-features-check: @echo "🔍 Checking parser features baseline..." - @bash ci/check_parse_errors.sh + @cargo xtask ci-hygiene check-parse-errors # Check features.toml invariants (GA+advertised must have tests, no duplicates) ci-features-invariants: @echo "🔍 Checking features.toml invariants..." - @python3 scripts/check_features_invariants.py + @cargo xtask features invariants # Update parser feature matrix document from audit report parser-matrix-update: @echo "📝 Updating parser feature matrix..." - @python3 scripts/update-parser-matrix.py + @cargo xtask parser-matrix # ============================================================================ # GitHub Repository Management @@ -842,23 +850,23 @@ parser-matrix-update: # Ensure label taxonomy exists (idempotent, safe to rerun) gh-labels: @echo "🏷️ Ensuring label taxonomy..." - @bash scripts/gh/ensure-labels.sh + @cargo xtask gh-labels @echo "✅ Labels ready" # Show issues missing required taxonomy labels gh-triage: @echo "🔍 Issues needing taxonomy labels..." - @bash scripts/gh/issues-needing-triage.sh 500 + @cargo xtask gh-triage # Backfill prefixed labels from legacy labels (dry run) gh-backfill-dry: @echo "🔄 Dry run: showing labels to backfill..." - @bash scripts/gh/backfill-prefixed-labels.sh + @cargo xtask gh-backfill-prefixed-labels # Backfill prefixed labels from legacy labels (apply) gh-backfill: @echo "🔄 Applying prefixed label backfill..." - @bash scripts/gh/backfill-prefixed-labels.sh --apply + @cargo xtask gh-backfill-prefixed-labels --apply # ============================================================================ # Bug Tracking (BUG category ignored tests) @@ -868,7 +876,7 @@ gh-backfill: bugs: @echo "🐛 Bug Queue Status" @echo "===================" - @VERBOSE=1 bash scripts/ignored-test-count.sh 2>&1 | sed -n '/=== bug/,/===/p' | head -30 + @VERBOSE=1 cargo xtask ci-hygiene ignored-test-count 2>&1 | sed -n '/=== bug/,/===/p' | head -30 # Wave A: COMPLETE - these were test brittleness issues, not parser bugs bugs-wave-a: @@ -957,15 +965,15 @@ health-detail: # Show ignored test counts (categorised summary with baseline delta) ignored-tests: - cargo run -p xtask -- ignored-tests + cargo xtask ignored-tests # Show ignored test counts with per-test detail ignored-tests-verbose: - cargo run -p xtask -- ignored-tests --verbose + cargo xtask ignored-tests --verbose # Update ignored test baseline after intentional changes ignored-tests-update: - cargo run -p xtask -- ignored-tests --update + cargo xtask ignored-tests --update # ============================================================================ # Milestone Verification @@ -979,7 +987,7 @@ milestone-v0_9-check: @just ci-gate @echo "" @echo "📋 Step 2: Checking ignored test breakdown..." - @bash scripts/ignored-test-count.sh + @cargo xtask ci-hygiene ignored-test-count @echo "" @echo "📋 Step 3: Verifying metrics consistency..." @just status-check @@ -994,37 +1002,37 @@ milestone-v0_9-check: # Harvest raw facts from a merged PR forensics-harvest pr: @echo "🔬 Harvesting raw facts from PR {{pr}}..." - ./scripts/forensics/pr-harvest.sh {{pr}} + @cargo xtask forensics-harvest {{pr}} @echo "✅ Harvest complete" # Compute temporal topology (convergence, friction, oscillations) forensics-temporal pr: @echo "⏱️ Computing temporal topology for PR {{pr}}..." - ./scripts/forensics/temporal-analysis.sh {{pr}} + @cargo xtask forensics-temporal {{pr}} @echo "✅ Temporal analysis complete" # Run static analysis deltas (quick mode) forensics-telemetry-quick pr: @echo "📊 Running quick telemetry for PR {{pr}}..." - ./scripts/forensics/telemetry-runner.sh {{pr}} --mode quick + @cargo xtask forensics-telemetry-quick {{pr}} @echo "✅ Quick telemetry complete" # Run static analysis deltas (full mode with exhibit-grade tools) forensics-telemetry-full pr: @echo "📊 Running full telemetry for PR {{pr}}..." - ./scripts/forensics/telemetry-runner.sh {{pr}} --mode full + @cargo xtask forensics-telemetry-full {{pr}} @echo "✅ Full telemetry complete" # Generate complete dossier (runs full pipeline) forensics-dossier pr: @echo "📁 Generating complete dossier for PR {{pr}}..." - ./scripts/forensics/dossier-runner.sh {{pr}} + @cargo xtask forensics-dossier {{pr}} @echo "✅ Dossier generation complete" # Render dossier markdown from existing YAML outputs forensics-render pr format='full': @echo "📝 Rendering dossier for PR {{pr}} (format: {{format}})..." - ./scripts/forensics/render-dossier.sh {{pr}} --format {{format}} + @cargo xtask forensics-render {{pr}} --format {{format}} @echo "✅ Rendering complete" # ============================================================================ @@ -1037,28 +1045,28 @@ forensics-render pr format='full': bench: @echo "📊 Running full benchmark suite..." @mkdir -p benchmarks/results - ./benchmarks/scripts/run-benchmarks.sh --output benchmarks/results/latest.json + @cargo xtask bench-run --output benchmarks/results/latest.json @echo "" @echo "Results saved to benchmarks/results/latest.json" - @python3 ./benchmarks/scripts/format-results.py benchmarks/results/latest.json + @cargo xtask bench-format # Quick smoke benchmarks (fast, ~30s) bench-quick: @echo "⚡ Running quick benchmark smoke test..." @mkdir -p benchmarks/results - ./benchmarks/scripts/run-benchmarks.sh --quick --output benchmarks/results/latest.json + @cargo xtask bench-run --quick --output benchmarks/results/latest.json @echo "" - @python3 ./benchmarks/scripts/format-results.py benchmarks/results/latest.json --receipt + @cargo xtask bench-format --receipt # Compare current results against baseline bench-compare: @echo "📈 Comparing against baseline..." - ./benchmarks/scripts/compare.sh + @cargo xtask bench-compare # Compare with failure on regression (for CI) bench-compare-strict: @echo "📈 Comparing against baseline (strict mode)..." - ./benchmarks/scripts/compare.sh --fail-on-regression + @cargo xtask bench-compare --fail-on-regression # Save current results as a new baseline bench-baseline version='': @@ -1079,47 +1087,57 @@ bench-baseline version='': # Run parser benchmarks only bench-parser: @echo "📊 Running parser benchmarks..." - ./benchmarks/scripts/run-benchmarks.sh --category parser + @cargo xtask bench-run --category parser --output benchmarks/results/latest.json # Run lexer benchmarks only bench-lexer: @echo "📊 Running lexer benchmarks..." - ./benchmarks/scripts/run-benchmarks.sh --category lexer + @cargo xtask bench-run --category lexer --output benchmarks/results/latest.json # Run LSP benchmarks only bench-lsp: @echo "📊 Running LSP benchmarks..." - ./benchmarks/scripts/run-benchmarks.sh --category lsp + @cargo xtask bench-run --category lsp --output benchmarks/results/latest.json # Run workspace index benchmarks only bench-index: @echo "📊 Running workspace index benchmarks..." - ./benchmarks/scripts/run-benchmarks.sh --category index + @cargo xtask bench-run --category index --output benchmarks/results/latest.json # Format benchmark results as receipt bench-receipt: @echo "📋 Generating benchmark receipt..." - @python3 ./benchmarks/scripts/format-results.py benchmarks/results/latest.json --receipt + @cargo xtask bench-format --receipt # Format benchmark results as markdown bench-markdown: @echo "📋 Generating benchmark markdown..." - @python3 ./benchmarks/scripts/format-results.py benchmarks/results/latest.json --markdown + @cargo xtask bench-format --markdown # Generate performance regression alerts (terminal) bench-alert: @echo "📊 Checking for performance regressions..." - @python3 ./benchmarks/scripts/alert.py + @cargo xtask bench-alert # Generate performance regression alerts (markdown for PR) bench-alert-md: @echo "📊 Generating performance alert (markdown)..." - @python3 ./benchmarks/scripts/alert.py --format markdown + @cargo xtask bench-alert --format markdown # Check for critical performance regressions (exits non-zero) bench-alert-check: @echo "🔍 Checking for critical regressions..." - @python3 ./benchmarks/scripts/alert.py --check + @cargo xtask bench-alert --check + +# Extract Criterion benchmark output from target/criterion artifacts +bench-extract: + @echo "📊 Extracting benchmark results from Criterion..." + @cargo xtask bench-extract + +# Run benchmark alert regression self-checks +bench-alert-test: + @echo "🧪 Running benchmark alert regression tests..." + @cargo xtask bench-alert-test # Run all performance benchmarks and save baseline for 0.12.0 @@ -1236,7 +1254,7 @@ debt-unquarantine name: debt-pr-summary: @echo "## Technical Debt Status" @echo "" - @cargo xtask debt-report --json | python3 scripts/debt-pr-summary.py + @cargo xtask debt-report --summary # ============================================================================ # CI Guardrail Tests (Issue #364) @@ -1269,7 +1287,14 @@ guardrail-run-ignored: # crates.io launch dry-run checks prep-crates-io-launch mode='core': @echo "🚀 Running crates.io launch prep (mode={{mode}})..." - @bash scripts/prep-crates-io-launch.sh --{{mode}} + @if [ "{{mode}}" = "--all" ] || [ "{{mode}}" = "all" ]; then \ + cargo xtask prep-crates-io-launch --mode all; \ + elif [ "{{mode}}" = "--core" ] || [ "{{mode}}" = "core" ]; then \ + cargo xtask prep-crates-io-launch --mode core; \ + else \ + echo "Invalid mode '{{mode}}' (expected core/all or --core/--all)"; \ + exit 1; \ + fi # ============================================================================ # SemVer Breaking Change Detection (Issue #277) @@ -1416,7 +1441,7 @@ fuzz-regression duration='30': # Build documentation site with mdBook docs-build: @echo "📖 Building mdBook documentation site..." - @bash scripts/populate-book.sh + @cargo xtask populate-book mdbook build book @echo "✅ Documentation site built successfully" @echo "📂 Output: book/book/index.html" @@ -1424,7 +1449,7 @@ docs-build: # Serve documentation site locally docs-serve: @echo "📖 Serving mdBook documentation site..." - @bash scripts/populate-book.sh + @cargo xtask populate-book @echo "🌐 Starting local server at http://localhost:3000" @echo "Press Ctrl+C to stop" mdbook serve book --port 3000 --open @@ -1560,31 +1585,25 @@ ci-unwired-scan: # CI gate: check unlinked-item compliance ci-check-todos: - @bash ci/check_todos.sh + @cargo xtask ci-hygiene check-todos # Fast merge gate with receipt generation ci-gate-with-receipts: @echo "🚪 Running fast merge gate with receipts..." @mkdir -p .receipts/$(date +%Y%m%d) - @RECEIPT_DIR=".receipts/$(date +%Y%m%d)" bash -c '\ - ./scripts/execute-gate.sh workflow-audit --receipt-dir "$RECEIPT_DIR" && \ - ./scripts/execute-gate.sh no-nested-lock --receipt-dir "$RECEIPT_DIR" && \ - ./scripts/execute-gate.sh format --receipt-dir "$RECEIPT_DIR" && \ - ./scripts/execute-gate.sh clippy-lib --receipt-dir "$RECEIPT_DIR" && \ - ./scripts/execute-gate.sh test-lib --receipt-dir "$RECEIPT_DIR" && \ - ./scripts/execute-gate.sh policy --receipt-dir "$RECEIPT_DIR" && \ - ./scripts/execute-gate.sh lsp-definition --receipt-dir "$RECEIPT_DIR" \ - ' + @for gate in workflow-audit no-nested-lock format clippy-lib test-lib policy lsp-definition; do \ + cargo xtask gates --gate "$${gate}" --receipt --receipt-path ".receipts/$(date +%Y%m%d)/$${gate}.json" || exit 1; \ + done @echo "✅ Merge gate passed with receipts!" @echo "📁 Receipts: .receipts/$(date +%Y%m%d)/" # Gate execution for individual gate (with receipt) gate-execute gate_id: - @./scripts/execute-gate.sh {{gate_id}} --receipt-dir .receipts/$(date +%Y%m%d) + @cargo xtask gates --gate "{{gate_id}}" --receipt --receipt-path ".receipts/$(date +%Y%m%d)/{{gate_id}}.json" # Show gate registry gate-list: - @python3 scripts/list-gates.py + @cargo xtask gates --list # ============================================================================ # Release Gate (Slice C: Release candidate validation) @@ -1598,12 +1617,19 @@ release-build: # Version sync check (Slice B: single source of version truth) version-check: - @echo "Checking version sync..." - @bash scripts/check-version-sync.sh + @cargo xtask check-version-sync # Turnkey PR-driven release orchestrator for 0.x.y releases. -release-turnkey VERSION: - @bash scripts/release-turnkey-pr.sh "{{VERSION}}" +release-turnkey VERSION *ARGS="": + @cargo xtask release-turnkey "{{VERSION}}" {{ARGS}} + +# Dispatch publish-to-crates.io workflow for a release version. +publish-release VERSION *ARGS="": + @cargo xtask publish-release "{{VERSION}}" {{ARGS}} + +# Run post-release installed-binary smoke test for a release version. +smoke-test-release VERSION: + @cargo xtask smoke-test-release "{{VERSION}}" # Release gate: full validation for release candidates (~10 min) # Composes: ci-gate + release-specific checks diff --git a/scripts/DEPRECATED_RELEASE_SCRIPTS.md b/scripts/DEPRECATED_RELEASE_SCRIPTS.md index 815ad620ec..331d8ec74c 100644 --- a/scripts/DEPRECATED_RELEASE_SCRIPTS.md +++ b/scripts/DEPRECATED_RELEASE_SCRIPTS.md @@ -2,9 +2,9 @@ ## Authoritative path for RC orchestration -Use `scripts/release-turnkey-pr.sh` for the supported release orchestration flow -(or the equivalent `just release-turnkey`/xtask flow). This is the canonical -entrypoint for RC-style releases. +Use `just release-turnkey` (equivalent `cargo xtask release-turnkey`) for the +supported release orchestration flow. This is the canonical entrypoint for +RC-style releases. ## Deprecated / removed legacy scripts @@ -14,3 +14,4 @@ current release operations: - `scripts/release.sh` (removed, legacy pre-flows) - `scripts/release-ga.sh` (removed, legacy GA helper) - `scripts/publish-v0.8.3.sh` (removed, historical one-off v0.8.3 helper) +- `scripts/release-turnkey-pr.sh` (legacy orchestration wrapper; retained for compatibility) diff --git a/scripts/build-timing-receipt.sh b/scripts/build-timing-receipt.sh index 56c2c49ec8..32e5eaa6aa 100755 --- a/scripts/build-timing-receipt.sh +++ b/scripts/build-timing-receipt.sh @@ -1,197 +1,3 @@ #!/usr/bin/env bash -# Build timing receipt script for Perl LSP microcrate modularization -# Usage: -# ./scripts/build-timing-receipt.sh [--clean] [--incremental] [--tests] [--output FILE] [--baseline] -# -# Options: -# --clean Run clean build (cargo clean first) -# --incremental Run incremental build (touch a file in target crate) -# --tests Run test build -# --output FILE Output file (default: artifacts/build-timing-receipt.json) -# --baseline Save as baseline (artifacts/build-timing-baseline.json) -set -euo pipefail - -# Set locale to C for consistent number formatting (prevent comma separators) -export LC_ALL=C - -PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -ARTIFACTS_DIR="${PROJECT_ROOT}/artifacts" -mkdir -p "${ARTIFACTS_DIR}" - -# Default values -RUN_CLEAN=false -RUN_INCREMENTAL=false -RUN_TESTS=false -OUTPUT_FILE="${ARTIFACTS_DIR}/build-timing-receipt.json" -IS_BASELINE=false - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - --clean) - RUN_CLEAN=true - shift - ;; - --incremental) - RUN_INCREMENTAL=true - shift - ;; - --tests) - RUN_TESTS=true - shift - ;; - --output) - OUTPUT_FILE="$2" - shift 2 - ;; - --baseline) - IS_BASELINE=true - OUTPUT_FILE="${ARTIFACTS_DIR}/build-timing-baseline.json" - shift - ;; - --help) - echo "Usage: $0 [--clean] [--incremental] [--tests] [--output FILE] [--baseline]" - echo "" - echo "Options:" - echo " --clean Run clean build (cargo clean first)" - echo " --incremental Run incremental build (touch a file in target crate)" - echo " --tests Run test build" - echo " --output FILE Output file (default: artifacts/build-timing-receipt.json)" - echo " --baseline Save as baseline (artifacts/build-timing-baseline.json)" - echo "" - echo "Examples:" - echo " $0 --clean --incremental --tests --baseline" - echo " $0 --clean --output my-timing.json" - echo " $0 --incremental" - exit 0 - ;; - *) - echo "Unknown option: $1" >&2 - echo "Use --help for usage information" >&2 - exit 1 - ;; - esac -done - -# If no specific options provided, run all measurements -if [[ "$RUN_CLEAN" == false && "$RUN_INCREMENTAL" == false && "$RUN_TESTS" == false ]]; then - RUN_CLEAN=true - RUN_INCREMENTAL=true - RUN_TESTS=true -fi - -cd "${PROJECT_ROOT}" - -# Gather system information -TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") -TOOLCHAIN=$(rustc --version 2>/dev/null || echo "unknown") -CPU_CORES=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo "unknown") -MEMORY_GB=$(free -g 2>/dev/null | awk '/^Mem:/{print $2}' || sysctl -n hw.memsize 2>/dev/null | awk '{print int($1/1024/1024/1024)}' || echo "unknown") -OS=$(uname -s -r 2>/dev/null || echo "unknown") - -# Start building JSON output -JSON_OUTPUT="{" -JSON_OUTPUT+='"timestamp":"'"${TIMESTAMP}"'",' -JSON_OUTPUT+='"toolchain":"'"${TOOLCHAIN}"'",' -JSON_OUTPUT+='"system":{' -JSON_OUTPUT+='"cpu_cores":'"${CPU_CORES}"',' -JSON_OUTPUT+='"memory_gb":'"${MEMORY_GB}"',' -JSON_OUTPUT+='"os":"'"${OS}"'"' -JSON_OUTPUT+='},' -JSON_OUTPUT+='"measurements":{' - -# Temporary file for measurements -MEASUREMENTS_FILE=$(mktemp) - -# Function to run a command and measure its time -measure_time() { - local name="$1" - local command="$2" - local pre_command="${3:-}" - - echo "=== Measuring: ${name} ===" - echo "Command: ${command}" - - # Run pre-command if provided - if [[ -n "${pre_command}" ]]; then - echo "Pre-command: ${pre_command}" - eval "${pre_command}" > /dev/null 2>&1 || true - fi - - # Measure time using bash's built-in time - local start_time end_time elapsed - start_time=$(date +%s.%N) - eval "${command}" > /dev/null 2>&1 || true - end_time=$(date +%s.%N) - elapsed=$(echo "${end_time} - ${start_time}" | bc) - - echo "Duration: ${elapsed}s" - echo "" - - # Escape the command for JSON - local escaped_command - escaped_command=$(echo "${command}" | sed 's/"/\\"/g') - - # Write measurement to temp file as single line - echo '"'"${name}"'":{"duration_seconds":'"${elapsed}"',"command":"'"${escaped_command}"'"}' >> "${MEASUREMENTS_FILE}" -} - -# Run clean build measurement -if [[ "$RUN_CLEAN" == true ]]; then - measure_time "clean_build_workspace" "cargo build --workspace --locked" "cargo clean" -fi - -# Run incremental build measurement -if [[ "$RUN_INCREMENTAL" == true ]]; then - # First ensure we have a clean build to measure from - cargo build --workspace --locked > /dev/null 2>&1 || true - - # Touch a file in perl-lsp-providers to trigger incremental rebuild - if [[ -d "${PROJECT_ROOT}/crates/perl-lsp-providers" ]]; then - touch "${PROJECT_ROOT}/crates/perl-lsp-providers/src/lib.rs" 2>/dev/null || true - measure_time "incremental_build_providers" "cargo build -p perl-lsp-providers --locked" - else - # If perl-lsp-providers doesn't exist yet, measure incremental on perl-parser - touch "${PROJECT_ROOT}/crates/perl-parser/src/lib.rs" 2>/dev/null || true - measure_time "incremental_build_parser" "cargo build -p perl-parser --locked" - fi -fi - -# Run test build measurement -if [[ "$RUN_TESTS" == true ]]; then - measure_time "test_build_workspace" "cargo test --workspace --lib --locked" -fi - -# Read measurements from temp file and build final JSON -MEASUREMENTS_JSON=$(cat "${MEASUREMENTS_FILE}" | paste -sd ',') - -# Clean up temp file -rm -f "${MEASUREMENTS_FILE}" - -# Build final JSON -FINAL_JSON="{" -FINAL_JSON+='"timestamp":"'"${TIMESTAMP}"'",' -FINAL_JSON+='"toolchain":"'"${TOOLCHAIN}"'",' -FINAL_JSON+='"system":{' -FINAL_JSON+='"cpu_cores":'"${CPU_CORES}"',' -FINAL_JSON+='"memory_gb":'"${MEMORY_GB}"',' -FINAL_JSON+='"os":"'"${OS}"'"' -FINAL_JSON+='},' -FINAL_JSON+='"measurements":{'"${MEASUREMENTS_JSON}"'}' -FINAL_JSON+='}' - -# Write output to file -echo "${FINAL_JSON}" | jq '.' > "${OUTPUT_FILE}" - -echo "=== Build Timing Receipt Generated ===" -echo "Output: ${OUTPUT_FILE}" -echo "" -cat "${OUTPUT_FILE}" - -if [[ "$IS_BASELINE" == true ]]; then - echo "" - echo "Baseline saved to: ${OUTPUT_FILE}" - echo "Use this baseline to compare against future measurements:" - echo " ./scripts/compare-build-timing.sh ${OUTPUT_FILE} " -fi +exec cargo xtask build-timing-receipt "$@" diff --git a/scripts/check-doc-claims.py b/scripts/check-doc-claims.py index 8789dcbd6b..2813636345 100755 --- a/scripts/check-doc-claims.py +++ b/scripts/check-doc-claims.py @@ -1,98 +1,11 @@ #!/usr/bin/env python3 -"""Check that article inline claims match values in PUBLICATION_FACTS_LEDGER.md. -Flags known-stale string patterns found in docs/articles/*.md. -This is a claim-match linter: it checks known-wrong values, not all numbers. +"""Compatibility shim for `cargo xtask doc-claims`.""" -Exit code 0 = clean. -Exit code 1 = stale claims detected. -""" - -from __future__ import annotations - -import pathlib -import sys - -ROOT = pathlib.Path(__file__).resolve().parents[1] -ARTICLES_DIR = ROOT / "docs" / "articles" - -# Each entry: (stale_pattern, correct_replacement, description) -# These are patterns that are known to be wrong based on PUBLICATION_FACTS_LEDGER.md. -# Only scan docs/articles/*.md (not docs/articles/research/ — historical notes). -STALE_PATTERNS: list[tuple[str, str, str]] = [ - # Lines-of-Rust claims - ("563,228 lines", "591,034 lines", "LOC claim (563K is stale; ledger: 591,034)"), - ("563K lines", "591K lines", "LOC claim (563K is stale; ledger: 591K)"), - ("546,000", "591,034", "LOC claim (546K is stale; ledger: 591,034)"), - ("546K lines", "591K lines", "LOC claim (546K is stale; ledger: 591K)"), - # Workspace crate counts - ("131 crates", "133 crates", "Crate count (131 is stale; ledger: 133)"), - ("131 workspace crates", "133 workspace crates", "Crate count (131 is stale; ledger: 133)"), - ("132 workspace crates", "133 workspace crates", "Crate count (132 is stale; ledger: 133)"), - ("132 crates", "133 crates", "Crate count (132 is stale; ledger: 133)"), - # LSP feature counts - ("97 LSP and DAP features", "98 LSP and DAP features", "Feature count (97 is stale; ledger: 98)"), - ("97 LSP/DAP features", "98 LSP/DAP features", "Feature count (97 is stale; ledger: 98)"), - ("97 features defined", "98 features defined", "Feature count (97 is stale; ledger: 98)"), - ("97 features governed", "98 features governed", "Feature count (97 is stale; ledger: 98)"), - ("97 features:", "98 features:", "Feature count (97 is stale; ledger: 98)"), - # Commit counts - ("2,700+ commits", "3,200+ commits", "Commit count (2,700+ is stale; ledger: 3,210)"), - # PR counts - ("2,200+ pull requests", "2,646+ pull requests", "PR count (2,200+ is stale; ledger: 2,646+)"), - ("2,200+ PRs", "2,646+ PRs", "PR count (2,200+ is stale; ledger: 2,646+)"), -] - - -def scan_articles() -> list[tuple[pathlib.Path, int, str, str, str]]: - """Scan docs/articles/*.md for stale patterns. - - Returns list of (file, line_number, matched_pattern, replacement, description). - """ - hits: list[tuple[pathlib.Path, int, str, str, str]] = [] - - # Only scan top-level articles, not research/ subdirectory (historical notes) - for md_file in sorted(ARTICLES_DIR.glob("*.md")): - text = md_file.read_text(encoding="utf-8") - lines = text.splitlines() - for lineno, line in enumerate(lines, start=1): - for stale, replacement, description in STALE_PATTERNS: - if stale in line: - hits.append((md_file, lineno, stale, replacement, description)) - - return hits - - -def main() -> int: - hits = scan_articles() - - if hits: - print("DOC CLAIM VIOLATIONS:", file=sys.stderr) - print("=" * 60, file=sys.stderr) - for filepath, lineno, stale, replacement, description in hits: - rel = filepath.relative_to(ROOT) - print(f" {rel}:{lineno}: {description}", file=sys.stderr) - print(f" found: {stale!r}", file=sys.stderr) - print(f" expected: {replacement!r}", file=sys.stderr) - print("=" * 60, file=sys.stderr) - print( - f"\n{len(hits)} stale claim(s) found in docs/articles/.", - file=sys.stderr, - ) - print( - "\nTo fix: update the article to match docs/project/PUBLICATION_FACTS_LEDGER.md", - file=sys.stderr, - ) - return 1 - - article_count = len(list(ARTICLES_DIR.glob("*.md"))) - pattern_count = len(STALE_PATTERNS) - print( - f"Doc claims OK: {article_count} articles scanned, " - f"{pattern_count} stale patterns checked, 0 violations found" - ) - return 0 +import subprocess +from pathlib import Path if __name__ == "__main__": - raise SystemExit(main()) + repo_root = Path(__file__).resolve().parents[1] + raise SystemExit(subprocess.call(["cargo", "xtask", "doc-claims"], cwd=repo_root)) diff --git a/scripts/check-version-sync.sh b/scripts/check-version-sync.sh index 510087b62a..9dddd033e8 100755 --- a/scripts/check-version-sync.sh +++ b/scripts/check-version-sync.sh @@ -3,10 +3,6 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -BIN="${REPO_ROOT}/target/debug/perl-ci-hygiene" +cd "$REPO_ROOT" -if [ -x "$BIN" ]; then - exec "$BIN" check-version-sync -fi - -exec cargo run --quiet --manifest-path "$REPO_ROOT/Cargo.toml" -p perl-ci-hygiene -- check-version-sync +exec cargo xtask check-version-sync "$@" diff --git a/scripts/check_features_invariants.py b/scripts/check_features_invariants.py index 35262bb092..be306b9506 100755 --- a/scripts/check_features_invariants.py +++ b/scripts/check_features_invariants.py @@ -1,104 +1,13 @@ #!/usr/bin/env python3 -"""Validate features.toml invariants for truth surface integrity. -This script enforces rules that prevent the feature catalog from -accidentally misrepresenting the project's capabilities: +"""Compatibility shim for `cargo xtask features invariants`.""" -1. GA + advertised => tests must be non-empty (no claiming untested features) -2. No duplicate feature IDs -3. advertised=true + counts_in_coverage=true is the headline metric - -Exit code 0 = all invariants pass -Exit code 1 = invariant violations found -""" - -from __future__ import annotations - -import pathlib -import sys -from typing import Any - -try: - import tomllib -except ImportError: - import tomli as tomllib - - -ROOT = pathlib.Path(__file__).resolve().parents[1] -FEATURES_TOML = ROOT / "features.toml" - - -def check_invariants(features: list[dict[str, Any]]) -> list[str]: - """Check all invariants and return list of violations.""" - violations: list[str] = [] - seen_ids: set[str] = set() - - for i, f in enumerate(features): - fid = f.get("id", f"") - advertised = f.get("advertised", False) - maturity = f.get("maturity", "planned") - tests = f.get("tests", []) - counts_in_coverage = f.get("counts_in_coverage", True) - - # Rule 1: No duplicate IDs - if fid in seen_ids: - violations.append(f"DUPLICATE_ID: {fid!r} appears more than once") - seen_ids.add(fid) - - # Rule 2: GA + advertised => tests must be non-empty - # This prevents claiming "GA" status for untested features - if advertised and maturity == "ga" and not tests: - # Exception: features with counts_in_coverage=false are protocol/plumbing - # and can be GA without tests (they're not in the headline metric) - if counts_in_coverage is not False: - violations.append( - f"UNTESTED_GA: {fid!r} is advertised+GA but has no tests. " - f"Either add tests or set counts_in_coverage=false (if it's protocol plumbing)." - ) - - # Rule 3: Warn if advertised but not counting in coverage - # (This is allowed but worth noting) - if advertised and counts_in_coverage is False and maturity == "ga": - # This is fine - just informational - pass - - return violations - - -def main() -> int: - data = tomllib.loads(FEATURES_TOML.read_text(encoding="utf-8")) - features = data.get("feature", []) - - violations = check_invariants(features) - - if violations: - print("FEATURE INVARIANT VIOLATIONS:", file=sys.stderr) - print("=" * 50, file=sys.stderr) - for v in violations: - print(f" - {v}", file=sys.stderr) - print("=" * 50, file=sys.stderr) - print(f"\n{len(violations)} violation(s) found.", file=sys.stderr) - print("\nTo fix:", file=sys.stderr) - print(" - DUPLICATE_ID: Remove duplicate entries from features.toml", file=sys.stderr) - print(" - UNTESTED_GA: Either add tests[] entries or set counts_in_coverage=false", file=sys.stderr) - return 1 - - # Print summary - total = len(features) - ga_advertised = sum( - 1 for f in features - if f.get("advertised") and f.get("maturity") == "ga" - ) - headline_features = sum( - 1 for f in features - if f.get("advertised") - and f.get("maturity") == "ga" - and f.get("counts_in_coverage", True) is not False - ) - - print(f"Feature invariants OK: {total} features, {ga_advertised} GA+advertised, {headline_features} in headline metric") - return 0 +import subprocess +from pathlib import Path if __name__ == "__main__": - raise SystemExit(main()) + repo_root = Path(__file__).resolve().parents[1] + raise SystemExit( + subprocess.call(["cargo", "xtask", "features", "invariants"], cwd=repo_root) + ) diff --git a/scripts/ci-audit-workflows.py b/scripts/ci-audit-workflows.py index d913eaeb14..cb8a8e8061 100755 --- a/scripts/ci-audit-workflows.py +++ b/scripts/ci-audit-workflows.py @@ -1,123 +1,11 @@ #!/usr/bin/env python3 -""" -CI Workflow Spend Audit -Enforces that expensive jobs in GitHub Actions workflows are gated behind labels -or workflow_dispatch, preventing silent spend regression on PRs. +"""Compatibility shim for `cargo xtask ci-audit-workflows`.""" -Exit codes: - 0 - All jobs are properly gated or allowlisted - 1 - Found ungated expensive jobs (potential spend risk) -""" - -import sys -import yaml +import subprocess from pathlib import Path -# Workflows that are allowed to have ungated jobs (cheap/essential) -ALLOWED_WORKFLOWS = { - "ci.yml", # Core fast gate (fmt → clippy → test → docs) - "check-ignored.yml", # Cheap check -} - -# Jobs that are allowed to run ungated (very cheap, ~seconds) -ALLOWED_UNGATED_JOBS = { - "tautology-check", # ~1s grep-based check - "test-metrics", # ~2s metric counting - "fmt", # Fast rustfmt check - "clippy", # Fast lint (when cached) -} - - -def parse_workflow(path: Path) -> dict: - """Parse a workflow YAML file.""" - with open(path) as f: - return yaml.safe_load(f) - - -def has_pr_trigger(workflow: dict) -> bool: - """Check if workflow runs on pull_request events.""" - on_field = workflow.get("on", {}) - if isinstance(on_field, list): - return "pull_request" in on_field - if isinstance(on_field, dict): - return "pull_request" in on_field - if isinstance(on_field, str): - return on_field == "pull_request" - return False - - -def is_gated(job: dict) -> bool: - """Check if a job has an if: condition (gated).""" - return "if" in job - - -def audit_workflows(workflows_dir: Path) -> list[str]: - """Audit all workflows for ungated expensive jobs.""" - violations = [] - - for wf_path in workflows_dir.glob("*.yml"): - wf_name = wf_path.name - - # Skip allowlisted workflows - if wf_name in ALLOWED_WORKFLOWS: - continue - - try: - workflow = parse_workflow(wf_path) - except yaml.YAMLError as e: - violations.append(f"{wf_name}: YAML parse error: {e}") - continue - - if not workflow: - continue - - # Only check workflows with PR triggers - if not has_pr_trigger(workflow): - continue - - jobs = workflow.get("jobs", {}) - for job_name, job_config in jobs.items(): - # Skip allowlisted jobs - if job_name in ALLOWED_UNGATED_JOBS: - continue - - if not isinstance(job_config, dict): - continue - - if not is_gated(job_config): - violations.append( - f"{wf_name}:{job_name} - runs on PRs without if: condition" - ) - - return violations - - -def main(): - workflows_dir = Path(".github/workflows") - - if not workflows_dir.exists(): - print("✓ No .github/workflows directory found") - return 0 - - violations = audit_workflows(workflows_dir) - - if violations: - print("❌ CI Spend Audit Failed") - print() - print("The following jobs run on every PR without gating:") - for v in violations: - print(f" - {v}") - print() - print("Fix by adding one of:") - print(" 1. if: contains(github.event.pull_request.labels.*.name, 'ci: