Compare commits
19 Commits
ralph/feat
...
fix/metric
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5db033fdc | ||
|
|
cd2da6a1ec | ||
|
|
1f9dfdd934 | ||
|
|
aec5a80cfb | ||
|
|
d38b470572 | ||
|
|
6c54a2821d | ||
|
|
320cc6e6a0 | ||
|
|
c86158c911 | ||
|
|
ce66b069e5 | ||
|
|
2071109258 | ||
|
|
dea5ddbebd | ||
|
|
6011fe9cf1 | ||
|
|
604b3d6702 | ||
|
|
f29ac02ac5 | ||
|
|
f8aaaabace | ||
|
|
4e4c73faf5 | ||
|
|
e3d2ac5a7c | ||
|
|
34b76ab2fa | ||
|
|
83d5b22ca9 |
157
.github/scripts/parse-metrics.mjs
vendored
Normal file
157
.github/scripts/parse-metrics.mjs
vendored
Normal file
@@ -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();
|
||||||
86
.github/workflows/weekly-metrics-discord.yml
vendored
86
.github/workflows/weekly-metrics-discord.yml
vendored
@@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
issues: read
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -17,15 +17,25 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_METRICS_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_METRICS_WEBHOOK }}
|
||||||
steps:
|
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: |
|
run: |
|
||||||
# Last 7 days
|
set -Eeuo pipefail
|
||||||
first_day=$(date -d "7 days ago" +%Y-%m-%d)
|
# Last 14 days
|
||||||
|
first_day=$(date -d "14 days ago" +%Y-%m-%d)
|
||||||
last_day=$(date +%Y-%m-%d)
|
last_day=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
echo "first_day=$first_day" >> $GITHUB_ENV
|
echo "first_day=$first_day" >> $GITHUB_ENV
|
||||||
echo "last_day=$last_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 "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
|
- name: Generate issue metrics
|
||||||
uses: github/issue-metrics@v3
|
uses: github/issue-metrics@v3
|
||||||
@@ -34,40 +44,39 @@ jobs:
|
|||||||
SEARCH_QUERY: "repo:${{ github.repository }} is:issue created:${{ env.first_day }}..${{ env.last_day }}"
|
SEARCH_QUERY: "repo:${{ github.repository }} is:issue created:${{ env.first_day }}..${{ env.last_day }}"
|
||||||
HIDE_TIME_TO_ANSWER: true
|
HIDE_TIME_TO_ANSWER: true
|
||||||
HIDE_LABEL_METRICS: false
|
HIDE_LABEL_METRICS: false
|
||||||
|
OUTPUT_FILE: issue_metrics.md
|
||||||
|
|
||||||
- name: Generate PR metrics
|
- name: Generate PR created metrics
|
||||||
uses: github/issue-metrics@v3
|
uses: github/issue-metrics@v3
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
SEARCH_QUERY: "repo:${{ github.repository }} is:pr created:${{ env.first_day }}..${{ env.last_day }}"
|
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
|
- name: Parse metrics
|
||||||
id: metrics
|
id: metrics
|
||||||
run: |
|
run: node .github/scripts/parse-metrics.mjs
|
||||||
# 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
|
|
||||||
|
|
||||||
- name: Send to Discord
|
- name: Send to Discord
|
||||||
uses: sarisia/actions-status-discord@v1
|
uses: sarisia/actions-status-discord@v1
|
||||||
@@ -78,19 +87,22 @@ jobs:
|
|||||||
title: "📊 Weekly Metrics Report"
|
title: "📊 Weekly Metrics Report"
|
||||||
description: |
|
description: |
|
||||||
**${{ env.week_of }}**
|
**${{ env.week_of }}**
|
||||||
|
*${{ env.date_range }}*
|
||||||
|
|
||||||
**🎯 Issues**
|
**🎯 Issues**
|
||||||
• Created: ${{ steps.metrics.outputs.issues_created }}
|
• Created: ${{ steps.metrics.outputs.issues_created }}
|
||||||
• Closed: ${{ steps.metrics.outputs.issues_closed }}
|
• 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**
|
**🔀 Pull Requests**
|
||||||
• Created: ${{ steps.metrics.outputs.prs_created }}
|
• Created: ${{ steps.metrics.outputs.prs_created }}
|
||||||
• Merged: ${{ steps.metrics.outputs.prs_merged }}
|
• Merged: ${{ steps.metrics.outputs.prs_merged }}
|
||||||
|
• Avg Response Time: ${{ steps.metrics.outputs.pr_avg_first_response }}
|
||||||
**⏱️ Response Times**
|
• Avg Time to Merge: ${{ steps.metrics.outputs.pr_avg_merge_time }}
|
||||||
• First Response: ${{ steps.metrics.outputs.avg_first_response }}
|
|
||||||
• Time to Close: ${{ steps.metrics.outputs.avg_time_to_close }}
|
**📈 Visual Analytics**
|
||||||
• PR Merge Time: ${{ steps.metrics.outputs.pr_avg_merge_time }}
|
https://repobeats.axiom.co/api/embed/b439f28f0ab5bd7a2da19505355693cd2c55bfd4.svg
|
||||||
color: 0x58AFFF
|
color: 0x58AFFF
|
||||||
username: Task Master Metrics Bot
|
username: Task Master Metrics Bot
|
||||||
avatar_url: https://raw.githubusercontent.com/eyaltoledano/claude-task-master/main/images/logo.png
|
avatar_url: https://raw.githubusercontent.com/eyaltoledano/claude-task-master/main/images/logo.png
|
||||||
|
|||||||
Reference in New Issue
Block a user