From fd9dd43ee0e94863975ff34ccbc4801e9eece6dc Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:47:03 +0200 Subject: [PATCH] fix: improve weekly metrics workflow with 14-day window and debug output (#1226) --- .github/scripts/parse-metrics.mjs | 157 +++++++++++++++++++ .github/workflows/weekly-metrics-discord.yml | 86 +++++----- 2 files changed, 206 insertions(+), 37 deletions(-) create mode 100644 .github/scripts/parse-metrics.mjs diff --git a/.github/scripts/parse-metrics.mjs b/.github/scripts/parse-metrics.mjs new file mode 100644 index 00000000..844d0da2 --- /dev/null +++ b/.github/scripts/parse-metrics.mjs @@ -0,0 +1,157 @@ +#!/usr/bin/env node + +import { readFileSync, existsSync, writeFileSync } from 'fs'; + +function parseMetricsTable(content, metricName) { + const lines = content.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + // Match a markdown table row like: | Metric Name | value | ... + const safeName = metricName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const re = new RegExp(`^\\|\\s*${safeName}\\s*\\|\\s*([^|]+)\\|?`); + const match = line.match(re); + if (match) { + return match[1].trim() || 'N/A'; + } + } + return 'N/A'; +} + +function parseCountMetric(content, metricName) { + const result = parseMetricsTable(content, metricName); + // Extract number from string, handling commas and spaces + const numberMatch = result.toString().match(/[\d,]+/); + if (numberMatch) { + const number = parseInt(numberMatch[0].replace(/,/g, '')); + return isNaN(number) ? 0 : number; + } + return 0; +} + +function main() { + const metrics = { + issues_created: 0, + issues_closed: 0, + prs_created: 0, + prs_merged: 0, + issue_avg_first_response: 'N/A', + issue_avg_time_to_close: 'N/A', + pr_avg_first_response: 'N/A', + pr_avg_merge_time: 'N/A' + }; + + // Parse issue metrics + if (existsSync('issue_metrics.md')) { + console.log('📄 Found issue_metrics.md, parsing...'); + const issueContent = readFileSync('issue_metrics.md', 'utf8'); + + metrics.issues_created = parseCountMetric( + issueContent, + 'Total number of items created' + ); + metrics.issues_closed = parseCountMetric( + issueContent, + 'Number of items closed' + ); + metrics.issue_avg_first_response = parseMetricsTable( + issueContent, + 'Time to first response' + ); + metrics.issue_avg_time_to_close = parseMetricsTable( + issueContent, + 'Time to close' + ); + } else { + console.warn('[parse-metrics] issue_metrics.md not found; using defaults.'); + } + + // Parse PR created metrics + if (existsSync('pr_created_metrics.md')) { + console.log('📄 Found pr_created_metrics.md, parsing...'); + const prCreatedContent = readFileSync('pr_created_metrics.md', 'utf8'); + + metrics.prs_created = parseCountMetric( + prCreatedContent, + 'Total number of items created' + ); + metrics.pr_avg_first_response = parseMetricsTable( + prCreatedContent, + 'Time to first response' + ); + } else { + console.warn( + '[parse-metrics] pr_created_metrics.md not found; using defaults.' + ); + } + + // Parse PR merged metrics (for more accurate merge data) + if (existsSync('pr_merged_metrics.md')) { + console.log('📄 Found pr_merged_metrics.md, parsing...'); + const prMergedContent = readFileSync('pr_merged_metrics.md', 'utf8'); + + metrics.prs_merged = parseCountMetric( + prMergedContent, + 'Total number of items created' + ); + // For merged PRs, "Time to close" is actually time to merge + metrics.pr_avg_merge_time = parseMetricsTable( + prMergedContent, + 'Time to close' + ); + } else { + console.warn( + '[parse-metrics] pr_merged_metrics.md not found; falling back to pr_metrics.md.' + ); + // Fallback: try old pr_metrics.md if it exists + if (existsSync('pr_metrics.md')) { + console.log('📄 Falling back to pr_metrics.md...'); + const prContent = readFileSync('pr_metrics.md', 'utf8'); + + const mergedCount = parseCountMetric(prContent, 'Number of items merged'); + metrics.prs_merged = + mergedCount || parseCountMetric(prContent, 'Number of items closed'); + + const maybeMergeTime = parseMetricsTable( + prContent, + 'Average time to merge' + ); + metrics.pr_avg_merge_time = + maybeMergeTime !== 'N/A' + ? maybeMergeTime + : parseMetricsTable(prContent, 'Time to close'); + } else { + console.warn('[parse-metrics] pr_metrics.md not found; using defaults.'); + } + } + + // Output for GitHub Actions + const output = Object.entries(metrics) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + + // Always output to stdout for debugging + console.log('\n=== FINAL METRICS ==='); + Object.entries(metrics).forEach(([key, value]) => { + console.log(`${key}: ${value}`); + }); + + // Write to GITHUB_OUTPUT if in GitHub Actions + if (process.env.GITHUB_OUTPUT) { + try { + writeFileSync(process.env.GITHUB_OUTPUT, output + '\n', { flag: 'a' }); + console.log( + `\nSuccessfully wrote metrics to ${process.env.GITHUB_OUTPUT}` + ); + } catch (error) { + console.error(`Failed to write to GITHUB_OUTPUT: ${error.message}`); + process.exit(1); + } + } else { + console.log( + '\nNo GITHUB_OUTPUT environment variable found, skipping file write' + ); + } +} + +main(); diff --git a/.github/workflows/weekly-metrics-discord.yml b/.github/workflows/weekly-metrics-discord.yml index 8a638b36..fdf461f7 100644 --- a/.github/workflows/weekly-metrics-discord.yml +++ b/.github/workflows/weekly-metrics-discord.yml @@ -8,7 +8,7 @@ on: permissions: contents: read - issues: write + issues: read pull-requests: read jobs: @@ -17,15 +17,25 @@ jobs: env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_METRICS_WEBHOOK }} steps: - - name: Get dates for last week + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Get dates for last 14 days run: | - # Last 7 days - first_day=$(date -d "7 days ago" +%Y-%m-%d) + set -Eeuo pipefail + # Last 14 days + first_day=$(date -d "14 days ago" +%Y-%m-%d) last_day=$(date +%Y-%m-%d) echo "first_day=$first_day" >> $GITHUB_ENV echo "last_day=$last_day" >> $GITHUB_ENV echo "week_of=$(date -d '7 days ago' +'Week of %B %d, %Y')" >> $GITHUB_ENV + echo "date_range=Past 14 days ($first_day to $last_day)" >> $GITHUB_ENV - name: Generate issue metrics uses: github/issue-metrics@v3 @@ -34,40 +44,39 @@ jobs: SEARCH_QUERY: "repo:${{ github.repository }} is:issue created:${{ env.first_day }}..${{ env.last_day }}" HIDE_TIME_TO_ANSWER: true HIDE_LABEL_METRICS: false + OUTPUT_FILE: issue_metrics.md - - name: Generate PR metrics + - name: Generate PR created metrics uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: "repo:${{ github.repository }} is:pr created:${{ env.first_day }}..${{ env.last_day }}" - OUTPUT_FILE: pr_metrics.md + OUTPUT_FILE: pr_created_metrics.md + + - name: Generate PR merged metrics + uses: github/issue-metrics@v3 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SEARCH_QUERY: "repo:${{ github.repository }} is:pr is:merged merged:${{ env.first_day }}..${{ env.last_day }}" + OUTPUT_FILE: pr_merged_metrics.md + + - name: Debug generated metrics + run: | + set -Eeuo pipefail + echo "Listing markdown files in workspace:" + ls -la *.md || true + for f in issue_metrics.md pr_created_metrics.md pr_merged_metrics.md; do + if [ -f "$f" ]; then + echo "== $f (first 10 lines) ==" + head -n 10 "$f" + else + echo "Missing $f" + fi + done - name: Parse metrics id: metrics - run: | - # Parse the metrics from the generated markdown files - if [ -f "issue_metrics.md" ]; then - # Extract key metrics using grep/awk - AVG_TIME_TO_FIRST_RESPONSE=$(grep -A 1 "Average time to first response" issue_metrics.md | tail -1 | xargs || echo "N/A") - AVG_TIME_TO_CLOSE=$(grep -A 1 "Average time to close" issue_metrics.md | tail -1 | xargs || echo "N/A") - NUM_ISSUES_CREATED=$(grep -oP '\d+(?= issues created)' issue_metrics.md || echo "0") - NUM_ISSUES_CLOSED=$(grep -oP '\d+(?= issues closed)' issue_metrics.md || echo "0") - fi - - if [ -f "pr_metrics.md" ]; then - PR_AVG_TIME_TO_MERGE=$(grep -A 1 "Average time to close" pr_metrics.md | tail -1 | xargs || echo "N/A") - NUM_PRS_CREATED=$(grep -oP '\d+(?= pull requests created)' pr_metrics.md || echo "0") - NUM_PRS_MERGED=$(grep -oP '\d+(?= pull requests closed)' pr_metrics.md || echo "0") - fi - - # Set outputs for Discord action - echo "issues_created=${NUM_ISSUES_CREATED:-0}" >> $GITHUB_OUTPUT - echo "issues_closed=${NUM_ISSUES_CLOSED:-0}" >> $GITHUB_OUTPUT - echo "prs_created=${NUM_PRS_CREATED:-0}" >> $GITHUB_OUTPUT - echo "prs_merged=${NUM_PRS_MERGED:-0}" >> $GITHUB_OUTPUT - echo "avg_first_response=${AVG_TIME_TO_FIRST_RESPONSE:-N/A}" >> $GITHUB_OUTPUT - echo "avg_time_to_close=${AVG_TIME_TO_CLOSE:-N/A}" >> $GITHUB_OUTPUT - echo "pr_avg_merge_time=${PR_AVG_TIME_TO_MERGE:-N/A}" >> $GITHUB_OUTPUT + run: node .github/scripts/parse-metrics.mjs - name: Send to Discord uses: sarisia/actions-status-discord@v1 @@ -78,19 +87,22 @@ jobs: title: "📊 Weekly Metrics Report" description: | **${{ env.week_of }}** - + *${{ env.date_range }}* + **🎯 Issues** • Created: ${{ steps.metrics.outputs.issues_created }} • Closed: ${{ steps.metrics.outputs.issues_closed }} - + • Avg Response Time: ${{ steps.metrics.outputs.issue_avg_first_response }} + • Avg Time to Close: ${{ steps.metrics.outputs.issue_avg_time_to_close }} + **🔀 Pull Requests** • Created: ${{ steps.metrics.outputs.prs_created }} • Merged: ${{ steps.metrics.outputs.prs_merged }} - - **⏱️ Response Times** - • First Response: ${{ steps.metrics.outputs.avg_first_response }} - • Time to Close: ${{ steps.metrics.outputs.avg_time_to_close }} - • PR Merge Time: ${{ steps.metrics.outputs.pr_avg_merge_time }} + • Avg Response Time: ${{ steps.metrics.outputs.pr_avg_first_response }} + • Avg Time to Merge: ${{ steps.metrics.outputs.pr_avg_merge_time }} + + **📈 Visual Analytics** + https://repobeats.axiom.co/api/embed/b439f28f0ab5bd7a2da19505355693cd2c55bfd4.svg color: 0x58AFFF username: Task Master Metrics Bot avatar_url: https://raw.githubusercontent.com/eyaltoledano/claude-task-master/main/images/logo.png